[Git][debian-proftpd-team/proftpd-mod-tar][master] New upstream release: mod_tar 0.4.

Hilmar Preuße gitlab at salsa.debian.org
Tue May 1 22:00:23 BST 2018


Hilmar Preuße pushed to branch master at Debian ProFTPD Team / proftpd-mod-tar


Commits:
51b160e7 by Hilmar Preusse at 2018-05-01T22:57:55+02:00
New upstream release: mod_tar 0.4.

- - - - -


11 changed files:

- + .gitattributes
- + .travis.yml
- + README.md
- debian/changelog
- debian/control
- + doc/NOTES.libarchive
- mod_tar.c
- mod_tar.html
- + t/etc/modules/mod_tar/subdir.tar
- + t/lib/ProFTPD/Tests/Modules/mod_tar.pm
- + t/modules/mod_tar.t


Changes:

=====================================
.gitattributes
=====================================
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.pl linguist-language=C
+*.pm linguist-language=C


=====================================
.travis.yml
=====================================
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,35 @@
+language: c
+
+compiler:
+  - gcc
+  - clang
+
+install:
+  - sudo apt-get update -qq
+  # for libarchive
+  - sudo apt-get install -y libarchive-dev
+  # for libbz2
+  - sudo apt-get install -y libbz2-dev
+  # for unit tests
+  - sudo apt-get install -y check
+  # for static code analysis
+  - sudo apt-get install -y cppcheck rats
+  # for test code coverage
+  - sudo apt-get install -y lcov
+  - gem install coveralls-lcov
+
+before_script:
+  - cd ${TRAVIS_BUILD_DIR}
+  - lcov --directory . --zerocounters
+
+script:
+  # - find . -type f -name "*.c" -print | grep -v t\/ | xargs cppcheck 2>&1
+  # - find . -type f -name "*.c" -print | grep -v t\/ | xargs rats --language=c
+  - git clone https://github.com/proftpd/proftpd.git
+  - cp mod_tar.c proftpd/contrib/
+  - cd proftpd
+  - ./configure --enable-devel=coverage --enable-dso --enable-tests --with-shared=mod_tar
+  - make
+  - make clean
+  - ./configure --enable-devel=coverage --enable-tests --with-modules=mod_tar
+  - make


=====================================
README.md
=====================================
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+proftpd-mod_tar
+===============
+
+Status
+------
+[![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_tar.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_tar)
+[![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg)
+
+
+Synopsis
+--------
+The `mod_tar` module for ProFTPD provides for on-the-fly creation of
+tarballs for downloaded directories.
+
+For further module documentation, see [mod_tar.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_tar/blob/master/mod_tar.html).


=====================================
debian/changelog
=====================================
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+proftpd-mod-tar (0.4-1) UNRELEASED; urgency=medium
+
+  * New upstream release
+    - Fixed FTBFS on Hurd (Closes: #897383).
+    - Uses libarchive-dev instead of libtar-dev.
+
+ -- Francesco Paolo Lovergine <frankie at debian.org>  Tue, 01 May 2018 22:45:51 +0200
+
 proftpd-mod-tar (0.3.3-2) unstable; urgency=low
 
   * Removing libacl1-dev as BD and increasing proftpd-dev to (>= 1.3.4~rc3-2~)


=====================================
debian/control
=====================================
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: net
 Priority: optional
 Maintainer: ProFTPD Maintainance Team <pkg-proftpd-maintainers at lists.alioth.debian.org>
 Uploaders: Francesco Paolo Lovergine <frankie at debian.org>
-Build-Depends: debhelper (>= 9~), libtar-dev, proftpd-dev (>= 1.3.4~rc3-2~), libbz2-dev, zlib1g-dev
+Build-Depends: debhelper (>= 9~), proftpd-dev (>= 1.3.4~rc3-2~), libbz2-dev, zlib1g-dev, libarchive-dev
 Standards-Version: 4.1.4
 Homepage: http://www.castaglia.org/proftpd/modules/mod_tar.html
 Vcs-Browser: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar


=====================================
doc/NOTES.libarchive
=====================================
--- /dev/null
+++ b/doc/NOTES.libarchive
@@ -0,0 +1,131 @@
+
+  struct archive *a;
+
+  a = archive_write_new();
+
+  # Set compression: none, bzip2, or gzip
+  archive_write_set_compression_none(a);
+  archive_write_set_compression_bzip2(a);
+  archive_write_set_compression_gzip(a);
+
+  # Set format
+  archive_write_set_format_ustar(a);
+
+    OR
+
+  archive_write_set_format_pax_restricted(a); // Note 1
+
+    /* Libarchive's "pax restricted" format is a tar format that uses pax
+     * extensions only when absolutely necessary. Most of the time, it will
+     * write plain ustar entries. This is the recommended tar format for most
+     * uses. You should explicitly use ustar format only when you have to
+     * create archives that will be readable on older systems; you should
+     * explicitly request pax format only when you need to preserve as many
+     * attributes as possible. 
+     */
+
+  archive_write_set_format_zip(a);
+    // ? See caveats about ZIP64 format, 64-bit platforms:
+    //   libarchive/archive_write_set_format_zip.c
+
+Reading data from disk:
+
+  use archive_read_disk_set_gname_lookup(),
+      archive_read_disk_set_uname_lookup()
+
+    to set callbacks that libarchive will use to resolve UID/GID to names.
+    E.g. set pr_auth_get_pwuid(), pr_auth_get_grgid().  
+
+  use archive_read_disk_set_symlink_logical() (follows symlinks) or
+      archive_read_disk_set_symlink_physical() (does not follow symlinks)
+
+  use archive_read_disk_entry_from_file()?
+
+Example (from libarchive_read_disk(3)).  Note that the libarchive_write_open()
+man page has a better example:
+
+  struct archive *a;
+
+  a = archive_write_new();
+  if (a == NULL) {
+    ...
+  }
+
+  archive_write_open(a, custom_data, open_cb, write_cb, close_cb)
+
+  archive_write_open_filename(a, outname, 10240);
+
+  void add_file_to_archive(struct archive *a, const char *path) {
+
+    char buf[8K];
+    size_t nread;
+    struct archive *lookup;
+    struct archive_entry *entry;
+    int fd;
+
+    /* Create a lookup archive; set the callbacks we want to use. */ 
+    lookup = archive_read_disk_new();
+    archive_read_disk_set_standard_lookup(lookup);
+
+    entry = archive_entry_new();
+    fd = open(path, O_RDONLY);
+    if (fd < 0) {
+      /* cleanup */
+    }
+
+    archive_entry_copy_pathname(entry, path);
+
+    /* The last argument is a struct stat *.  If we provide that,
+     * then read_disk_entry_from_file() just copies the stat info
+     * it needs.  We can use this, do the pr_fsio_fstat() ourselves.
+     */
+    archive_read_disk_entry_from_file(lookup, entry fd, NULL);
+    archive_write_header(a, entry);
+
+    /* XXX If a regular file, copy the file contents */
+    nread = read(fd, buf, sizeof(buf));
+    while (nread > 0) {
+      /* Handle signals */
+
+      archive_write_data(a, buf, nread);
+      nread = read(fd, buf, sizeof(buf));
+    }
+
+    archive_write_finish_entry(a);
+
+    archive_entry_free(entry);
+    archive_read_free(lookup);
+
+    close(fd);
+  }
+
+  archive_write_close(a); // Note 4
+  archive_write_free(a); // Note 5
+
+This example creates a fresh archive_entry object for each file. For better
+performance, you can reuse the same archive_entry object by using
+`archive_entry_clear()` to erase it after each use. 
+
+Note 3: Size, file type, and pathname are all required attributes here. You
+can also use `archive_entry_copy_stat()` to copy all information from the
+`struct stat` to the archive entry, including file type. To get even more
+complete information, look at the `archive_read_disk` API, which provides an
+easy way to get more extensive file metadata---including ACLs and extended
+attributes on some systems---than using the system `stat()` system call. It
+also works on platforms such as Windows where `stat()` either doesn't exist or
+is broken. 
+
+  This suggests that mod_tar should use archive_entry_copy_stat(), and
+  have a TarOptions option for enabling the recording of extended attributes
+  (and would switch to using archive_read_disk()).
+
+Note 4: The free/finish call will implicitly call `archive_write_close()` if
+necessary. However, the close call returns an error code and the free/finish
+call does not, so if you rely on the implicit close, you won't be able to
+detect any errors that happen with the final writes.
+
+Note 5: Beginning with libarchive 3.0, this function is called
+`archive_write_free()`. The previous name was `archive_write_finish()`. If you
+want to write software compatible with libarchive 2.x and libarchive 3.x, you
+should use the old name, but be aware that it will be removed when
+libarchive 4.x is released. 


=====================================
mod_tar.c
=====================================
--- a/mod_tar.c
+++ b/mod_tar.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_tar
- * Copyright (c) 2009-2010 TJ Saunders
+ * Copyright (c) 2009-2017 TJ Saunders
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -14,25 +14,23 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
  *
  * As a special exemption, TJ Saunders and other respective copyright holders
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * $Id: mod_tar.c,v 1.7 2009/10/01 15:30:57 tj Exp tj $
- * $Libraries: -ltar -lz -lbz2 $
+ * $Libraries: -larchive -lz -lbz2$
  */
 
 #include "conf.h"
 #include "privs.h"
 
-#include <libtar.h>
-#include <zlib.h>
-#include <bzlib.h>
+#include <archive.h>
+#include <archive_entry.h>
 
-#define MOD_TAR_VERSION		"mod_tar/0.3.3"
+#define MOD_TAR_VERSION		"mod_tar/0.4"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030101
@@ -41,142 +39,187 @@
 
 module tar_module;
 
+/* Necessary prototype. */
+static void tar_exit_ev(const void *, void *);
+
 static int tar_engine = FALSE;
 static int tar_logfd = -1;
 
 static unsigned long tar_opts = 0UL;
 #define TAR_OPT_DEREF_SYMLINKS		0x001
 
-static const char *tar_tmp_path = "./";
+#define TAR_ARCHIVE_FL_USE_GZIP		0x001
+#define TAR_ARCHIVE_FL_USE_BZIP2	0x002
+#define TAR_ARCHIVE_FL_USE_USTAR	0x004
+#define TAR_ARCHIVE_FL_USE_PAX		0x008
+#define TAR_ARCHIVE_FL_USE_ZIP		0x010
 
+static const char *tar_tmp_path = "./";
 static char *tar_tmp_file = NULL;
 
-/* These are re-implementation of the tar_append_file() and tar_append_tree()
- * functions found in libtar.  We needed to implement them ourselves in order
- * to support options such as "deference", so that mod_tar's .tar files
- * follow symlinks (libtar's default behavior, hardcoded, is to NOT follow
- * symlink).
- */
-
-struct tar_dev {
-  dev_t td_dev;
-  libtar_hash_t *td_h;
+struct archive_data {
+  const char *path;
+  pr_fh_t *fh;
 };
-typedef struct tar_dev tar_dev_t;
 
-struct tar_ino {
-  ino_t ti_ino;
-  char ti_name[MAXPATHLEN];
-};
-typedef struct tar_ino tar_ino_t;
+static const char *trace_channel = "tar";
 
-/* Necessary prototype */
-static void tar_exit_ev(const void *, void *);
+static int append_data(pool *p, struct archive *tar,
+    struct archive_entry *entry, char *path, struct stat *st) {
+  pool *tmp_pool;
+  pr_fh_t *fh;
+  void *buf;
+  size_t buflen;
+  int res;
+  struct stat pst;
 
-static int append_file(TAR *t, char *real_name, char *save_name) {
-  struct stat st;
-  int i, res;
-  libtar_hashptr_t hp;
-  tar_dev_t *td = NULL;
-  tar_ino_t *ti = NULL;
-  char path[PR_TUNABLE_PATH_MAX+1];
+  fh = pr_fsio_open(path, O_RDONLY);
+  if (fh == NULL) {
+    int xerrno = errno;
 
-  if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
-    res = lstat(real_name, &st);
+    pr_trace_msg(trace_channel, 3, "unable to read '%s': %s", path,
+      strerror(xerrno));
 
-  } else {
-    res = stat(real_name, &st);
+    errno = xerrno;
+    return -1;
   }
 
-  if (res != 0)
+  res = pr_fsio_fstat(fh, &pst);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3, "unable to stat '%s': %s", path,
+      strerror(xerrno));
+
+    pr_fsio_close(fh);
+    errno = xerrno;
     return -1;
+  }
 
-  /* set header block */
-  memset(&(t->th_buf), 0, sizeof(struct tar_header));
-  th_set_from_stat(t, &st);
+  if (S_ISDIR(pst.st_mode)) {
+    int xerrno = EISDIR;
+  
+    pr_trace_msg(trace_channel, 3, "unable to use '%s': %s", path,
+      strerror(xerrno));
 
-  /* set the header path */
-  th_set_path(t, (save_name ? save_name : real_name));
+    pr_fsio_close(fh);
+    errno = xerrno;
+    return -1;
+  }
 
-  /* check if it's a hardlink */
-  libtar_hashptr_reset(&hp);
+  tmp_pool = make_sub_pool(p);
 
-  res = libtar_hash_getkey(t->h, &hp, &(st.st_dev),
-    (libtar_matchfunc_t) dev_match);
-  if (res != 0) {
-    td = (tar_dev_t *) libtar_hashptr_data(&hp);
+  /* Use a buffer size based on the filesystem blocksize, for better IO. */
+  buflen = st->st_blksize;
+  buf = palloc(tmp_pool, buflen);
 
-  } else {
-    td = (tar_dev_t *) calloc(1, sizeof(tar_dev_t));
-    if (td == NULL)
-      return -1;
+  res = pr_fsio_read(fh, buf, buflen);
+  while (res > 0) {
+    pr_signals_handle();
+    if (archive_write_data(tar, buf, res) < 0) {
+      int xerrno;
 
-    td->td_dev = st.st_dev;
-    td->td_h = libtar_hash_new(256, (libtar_hashfunc_t) ino_hash);
+      xerrno = archive_errno(tar);
+      pr_trace_msg(trace_channel, 3, "error adding data to archive: %s",
+        archive_error_string(tar));
 
-    if (td->td_h == NULL) {
-      free(td);
-      return -1;
-    }
+      destroy_pool(tmp_pool);
+      pr_fsio_close(fh);
 
-    if (libtar_hash_add(t->h, td) == -1) {
-      libtar_hash_free(td->td_h, free);
-      free(td);
+      errno = xerrno;
       return -1;
     }
+ 
+    res = pr_fsio_read(fh, buf, buflen);
   }
 
-  libtar_hashptr_reset(&hp);
+  destroy_pool(tmp_pool);
+  pr_fsio_close(fh);  
+
+  return 0;
+}
+
+static int append_file(pool *p, struct archive *tar,
+    struct archive_entry *entry, char *real_name, char *save_name) {
+  struct stat st;
+  int res;
 
-  res = libtar_hash_getkey(td->td_h, &hp, &(st.st_ino),
-    (libtar_matchfunc_t) ino_match);
-  if (res != 0) {
-    ti = (tar_ino_t *) libtar_hashptr_data(&hp);
-    t->th_buf.typeflag = LNKTYPE;
-    th_set_link(t, ti->ti_name);
+  if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
+    res = pr_fsio_lstat(real_name, &st);
 
   } else {
-    ti = (tar_ino_t *)calloc(1, sizeof(tar_ino_t));
-    if (ti == NULL)
-      return -1;
+    res = pr_fsio_stat(real_name, &st);
+  }
+
+  if (res < 0) {
+    int xerrno = errno;
 
-    ti->ti_ino = st.st_ino;
-    snprintf(ti->ti_name, sizeof(ti->ti_name), "%s",
-      save_name ? save_name : real_name);
-    libtar_hash_add(td->td_h, ti);
+    pr_trace_msg(trace_channel, 9, "error stat'ing '%s': %s", real_name,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
   }
 
-  /* check if it's a symlink */
-  if (TH_ISSYM(t)) {
-    i = readlink(real_name, path, sizeof(path));
+  archive_entry_clear(entry);
+  archive_entry_copy_stat(entry, &st);
+  archive_entry_copy_pathname(entry, save_name);
+
+  if (S_ISLNK(st.st_mode)) {
+    int i;
+    char path[PR_TUNABLE_PATH_MAX+1];
+
+    i = readlink(real_name, path, sizeof(path)-1);
     if (i == -1)
       return -1;
 
-    if (i >= PR_TUNABLE_PATH_MAX)
+    if (i >= PR_TUNABLE_PATH_MAX) {
       i = PR_TUNABLE_PATH_MAX - 1;
+    }
 
     path[i] = '\0';
-    th_set_link(t, path);
+
+    pr_trace_msg(trace_channel, 15,
+      "setting destination path '%s' for symlink '%s'", path, real_name);
+    archive_entry_set_symlink(entry, path);
   }
+ 
+  res = archive_write_header(tar, entry);
+  if (res != ARCHIVE_OK) {
+    int xerrno;
 
-  /* print file info */
-  if (t->options & TAR_VERBOSE)
-    th_print_long_ls(t);
+    xerrno = archive_errno(tar);
+    pr_trace_msg(trace_channel, 3, "error writing archive entry header: %s",
+      archive_error_string(tar));
 
-  /* write header */
-  res = th_write(t);
-  if (res != 0)
+    errno = xerrno;
     return -1;
+  }
 
-  /* if it's a regular file, write the contents as well */
-  if (TH_ISREG(t) &&
-      tar_append_regfile(t, real_name) != 0)
+  /* If it's a regular file, write the contents as well */
+  if (S_ISREG(st.st_mode)) {
+    if (append_data(p, tar, entry, real_name, &st) < 0) {
+      return -1;
+    }
+  }
+
+  res = archive_write_finish_entry(tar);
+  if (res != ARCHIVE_OK) {
+    int xerrno;
+
+    xerrno = archive_errno(tar);
+    pr_trace_msg(trace_channel, 3, "error finishing archive entry: %s",
+      archive_error_string(tar));
+
+    errno = xerrno;
     return -1;
+  }
 
   return 0;
 }
 
-static int append_tree(TAR *t, char *real_dir, char *save_dir) {
+static int append_tree(pool *p, struct archive *tar,
+    struct archive_entry *entry, char *real_dir, char *save_dir) {
   char real_path[PR_TUNABLE_PATH_MAX+1];
   char save_path[PR_TUNABLE_PATH_MAX+1];
   struct dirent *dent;
@@ -184,14 +227,16 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
   struct stat st;
   int res;
 
-  res = append_file(t, real_dir, save_dir);
-  if (res != 0)
+  res = append_file(p, tar, entry, real_dir, save_dir);
+  if (res < 0) {
     return -1;
+  }
 
   dirh = opendir(real_dir);
   if (dirh == NULL) {
-    if (errno == ENOTDIR)
+    if (errno == ENOTDIR) {
       return 0;
+    }
 
     return -1;
   }
@@ -199,9 +244,10 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
   while ((dent = readdir(dirh)) != NULL) {
     pr_signals_handle();
 
-    if (strcmp(dent->d_name, ".") == 0 ||
-        strcmp(dent->d_name, "..") == 0)
+    if (strncmp(dent->d_name, ".", 2) == 0 ||
+        strncmp(dent->d_name, "..", 3) == 0) {
       continue;
+    }
 
     memset(real_path, '\0', sizeof(real_path));
     snprintf(real_path, sizeof(real_path)-1, "%s/%s", real_dir, dent->d_name);
@@ -212,145 +258,251 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
     }
 
     if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
-      res = lstat(real_path, &st);
+      res = pr_fsio_lstat(real_path, &st);
 
     } else {
-      res = stat(real_path, &st);
+      res = pr_fsio_stat(real_path, &st);
     }
 
-    if (res != 0)
+    if (res < 0) {
+      int xerrno = errno;
+
+      (void) closedir(dirh);
+
+      errno = xerrno;
       return -1;
+    }
 
     if (S_ISDIR(st.st_mode)) {
-      res = append_tree(t, real_path, (save_dir ? save_path : NULL));
-      if (res != 0)
+      res = append_tree(p, tar, entry, real_path,
+        (save_dir ? save_path : NULL));
+      if (res < 0) {
+        int xerrno = errno;
+
+        (void) closedir(dirh);
+ 
+        errno = xerrno;
         return -1;
+      }
 
       continue;
     }
 
-    res = append_file(t, real_path, (save_dir ? save_path : NULL));
-    if (res != 0)
+    res = append_file(p, tar, entry, real_path, (save_dir ? save_path : NULL));
+    if (res < 0) {
+      int xerrno = errno;
+
+      (void) closedir(dirh);
+
+      errno = xerrno;
       return -1;
+    }
   }
 
   closedir(dirh);
   return 0;
 }
 
-static int tar_create_tar(tartype_t *type, char *dst_file, char *src_path,
-    char *src_dir) {
-  TAR *tar;
+static int tar_archive_open_cb(struct archive *tar, void *user_data) {
+  struct archive_data *tar_data;
+  pr_fh_t *fh;
 
-  if (tar_open(&tar, dst_file, type, O_WRONLY|O_CREAT, 0644, 0) < 0) {
-    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "unable to open '%s' as tar file: %s", dst_file, strerror(errno));
-    return -1;
-  }
+  tar_data = user_data;
 
-  if (append_tree(tar, src_path, src_dir) < 0) {
-    int xerrno = errno;
-    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "error appending '%s' to tar file: %s", src_path, strerror(xerrno));
-    tar_close(tar);
+  fh = pr_fsio_open(tar_data->path, O_WRONLY|O_CREAT);
+  if (fh == NULL) {
+    return ARCHIVE_FATAL;
+  }
 
-    errno = xerrno;
-    return -1;
+  /* Override the default 0666 mode that pr_fsio_open() uses. */
+  if (pr_fsio_fchmod(fh, 0644) < 0) {
+    pr_trace_msg(trace_channel, 3, "error setting mode on '%s' to 0644: %s",
+      tar_data->path, strerror(errno));
   }
 
-  if (tar_append_eof(tar) < 0) {
-    int xerrno = errno;
-    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "error appending EOF to tar file: %s", strerror(xerrno));
-    tar_close(tar);
+  tar_data->fh = fh;
+  return ARCHIVE_OK;
+}
 
-    errno = xerrno;
-    return -1;
-  }
+static ssize_t tar_archive_write_cb(struct archive *tar, void *user_data,
+    const void *buf, size_t buflen) {
+  struct archive_data *tar_data;
 
-  if (tar_close(tar) < 0) {
-    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "error writing tar file: %s", strerror(errno));
-    return -1;
+  tar_data = user_data;
+  return pr_fsio_write(tar_data->fh, buf, buflen);
+}
+
+static int tar_archive_close_cb(struct archive *tar, void *user_data) {
+  struct archive_data *tar_data;
+  int res;
+
+  tar_data = user_data;
+
+  res = pr_fsio_close(tar_data->fh);
+  if (res < 0) {
+    return ARCHIVE_FATAL;
   }
 
-  return 0;
+  tar_data->fh = NULL;
+  return ARCHIVE_OK;
 }
 
-static int tar_bzopen(const char *path, int flags, mode_t mode) {
-  int fd;
-  BZFILE *bzf;
+static int tar_create_archive(pool *p, char *dst_file, unsigned long blksize,
+    char *src_path, char *src_dir, unsigned long flags) {
+  struct archive_data *tar_data;
+  struct archive *tar;
+  struct archive_entry *entry;
+  int res;
 
-  fd = open(path, flags, mode);
-  if (fd < 0) {
+  tar = archive_write_new();
+  if (tar == NULL) {
     (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "unable to open '%s': %s", path, strerror(errno));
+      "error allocating new archive handle: %s", archive_error_string(tar));
+    errno = archive_errno(tar);
     return -1;
   }
 
-  if (flags & O_CREAT) {
-    if (fchmod(fd, mode) < 0) {
-      int xerrno = errno;
+  /* Call archive_write_set_bytes_per_block() there, so that the optimal
+   * block size for writing data out to the archive file is used.
+   *
+   * Sadly, the libarchive API uses an int for the block size, not an
+   * unsigned long, size_t, or off_t.  Why even allow a signed data type
+   * for that parameter?
+   *
+   * NOTE: The `tar' program provided by libarchive defaults to a value
+   * of (20 * 512) for the bytes_per_block value; perhaps we should
+   * use that, too?
+   */
+  archive_write_set_bytes_per_block(tar, blksize);
+
+  if (flags & TAR_ARCHIVE_FL_USE_USTAR) {
+    res = archive_write_set_format_ustar(tar);
+    if (res != ARCHIVE_OK) {
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-        "error setting mode %04o on '%s': %s", mode, path, strerror(xerrno));
+        "error configuring archive handle for ustar format: %s",
+        archive_error_string(tar));
+      errno = archive_errno(tar);
+      return -1;
+    }
 
-      close(fd);
-      errno = xerrno;
+  } else if (flags & TAR_ARCHIVE_FL_USE_ZIP) {
+    res = archive_write_set_format_zip(tar);
+    if (res != ARCHIVE_OK) {
+      (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+        "error configuring archive handle for zip format: %s",
+        archive_error_string(tar));
+      errno = archive_errno(tar);
       return -1;
     }
   }
 
-  bzf = BZ2_bzdopen(fd, "wb");
-  if (bzf == NULL) {
-    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "unable to open bzlib stream on '%s': Not enough memory", path);
-    close(fd);
-    errno = EPERM;
-    return -1;
+  if (flags & TAR_ARCHIVE_FL_USE_GZIP) {
+    res = archive_write_add_filter_gzip(tar);
+    if (res != ARCHIVE_OK) {
+      (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+        "error configuring archive handle for gzip compression: %s",
+        archive_error_string(tar));
+      errno = archive_errno(tar);
+      return -1;
+    }
+
+    pr_trace_msg(trace_channel, 9, "using gzip compression for '%s'", src_path);
+
+  } else if (flags & TAR_ARCHIVE_FL_USE_BZIP2) {
+    res = archive_write_add_filter_bzip2(tar);
+    if (res != ARCHIVE_OK) {
+      (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+        "error configuring archive handle for bzip2 compression: %s",
+        archive_error_string(tar));
+      errno = archive_errno(tar);
+      return -1;
+    }
+
+    pr_trace_msg(trace_channel, 9, "using bzip2 compression for '%s'",
+      src_path);
+
+  } else {
+    res = archive_write_add_filter_none(tar);
+    if (res != ARCHIVE_OK) {
+      (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+        "error configuring archive handle for no compression: %s",
+        archive_error_string(tar));
+      errno = archive_errno(tar);
+      return -1;
+    }
   }
 
-  /* XXX I don't like doing this, returning a pointer in the space of
-   * an int, but unfortunately it is the interface defined by libtar.
+  tar_data = palloc(p, sizeof(struct archive_data));
+  tar_data->path = dst_file;
+
+  /* Allocate a new archive_entry to use for adding all entries to the
+   * archive.  This avoid creating/destroying an archive_entry object per
+   * file.
    */
-  return (int) bzf;
-}
+  entry = archive_entry_new();
+  if (tar == NULL) {
+    int xerrno;
+ 
+    xerrno = archive_errno(tar);
+    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+      "error allocating new archive entry handle: %s",
+      archive_error_string(tar));
+    archive_write_free(tar);
 
-static int tar_gzopen(const char *path, int flags, mode_t mode) {
-  int fd;
-  gzFile gzf;
+    errno = xerrno;
+    return -1;
+  }
+
+  res = archive_write_open(tar, tar_data, tar_archive_open_cb,
+    tar_archive_write_cb, tar_archive_close_cb);
+  if (res != ARCHIVE_OK) {
+    int xerrno;
 
-  fd = open(path, flags, mode);
-  if (fd < 0) {
+    xerrno = archive_errno(tar);
     (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "unable to open '%s': %s", path, strerror(errno));
+      "error opening archive handle for file '%s': %s", dst_file,
+      archive_error_string(tar));
+    archive_entry_free(entry);
+    archive_write_free(tar);
+    (void) unlink(dst_file);
+
+    errno = xerrno;
     return -1;
   }
 
-  if (flags & O_CREAT) {
-    if (fchmod(fd, mode) < 0) {
-      int xerrno = errno;
-      (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-        "error setting mode %04o on '%s': %s", mode, path, strerror(xerrno));
+  if (append_tree(p, tar, entry, src_path, src_dir) < 0) {
+    int xerrno = errno;
 
-      close(fd);
-      errno = xerrno;
-      return -1;
-    }
+    (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+      "error appending '%s' to tar file: %s", src_path, strerror(xerrno));
+    archive_entry_free(entry);
+    (void) archive_write_close(tar);
+    archive_write_free(tar);
+    (void) unlink(dst_file);
+
+    errno = xerrno;
+    return -1;
   }
 
-  gzf = gzdopen(fd, "wb");
-  if (gzf == NULL) {
+  archive_entry_free(entry);
+
+  res = archive_write_close(tar);
+  if (res < 0) {
+    int xerrno;
+
+    xerrno = archive_errno(tar);
     (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-      "unable to open zlib stream on '%s': Not enough memory", path);
-    close(fd);
-    errno = EPERM;
+      "error writing tar file: %s", archive_error_string(tar));
+
+    archive_write_free(tar);
+    (void) unlink(dst_file);
+
+    errno = xerrno;
     return -1;
   }
 
-  /* XXX I don't like doing this, returning a pointer in the space of
-   * an int, but unfortunately it is the interface defined by libtar.
-   */
-  return (int) gzf;
+  archive_write_free(tar);
+  return 0;
 }
 
 static char *tar_get_ext_tar(char *path, size_t path_len) {
@@ -365,8 +517,6 @@ static char *tar_get_ext_tar(char *path, size_t path_len) {
 
       return &path[path_len-4];
     }
-
-    return NULL;
   }
 
   return NULL;
@@ -384,8 +534,6 @@ static char *tar_get_ext_tgz(char *path, size_t path_len) {
 
       return &path[path_len-4];
     }
-
-    return NULL;
   }
 
   return NULL;
@@ -413,6 +561,24 @@ static char *tar_get_ext_targz(char *path, size_t path_len) {
   return NULL;
 }
 
+static char *tar_get_ext_tbz2(char *path, size_t path_len) {
+  if (path_len < 5) {
+    return NULL;
+  }
+
+  if (path[path_len-5] == '.') {
+    if ((path[path_len-4] == 'T' || path[path_len-4] == 't') &&
+        (path[path_len-3] == 'B' || path[path_len-3] == 'b') &&
+        (path[path_len-2] == 'Z' || path[path_len-2] == 'z') &&
+        path[path_len-1] == '2') {
+
+      return &path[path_len-5];
+    }
+  }
+
+  return NULL;
+}
+
 static char *tar_get_ext_tarbz2(char *path, size_t path_len) {
   if (path_len < 8) {
     return NULL;
@@ -429,13 +595,77 @@ static char *tar_get_ext_tarbz2(char *path, size_t path_len) {
 
       return &path[path_len-8];
     }
+  }
+
+  return NULL;
+}
 
+static char *tar_get_ext_zip(char *path, size_t path_len) {
+  if (path_len < 4) {
     return NULL;
   }
 
+  if (path[path_len-4] == '.') {
+    if ((path[path_len-3] == 'Z' || path[path_len-3] == 'z') &&
+        (path[path_len-2] == 'I' || path[path_len-2] == 'i') &&
+        (path[path_len-1] == 'P' || path[path_len-1] == 'p')) {
+
+      return &path[path_len-4];
+    }
+  }
+
   return NULL;
 }
 
+static char *tar_get_flags(char *path, size_t path_len, unsigned long *flags) {
+  char *ptr;
+
+  ptr = tar_get_ext_tar(path, path_len);
+  if (ptr != NULL) {
+    *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+
+  } else {
+    ptr = tar_get_ext_tgz(path, path_len);
+    if (ptr != NULL) {
+      *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+      *flags |= TAR_ARCHIVE_FL_USE_GZIP;
+
+    } else {
+      ptr = tar_get_ext_targz(path, path_len);
+      if (ptr != NULL) {
+        *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+        *flags |= TAR_ARCHIVE_FL_USE_GZIP;
+
+      } else {
+        ptr = tar_get_ext_tbz2(path, path_len);
+        if (ptr != NULL) {
+          *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+          *flags |= TAR_ARCHIVE_FL_USE_BZIP2;
+
+        } else {
+          ptr = tar_get_ext_tarbz2(path, path_len);
+          if (ptr != NULL) {
+            *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+            *flags |= TAR_ARCHIVE_FL_USE_BZIP2;
+
+          } else {
+            ptr = tar_get_ext_zip(path, path_len);
+            if (ptr != NULL) {
+              *flags |= TAR_ARCHIVE_FL_USE_ZIP;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  if (*flags == 0) {
+    return NULL;
+  }
+
+  return ptr;
+}
+
 /* Configuration handlers
  */
 
@@ -501,7 +731,8 @@ MODRET set_taroptions(cmd_rec *cmd) {
   c = add_config_param(cmd->argv[0], 1, NULL);
 
   for (i = 1; i < cmd->argc; i++) {
-    if (strcmp(cmd->argv[i], "dereference") == 0) {
+    if (strcmp(cmd->argv[i], "FollowSymlinks") == 0 ||
+        strcmp(cmd->argv[i], "dereference") == 0) {
       opts |= TAR_OPT_DEREF_SYMLINKS;
 
     } else {
@@ -549,8 +780,15 @@ MODRET tar_post_pass(cmd_rec *cmd) {
     pr_event_register(&tar_module, "core.exit", tar_exit_ev, NULL);
 
     c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarOptions", FALSE);
-    if (c) {
-      tar_opts = *((unsigned long *) c->argv[0]);
+    while (c != NULL) {
+      unsigned long opts;
+
+      pr_signals_handle();
+
+      opts = *((unsigned long *) c->argv[0]);
+      tar_opts |= opts;
+
+      c = find_config_next(c, c->next, CONF_PARAM, "TarOptions", FALSE);
     }
 
     c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarTempPath", FALSE);
@@ -579,11 +817,13 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
   char *path, *tmp;
   size_t path_len;
 
-  if (!tar_engine)
+  if (tar_engine == FALSE) {
     return PR_DECLINED(cmd);
+  }
 
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     return PR_DECLINED(cmd);
+  }
 
   path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
 
@@ -606,32 +846,12 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
   path_len = strlen(path);
   if (path_len > 4) {
     char *dir, *notar_file, *ptr, *tar_file;
-    int fd, res, use_tar = FALSE, use_gz = FALSE, use_bz2 = FALSE;
+    int fd, res;
     struct stat st;
     config_rec *d;
+    unsigned long flags = 0UL;
 
-    ptr = tar_get_ext_tar(path, path_len);
-    if (ptr)
-      use_tar = TRUE;
-
-    if (ptr == NULL) {
-      ptr = tar_get_ext_tgz(path, path_len);
-      if (ptr)
-        use_gz = TRUE;
-    }
-
-    if (ptr == NULL) {
-      ptr = tar_get_ext_targz(path, path_len);
-      if (ptr)
-        use_gz = TRUE;
-    }
-
-    if (ptr == NULL) {
-      ptr = tar_get_ext_tarbz2(path, path_len);
-      if (ptr)
-        use_bz2 = TRUE;
-    }
-
+    ptr = tar_get_flags(path, path_len, &flags);
     if (ptr == NULL) {
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
         "no .tar file extension found in '%s'", path);
@@ -643,7 +863,7 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
     path = dir_realpath(cmd->tmp_pool, path);
 
     res = dir_exists(path);
-    if (!res) {
+    if (res == 0) {
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
         "'%s' is not a directory, ignoring", path);
       *ptr = '.';
@@ -680,13 +900,17 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
         int tar_enable;
 
         tar_enable = *((int *) c->argv[0]);
-        if (!tar_enable) {
+        if (tar_enable == FALSE) {
           (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
-            "TarEnable off found, skipping tar file of '%s' directory", path);
+            "'TarEnable off' found, skipping tar file of '%s' directory", path);
           *ptr = '.';
           return PR_DECLINED(cmd);
         }
       }
+
+    } else {
+      pr_trace_msg(trace_channel, 9,
+        "no <Directory> match found for '%s'", path);
     }
 
     dir = strrchr(path, '/');
@@ -701,47 +925,30 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
 
     fd = mkstemp(tar_file);
     if (fd < 0) {
+      int xerrno = errno;
+
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
         "error creating temporary filename using mkstemp: %s",
-        strerror(errno));
+        strerror(xerrno));
       *ptr = '.';
       return PR_DECLINED(cmd);
     }
 
+    (void) fstat(fd, &st);
     close(fd);
 
     (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
       "writing temporary .tar file to '%s'", tar_file);
 
     /* Create the tar file. */
-    if (use_tar) {
-      res = tar_create_tar(NULL, tar_file, path, dir);
-
-    } else if (use_gz) {
-      tartype_t gz_type = {
-        (openfunc_t) tar_gzopen,
-        (closefunc_t) gzclose,
-        (readfunc_t) gzread,
-        (writefunc_t) gzwrite
-      };
-
-      res = tar_create_tar(&gz_type, tar_file, path, dir);
-
-    } else if (use_bz2) {
-      tartype_t bz2_type = {
-        (openfunc_t) tar_bzopen,
-        (closefunc_t) BZ2_bzclose,
-        (readfunc_t) BZ2_bzread,
-        (writefunc_t) BZ2_bzwrite
-      };
-
-      res = tar_create_tar(&bz2_type, tar_file, path, dir);
-    }
-
+    res = tar_create_archive(cmd->tmp_pool, tar_file,
+      (unsigned long) st.st_blksize, path, dir, flags);
     if (res < 0) {
+      int xerrno = errno;
+
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
         "error creating tar file '%s' from directory '%s': %s",
-        tar_file, path, strerror(errno));
+        tar_file, path, strerror(xerrno));
       *ptr = '.';
       return PR_DECLINED(cmd);
     }
@@ -782,11 +989,12 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
 MODRET tar_log_retr(cmd_rec *cmd) {
   char *path;
 
-  if (!tar_engine)
+  if (tar_engine == FALSE) {
     return PR_DECLINED(cmd);
+  }
 
   path = pr_table_get(cmd->notes, "mod_tar.tar-file", NULL);
-  if (path) {
+  if (path != NULL) {
     if (unlink(path) < 0) {
       (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
         "error deleting '%s': %s", path, strerror(errno));
@@ -799,7 +1007,7 @@ MODRET tar_log_retr(cmd_rec *cmd) {
   }
 
   path = pr_table_get(cmd->notes, "mod_tar.orig-path", NULL);
-  if (path) {
+  if (path != NULL) {
     /* Replace session.xfer.path, so that the TransferLog/ExtendedLogs
      * show the originally requested path, not the temporary filename
      * generated by mod_tar.
@@ -830,16 +1038,6 @@ static void tar_exit_ev(const void *event_data, void *user_data) {
   }
 }
 
-#if defined(PR_SHARED_MODULE)
-static void tar_mod_unload_ev(const void *event_data, void *user_data) {
-  if (strcmp("mod_tar.c", (const char *) event_data) == 0) {
-    pr_event_unregister(&tar_module, NULL, NULL);
-    close(tar_logfd);
-    tar_logfd = -1;
-  }
-}
-#endif /* !PR_SHARED_MODULE */
-
 /* Initialization functions
  */
 
@@ -848,14 +1046,15 @@ static int tar_sess_init(void) {
 
   c = find_config(main_server->conf, CONF_PARAM, "TarLog", FALSE);
   if (c &&
-      strcasecmp((char *) c->argv[0], "none") != 0) {
-    int res;
+      strncasecmp((char *) c->argv[0], "none", 5) != 0) {
+    int res, xerrno;
     char *path;
 
     path = c->argv[0];
 
     PRIVS_ROOT
     res = pr_log_openfile(path, &tar_logfd, 0660);
+    xerrno = errno;
     PRIVS_RELINQUISH
 
     switch (res) {
@@ -864,7 +1063,7 @@ static int tar_sess_init(void) {
 
       case -1:
         pr_log_debug(DEBUG1, MOD_TAR_VERSION ": unable to open TarLog '%s': %s",
-         path, strerror(errno));
+         path, strerror(xerrno));
         break;
 
       case PR_LOG_SYMLINK:
@@ -883,11 +1082,8 @@ static int tar_sess_init(void) {
 }
 
 static int tar_init(void) {
-#if defined(PR_SHARED_MODULE)
-  pr_event_register(&tar_module, "core.module-unload", tar_mod_unload_ev, NULL);
-#endif
-
-  pr_log_debug(DEBUG0, MOD_TAR_VERSION ": using libtar %s", libtar_version);
+  pr_log_debug(DEBUG0, MOD_TAR_VERSION ": using libarchive %s",
+    archive_version_string());
   return 0;
 }
 


=====================================
mod_tar.html
=====================================
--- a/mod_tar.html
+++ b/mod_tar.html
@@ -22,9 +22,9 @@ directory.
 
 <p>
 To provide this feature, the <code>mod_tar</code> module uses the
-<code>libtar</code> library; see:
+<code>libarchive</code> library; see:
 <pre>
-  <a href="http://www.feep.net/libtar/">http://www.feep.net/libtar/</a>
+  <a href="http://libarchive.github.com/">http://libarchive.github.com/</a>
 </pre>
 
 <p>
@@ -115,13 +115,13 @@ behavior of <code>mod_tar</code>, usually pertaining to how the
 <p>
 Example:
 <pre>
-  TarOptions dereference
+  TarOptions FollowSymlinks
 </pre>
 
 <p>
 The currently implemented options are:
 <ul>
-  <li><code>dereference</code><br>
+  <li><code>FollowSymlinks</code><br>
     <p>
     Instead of creating <code>.tar</code> files which include symlinks,
     include the files that the symlinks point to.
@@ -174,7 +174,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_tar</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d -I /path/to/libtar/include -L /path/to/libtar/lib mod_tar.c
+  # prxs -c -i -d -I /path/to/libarchive/include -L /path/to/libarchive/lib mod_tar.c
 </pre>
 
 <p>
@@ -198,9 +198,11 @@ The following extensions will trigger <code>mod_tar</code> to attempt
 on-the-fly tar file creation:
 <ul>
   <li>.tar
+  <li>.tbz2
   <li>.tgz
   <li>.tar.gz
   <li>.tar.bz2
+  <li>.zip
 </ul>
 
 <p>
@@ -265,7 +267,7 @@ Last Updated: <i>$Date: 2009/08/20 17:07:14 $</i><br>
 <br><hr>
 
 <font size=2><b><i>
-© Copyright 2009 TJ Saunders<br>
+© Copyright 2009-2012 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 


=====================================
t/etc/modules/mod_tar/subdir.tar
=====================================
Binary files /dev/null and b/t/etc/modules/mod_tar/subdir.tar differ


=====================================
t/lib/ProFTPD/Tests/Modules/mod_tar.pm
=====================================
--- /dev/null
+++ b/t/lib/ProFTPD/Tests/Modules/mod_tar.pm
@@ -0,0 +1,2881 @@
+package ProFTPD::Tests::Modules::mod_tar;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Archive::Tar;
+use Archive::Tar::File;
+use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
+use Cwd;
+use Digest::MD5;
+use File::Copy;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IO::Zlib;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  tar_retr_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tar => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tar_already_exists => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tar_symlinks => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # XXX tar_retr_tar_subdirs
+
+  # XXX Need test for absolute symlinks, and chrooted session
+  tar_retr_tar_symlinks_opt_dereference => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_enable_off => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_notar => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tar_gz => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tgz => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_retr_tar_bz2 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_xferlog_retr_tar => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  tar_tmp_path_cleanup_on_abort => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # XXX tar_tmp_path_dev_full (on Linux), to test out-of-space handling
+
+  tar_retr_tar_2gb_single_file => {
+    order => ++$order,
+    test_class => [qw(forking slow)],
+  },
+
+   # XXX tar_retr_tar_2gb_many_files
+
+  tar_retr_zip => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub tar_retr_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("test.txt.tar");
+      if ($conn) {
+        die("RETR test.txt.tar succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.txt.tar: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $tar = Archive::Tar->new($conn);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/test.txt';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      # Make sure the file contents have not been corrupted in transit
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($tar->get_content('subdir/test.txt')); 
+
+      my $test_md5 = $ctx->hexdigest();
+
+      $self->assert($test_md5 eq $expected_md5,
+        test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_already_exists {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $archive = File::Spec->rel2abs("t/etc/modules/mod_tar/subdir.tar");
+  my $test_file = File::Spec->rel2abs("$tmpdir/subdir.tar");
+  unless (copy($archive, $test_file)) {
+    die("Can't copy $archive to $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  $ctx->add("Hello, World!\n");
+  my $expected_md5 = $ctx->hexdigest();
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $tar = Archive::Tar->new($conn);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/test.txt';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      # Make sure the file contents have not been corrupted in transit
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($tar->get_content('subdir/test.txt')); 
+
+      my $test_md5 = $ctx->hexdigest();
+
+      $self->assert($test_md5 eq $expected_md5,
+        test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_symlinks {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Create three files in subdir
+  for (my $i = 0; $i < 3; $i++) {
+    my $test_file = File::Spec->rel2abs("$sub_dir/test" . ($i + 1) . ".txt");
+    if (open(my $fh, "> $test_file")) {
+      print $fh "Hello, World!\n";
+      unless (close($fh)) {
+        die("Can't write $test_file: $!");
+      }
+
+    } else {
+      die("Can't open $test_file: $!");
+    }
+  }
+
+  my $symlink_dir = File::Spec->rel2abs("$tmpdir/symlinkdir");
+  mkpath($symlink_dir);
+
+  # Create three symlinks in symlinkdir to the files in subdir (using
+  # relative paths).
+
+  my $cwd = getcwd();
+
+  unless (chdir($symlink_dir)) {
+    die("Can't chdir to $symlink_dir: $!");
+  }
+
+  # Create three files in subdir
+  for (my $i = 0; $i < 3; $i++) {
+    my $symlink_src = "../subdir/test" . ($i + 1) . ".txt";
+    my $symlink_dst = "./test" . ($i + 1) . ".lnk";
+
+    unless (symlink($symlink_src, $symlink_dst)) {
+      die("Can't symlink '$symlink_src' to '$symlink_dst': $!");
+    }
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("symlinkdir.tar");
+      unless ($conn) {
+        die("RETR symlinkdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $tar = Archive::Tar->new($conn);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 4;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'symlinkdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      for (my $i = 0; $i < 3; $i++) {
+        $expected = "symlinkdir/test" . ($i + 1) . '.lnk';
+        $self->assert(defined($entries->{$expected}),
+          test_msg("Expected entry for '$expected', did not see one"));
+
+        my $ent = ($tar->get_files($expected))[0];
+        $self->assert(defined($ent),
+          test_msg("Unable to get info for $expected from archive"));
+
+        $self->assert($ent->is_symlink(),
+          test_msg("File $expected is not a symlink as expected"));
+
+        $expected = '../subdir/test' . ($i + 1) . '.txt';
+        my $ent_linkname = $ent->linkname();
+        $self->assert($ent_linkname eq $expected,
+          test_msg("Expected linkname '$expected', got '$ent_linkname'"));
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_symlinks_opt_dereference {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Create three files in subdir
+  for (my $i = 0; $i < 3; $i++) {
+    my $test_file = File::Spec->rel2abs("$sub_dir/test" . ($i + 1) . ".txt");
+    if (open(my $fh, "> $test_file")) {
+      print $fh "Hello, World!\n";
+      unless (close($fh)) {
+        die("Can't write $test_file: $!");
+      }
+
+    } else {
+      die("Can't open $test_file: $!");
+    }
+  }
+
+  my $symlink_dir = File::Spec->rel2abs("$tmpdir/symlinkdir");
+  mkpath($symlink_dir);
+
+  # Create three symlinks in symlinkdir to the files in subdir (using
+  # relative paths).
+
+  my $cwd = getcwd();
+
+  unless (chdir($symlink_dir)) {
+    die("Can't chdir to $symlink_dir: $!");
+  }
+
+  # Create three files in subdir
+  for (my $i = 0; $i < 3; $i++) {
+    my $symlink_src = "../subdir/test" . ($i + 1) . ".txt";
+    my $symlink_dst = "./test" . ($i + 1) . ".lnk";
+
+    unless (symlink($symlink_src, $symlink_dst)) {
+      die("Can't symlink '$symlink_src' to '$symlink_dst': $!");
+    }
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+        TarOptions => 'dereference',
+#        TarOptions => 'FollowSymlinks',
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("symlinkdir.tar");
+      unless ($conn) {
+        die("RETR symlinkdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $tar = Archive::Tar->new($conn);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 4;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'symlinkdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      for (my $i = 0; $i < 3; $i++) {
+        $expected = "symlinkdir/test" . ($i + 1) . '.lnk';
+        $self->assert(defined($entries->{$expected}),
+          test_msg("Expected entry for '$expected', did not see one"));
+
+        my $ent = ($tar->get_files($expected))[0];
+        $self->assert(defined($ent),
+          test_msg("Unable to get info for $expected from archive"));
+
+        $self->assert($ent->is_file(),
+          test_msg("File $expected is not a symlink as expected"));
+
+        $expected = 14;
+        my $ent_sz = $ent->size();
+        $self->assert($ent_sz == $expected,
+          test_msg("Expected file size $expected, got $ent_sz"));
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_enable_off {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # MacOSX-specific hack
+  if ($^O eq 'darwin') {
+    $sub_dir = ('/private' . $sub_dir);
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    Directory => {
+      $sub_dir => {
+        TarEnable => 'off',
+      },
+    },
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      # This should fail, since the file doesn't exist, and we configured
+      # "TarEnable off" in that directory.
+      my $conn = $client->retr_raw("subdir.tar");
+      if ($conn) {
+        die("RETR subdir.tar succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'subdir.tar: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_notar {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $notar_file = File::Spec->rel2abs("$sub_dir/.notar");
+  if (open(my $fh, "> $notar_file")) {
+    unless (close($fh)) {
+      die("Can't write $notar_file: $!");
+    }
+
+  } else {
+    die("Can't open $notar_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      # This should fail, since the file doesn't exist, and we configured
+      # a .notar file in that directory.
+      my $conn = $client->retr_raw("subdir.tar");
+      if ($conn) {
+        die("RETR subdir.tar succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'subdir.tar: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_gz {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar.gz");
+      unless ($conn) {
+        die("RETR subdir.tar.gz failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $zio = IO::Zlib->new($conn, 'rb');
+      my $tar = Archive::Tar->new($zio);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/test.txt';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      # Make sure the file contents have not been corrupted in transit
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($tar->get_content('subdir/test.txt')); 
+
+      my $test_md5 = $ctx->hexdigest();
+
+      $self->assert($test_md5 eq $expected_md5,
+        test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tgz {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tgz");
+      unless ($conn) {
+        die("RETR subdir.tgz failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $zio = IO::Zlib->new($conn, 'rb');
+      my $tar = Archive::Tar->new($zio);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/test.txt';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      # Make sure the file contents have not been corrupted in transit
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($tar->get_content('subdir/test.txt')); 
+
+      my $test_md5 = $ctx->hexdigest();
+
+      $self->assert($test_md5 eq $expected_md5,
+        test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_bz2 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $src_file = File::Spec->rel2abs("$sub_dir/src.bin");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $src_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $src_file: $!");
+  }
+
+  my $dst_bz2_file = File::Spec->rel2abs("$tmpdir/dst.tar.bz2");
+  my $dst_tar_file = File::Spec->rel2abs("$tmpdir/dst.tar");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar.bz2");
+      unless ($conn) {
+        die("RETR subdir.tar.bz2 failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Download the data to a separate bunzipped file, and let Archive::Tar
+      # work on that.
+
+      my $dstfh;
+      unless (open($dstfh, "> $dst_bz2_file")) {
+        die("Can't open $dst_bz2_file: $!");
+      }
+      binmode($dstfh);
+
+      my $buf;
+      my $buflen = 16384;
+      while ($conn->read($buf, $buflen, 25)) {
+        print $dstfh $buf;
+      }
+
+      unless (close($dstfh)) {
+        die("Can't write $dst_bz2_file: $!");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    # Uncompress the file
+    `bunzip2 -q $dst_bz2_file`;
+
+    my $dstfh;
+    unless (open($dstfh, "< $dst_tar_file")) {
+      die("Can't read $dst_tar_file: $!");
+    }
+    binmode($dstfh);
+
+    my $tar = Archive::Tar->new($dstfh);
+    unless (defined($tar)) {
+      die("Can't read tar file from $dst_tar_file: " . $Archive::Tar::error);
+    }
+
+    my $entries = { map { $_ => 1 } $tar->list_files() };
+
+    # Make sure the hashref contains the entries we expect
+    my $expected = 2;
+    my $nents = scalar(keys(%$entries));
+    $self->assert($nents == $expected,
+      test_msg("Expected $expected entries, found $nents"));
+
+    $expected = 'subdir/';
+    $self->assert(defined($entries->{$expected}),
+      test_msg("Expected entry for '$expected', did not see one"));
+
+    $expected = 'subdir/src.bin';
+    $self->assert(defined($entries->{$expected}),
+      test_msg("Expected entry for '$expected', did not see one"));
+
+    # Make sure the file contents have not been corrupted in transit
+
+    $ctx = Digest::MD5->new();
+    $ctx->add($tar->get_content('subdir/src.bin')); 
+
+    my $test_md5 = $ctx->hexdigest();
+
+    $self->assert($test_md5 eq $expected_md5,
+      test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+
+    close($dstfh);
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_xferlog_retr_tar {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $xfer_log = File::Spec->rel2abs("$tmpdir/xfer.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    TransferLog => $xfer_log,
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $tar = Archive::Tar->new($conn);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/test.txt';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      # Make sure the file contents have not been corrupted in transit
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($tar->get_content('subdir/test.txt')); 
+
+      my $test_md5 = $ctx->hexdigest();
+
+      $self->assert($test_md5 eq $expected_md5,
+        test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    # Now read in the TransferLog, make sure that the filename is the originally
+    # requested name, not the name as modified by mod_tar.
+
+    if (open(my $fh, "< $xfer_log")) {
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      my $expected = '^\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+\s+\d+\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+_\s+o\s+r\s+(\S+)\s+ftp\s+0\s+\*\s+c$';
+
+      $self->assert(qr/$expected/, $line,
+        test_msg("Expected '$expected', got '$line'"));
+
+      if ($line =~ /$expected/) {
+        my $remote_host = $1;
+        my $filesz = $2;
+        my $filename = $3;
+        my $xfer_type = $4;
+        my $user_name = $5;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $remote_host,
+          test_msg("Expected '$expected', got '$remote_host'"));
+
+        # The original filename, sans .tar extension
+        $expected = $sub_dir;
+
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack dealing with their tmp filesystem shenanigans
+          $expected = ('/private' . $expected);
+        }
+
+        $self->assert($expected eq $filename,
+          test_msg("Expected '$expected', got '$filename'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
+
+      } else {
+        die("Can't read $xfer_log: $!");
+      }
+    }
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_tmp_path_cleanup_on_abort {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $tmp_path = File::Spec->rel2abs("$tmpdir/tarwork");
+  mkpath($tmp_path);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+        TarTempPath => $tmp_path,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Read in a couple of bytes, then abort the connection.
+      my $buf;
+      $conn->read($buf, 4, 25);
+      $conn->abort();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Abort successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Scan the TarTempPath, make sure there are no files in there.
+      my $dirh;
+      unless (opendir($dirh, $tmp_path)) {
+        die("Can't open directory '$tmp_path': $!");
+      }
+
+      my $tmp_files = [grep { !/^\.$/ && !/^\.\.$/ } readdir($dirh)];
+      closedir($dirh);
+
+      my $nfiles = scalar(@$tmp_files);
+      $self->assert($nfiles == 0,
+        test_msg("Expected no tmp files, found $nfiles"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_tar_2gb_single_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $src_file = File::Spec->rel2abs("$sub_dir/src.bin");
+
+  # Create a file that is 2GB.
+  my $src_len = (2 ** 31);
+  if (open(my $fh, "> $src_file")) {
+
+    if ($ENV{TEST_VERBOSE}) {
+      print STDOUT "# Creating test file of $src_len bytes\n";
+    }
+
+    my $nchunks = 64;
+    my $chunklen = ($src_len / $nchunks);
+
+    for (my $i = 0; $i < $nchunks; $i++) {
+      print $fh "A" x $chunklen;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/dst.tar");
+
+  # This test could run for a while, since mod_tar has to read in the large
+  # file.  So give the test time to run.
+  my $timeout_idle = 1800;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0,
+        10, $timeout_idle);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.tar");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # I'm not sure why, but Archive::Tar does not like reading tar data
+      # directly from the $conn if that tar data contains large files.
+      #
+      # To work around this, read the data from $conn into a local temp
+      # file, then set Archive::Tar to work on that local file.
+
+      my $dstfh;
+      unless (open($dstfh, "> $dst_file")) {
+        die("Can't write $dst_file: $!");
+      }
+      binmode($dstfh);
+
+      my $buf;
+      my $buflen = 16384;
+
+      while ($conn->read($buf, $buflen, 25)) {
+        print $dstfh $buf;
+      }
+
+      unless (close($dstfh)) {
+        die("Can't write $dst_file: $!");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      unless (open($dstfh, "< $dst_file")) {
+        die("Can't read $dst_file: $!");
+      }
+      binmode($dstfh);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Finished downloading to $dst_file, verifying tar format\n";
+      }
+
+      my $tar = Archive::Tar->new($dstfh);
+      my $entries = { map { $_ => 1 } $tar->list_files() };
+
+      # Make sure the hashref contains the entries we expect
+      $expected = 2;
+      my $nents = scalar(keys(%$entries));
+      $self->assert($nents == $expected,
+        test_msg("Expected $expected entries, found $nents"));
+
+      $expected = 'subdir/';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      $expected = 'subdir/src.bin';
+      $self->assert(defined($entries->{$expected}),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      my $ent = ($tar->get_files($expected))[0];
+      $self->assert(defined($ent),
+        test_msg("Expected entry for '$expected', did not see one"));
+
+      my $ent_len = $ent->size();
+      $self->assert($ent_len eq $src_len,
+        test_msg("Expected file length $src_len, got $ent_len"));
+
+      close($dstfh);
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh, $timeout_idle + 10) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub tar_retr_zip {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/tar.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/src.bin");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $dst_zip_file = File::Spec->rel2abs("$tmpdir/dst.zip");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 tar:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_tar.c' => {
+        TarEngine => 'on',
+        TarLog => $log_file,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      $client->type('binary');
+
+      my $conn = $client->retr_raw("subdir.zip");
+      unless ($conn) {
+        die("RETR subdir.tar failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $dstfh;
+      unless (open($dstfh, "> $dst_zip_file")) {
+        die("Can't open $dst_zip_file: $!");
+      }
+
+      my $buf;
+      my $buflen = 16384;
+      while ($conn->read($buf, $buflen, 25)) {
+        print $dstfh $buf;
+      }
+
+      unless (close($dstfh)) {
+        die("Can't write $dst_zip_file: $!");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    my $zip = Archive::Zip->new($dst_zip_file);
+
+    my $entries = [$zip->members()];
+
+    # Make sure the arrayref contains the entries we expect
+    my $expected = 2;
+    my $nents = scalar(@$entries);
+    $self->assert($nents == $expected,
+      test_msg("Expected $expected entries, found $nents"));
+
+    $expected = 'subdir/';
+    my $ent = $zip->memberNamed($expected);
+    $self->assert(defined($ent),
+      test_msg("Expected entry for '$expected', did not see one"));
+
+    $expected = 'subdir/src.bin';
+    $ent = $zip->memberNamed($expected);
+    $self->assert(defined($ent),
+      test_msg("Expected entry for '$expected', did not see one"));
+
+    # Make sure the file contents have not been corrupted in transit
+
+    $ctx = Digest::MD5->new();
+    my $data = $zip->contents('subdir/src.bin');
+    $ctx->add($data);
+
+    my $test_md5 = $ctx->hexdigest();
+
+    $self->assert($test_md5 eq $expected_md5,
+      test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+1;


=====================================
t/modules/mod_tar.t
=====================================
--- /dev/null
+++ b/t/modules/mod_tar.t
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use lib qw(t/lib);
+use strict;
+
+use Test::Unit::HarnessUnit;
+
+$| = 1;
+
+my $r = Test::Unit::HarnessUnit->new();
+$r->start("ProFTPD::Tests::Modules::mod_tar");



View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar/commit/51b160e73027ee439ad70cea7a5f3f17a7dd4013

---
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar/commit/51b160e73027ee439ad70cea7a5f3f17a7dd4013
You're receiving this email because of your account on salsa.debian.org.


More information about the Pkg-proftpd-maintainers mailing list