/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 1997, 1998 Public Flood Software
 * Copyright (C) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver@tos.net>
 * Copyright (C) 2001, 2002, 2003, 2004, 2005 The ProFTPD Project
 *
 * 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
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * 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.
 *
 * As a special exemption, Public Flood Software/MacGyver aka Habeeb J. Dihu
 * 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.
 */

/* ProFTPD virtual/modular file-system support
 * $Id: fsio.c,v 1.44 2005/06/22 17:22:17 castaglia Exp $
 */

#include "conf.h"

#ifdef HAVE_REGEX_H
# include <regex.h>
#endif

#ifdef HAVE_SYS_STATVFS_H
# include <sys/statvfs.h>
#elif defined(HAVE_SYS_VFS_H)
# include <sys/vfs.h>
#elif defined(HAVE_SYS_MOUNT_H)
# include <sys/mount.h>
#endif

#ifdef AIX3
# include <sys/statfs.h>
#endif

#ifdef HAVE_ACL_LIBACL_H
# include <acl/libacl.h>
#endif

typedef struct fsopendir fsopendir_t;

struct fsopendir {
  fsopendir_t *next,*prev;

  /* pool for this object's use */
  pool *pool;

  pr_fs_t *fsdir;
  DIR *dir;
};

static pr_fs_t *root_fs = NULL, *fs_cwd = NULL;
static array_header *fs_map = NULL;

#ifdef PR_FS_MATCH
static pr_fs_match_t *fs_match_list = NULL;
#endif /* PR_FS_MATCH */

static fsopendir_t *fsopendir_list;

static void *fs_cache_dir = NULL;
static pr_fs_t *fs_cache_fsdir = NULL;

/* Internal flag set whenever a new pr_fs_t has been added or removed, and
 * cleared once the fs_map has been scanned
 */
static unsigned char chk_fs_map = FALSE;

/* Virtual working directory */
static char vwd[PR_TUNABLE_PATH_MAX + 1] = "/";
static char cwd[PR_TUNABLE_PATH_MAX + 1] = "/";

/* The following static functions are simply wrappers for system functions
 */

static int sys_stat(pr_fs_t *fs, const char *path, struct stat *sbuf) {
  return stat(path, sbuf);
}

static int sys_fstat(pr_fh_t *fh, int fd, struct stat *sbuf) {
  return fstat(fd, sbuf);
}

static int sys_lstat(pr_fs_t *fs, const char *path, struct stat *sbuf) {
  return lstat(path, sbuf);
}

static int sys_rename(pr_fs_t *fs, const char *rnfm, const char *rnto) {
  return rename(rnfm, rnto);
}

static int sys_unlink(pr_fs_t *fs, const char *path) {
  return unlink(path);
}

static int sys_open(pr_fh_t *fh, const char *path, int flags) {

#ifdef CYGWIN
  /* On Cygwin systems, we need the open(2) equivalent of fopen(3)'s "b"
   * option.  Cygwin defines an O_BINARY flag for this purpose.
   */
  flags |= O_BINARY;
#endif

  return open(path, flags, PR_OPEN_MODE);
}

static int sys_creat(pr_fh_t *fh, const char *path, mode_t mode) {
  return creat(path, mode);
}

static int sys_close(pr_fh_t *fh, int fd) {
  return close(fd);
}

static int sys_read(pr_fh_t *fh, int fd, char *buf, size_t size) {
  return read(fd, buf, size);
}

static int sys_write(pr_fh_t *fh, int fd, const char *buf, size_t size) {
  return write(fd, buf, size);
}

static off_t sys_lseek(pr_fh_t *fh, int fd, off_t offset, int whence) {
  return lseek(fd, offset, whence);
}

static int sys_link(pr_fs_t *fs, const char *path1, const char *path2) {
  return link(path1, path2);
}

static int sys_symlink(pr_fs_t *fs, const char *path1, const char *path2) {
  return symlink(path1, path2);
}

static int sys_readlink(pr_fs_t *fs, const char *path, char *buf,
    size_t buflen) {
  return readlink(path, buf, buflen);
}

static int sys_ftruncate(pr_fh_t *fh, int fd, off_t len) {
  return ftruncate(fd, len);
}

static int sys_truncate(pr_fs_t *fs, const char *path, off_t len) {
  return truncate(path, len);
}

static int sys_chmod(pr_fs_t *fs, const char *path, mode_t mode) {
  return chmod(path, mode);
}

static int sys_chown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) {
  return chown(path, uid, gid);
}

/* We provide our own equivalent of access(2) here, rather than using
 * access(2) directly, because access(2) uses the real IDs, rather than
 * the effective IDs, of the process.
 */
static int sys_access(pr_fs_t *fs, const char *path, int mode, uid_t uid,
    gid_t gid, array_header *suppl_gids) {
  mode_t mask;
  struct stat st;

  pr_fs_clear_cache();
  if (pr_fsio_stat(path, &st) < 0)
    return -1;

  /* Root always succeeds. */
  if (uid == PR_ROOT_UID)
    return 0;

  /* Initialize mask to reflect the permission bits that are applicable for
   * the given user. mask contains the user-bits if the user ID equals the
   * ID of the file owner. mask contains the group bits if the group ID
   * belongs to the group of the file. mask will always contain the other
   * bits of the permission bits.
   */
  mask = S_IROTH|S_IWOTH|S_IXOTH;

  if (st.st_uid == uid)
    mask |= S_IRUSR|S_IWUSR|S_IXUSR;

  /* Check the current group, as well as all supplementary groups.
   * Fortunately, we have this information cached, so accessing it is
   * almost free.
   */
  if (st.st_gid == gid) {
    mask |= S_IRGRP|S_IWGRP|S_IXGRP;

  } else {
    if (suppl_gids) {
      register unsigned int i = 0;

      for (i = 0; i < suppl_gids->nelts; i++) {
        if (st.st_gid == ((gid_t *) suppl_gids->elts)[i]) {
          mask |= S_IRGRP|S_IWGRP|S_IXGRP;
          break;
        }
      }
    }
  }

  mask &= st.st_mode;

  /* Perform requested access checks. */
  if (mode & R_OK) {
    if (!(mask & (S_IRUSR|S_IRGRP|S_IROTH))) {
      errno = EACCES;
      return -1;
    }
  }

  if (mode & W_OK) {
    if (!(mask & (S_IWUSR|S_IWGRP|S_IWOTH))) {
      errno = EACCES;
      return -1;
    }
  }

  if (mode & X_OK) {
    if (!(mask & (S_IXUSR|S_IXGRP|S_IXOTH))) {
      errno = EACCES;
      return -1;
    }
  }

  /* F_OK already checked by checking the return value of stat. */
  return 0;
}

static int sys_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
    array_header *suppl_gids) {
  return sys_access(fh->fh_fs, fh->fh_path, mode, uid, gid, suppl_gids);
}

static int sys_chroot(pr_fs_t *fs, const char *path) {
  if (chroot(path) < 0)
    return -1;

  session.chroot_path = (char *) path;
  return 0;
}

static int sys_chdir(pr_fs_t *fs, const char *path) {
  if (chdir(path) < 0)
    return -1;

  pr_fs_setcwd(path);
  return 0;
}

static void *sys_opendir(pr_fs_t *fs, const char *path) {
  return opendir(path);
}

static int sys_closedir(pr_fs_t *fs, void *dir) {
  return closedir((DIR *) dir);
}

static struct dirent *sys_readdir(pr_fs_t *fs, void *dir) {
  return readdir((DIR *) dir);
}

static int sys_mkdir(pr_fs_t *fs, const char *path, mode_t mode) {
  return mkdir(path, mode);
}

static int sys_rmdir(pr_fs_t *fs, const char *path) {
  return rmdir(path);
}

static int fs_cmp(const void *a, const void *b) {
  pr_fs_t *fsa, *fsb;

  fsa = *((pr_fs_t **) a);
  fsb = *((pr_fs_t **) b);

  return strcmp(fsa->fs_path, fsb->fs_path);
}

/* Statcache stuff */
typedef struct {
  char sc_path[PR_TUNABLE_PATH_MAX+1];
  struct stat sc_stat;
  int sc_errno;

} fs_statcache_t;

static fs_statcache_t statcache;

#define fs_cache_lstat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_LSTAT)
#define fs_cache_stat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_STAT)

static int cache_stat(pr_fs_t *fs, const char *path, struct stat *sbuf,
    unsigned int op) {
  int res = -1;
  char pathbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;

  /* Sanity checks */
  if (!fs) {
    errno = EINVAL;
    return -1;
  }

  if (!path) {
    errno = ENOENT;
    return -1;
  }

  /* Use only absolute path names.  Construct them, if given a relative
   * path, based on cwd.  This obviates the need for something like
   * realpath(3), which only introduces more stat system calls.
   */
  if (*path != '/') {
    sstrcat(pathbuf, cwd, sizeof(pathbuf)-1);

    /* If the cwd is "/", we don't need to duplicate the path separator. 
     * On some systems (e.g. Cygwin), this duplication can cause problems,
     * as the path may then have different semantics.
     */
    if (strcmp(cwd, "/") != 0)
      sstrcat(pathbuf, "/", sizeof(pathbuf)-1);

    sstrcat(pathbuf, path, sizeof(pathbuf)-1);

  } else
    sstrncpy(pathbuf, path, sizeof(pathbuf)-1);

  /* Determine which filesystem function to use, stat() or lstat() */
  if (op == FSIO_FILE_STAT) {
    mystat = fs->stat ? fs->stat : sys_stat;

  } else {
    mystat = fs->lstat ? fs->lstat : sys_lstat;
  }

  /* Can the last cached stat be used? */
  if (strcmp(pathbuf, statcache.sc_path) == 0) {

    /* Update the given struct stat pointer with the cached info */
    memcpy(sbuf, &statcache.sc_stat, sizeof(struct stat));

    /* Use the cached errno as well */
    errno = statcache.sc_errno;

    return 0;
  }

  res = mystat(fs, pathbuf, sbuf);

  /* Update the cache */
  memset(statcache.sc_path, '\0', sizeof(statcache.sc_path));
  sstrncpy(statcache.sc_path, pathbuf, sizeof(statcache.sc_path));
  memcpy(&statcache.sc_stat, sbuf, sizeof(struct stat));
  statcache.sc_errno = errno;

  return res;
}

/* Lookup routines */

/* Necessary prototype for static function */
static pr_fs_t *lookup_file_canon_fs(const char *, char **, int);

/* lookup_dir_fs() is called when we want to perform some sort of directory
 * operation on a directory or file.  A "closest" match algorithm is used.  If
 * the lookup fails or is not "close enough" (i.e. the final target does not
 * exactly match an existing filesystem handle) scan the list of fs_matches for
 * matchable targets and call any callback functions, then rescan the pr_fs_t
 * list.  The rescan is performed in case any modules registered pr_fs_ts
 * during the hit.
 */
static pr_fs_t *lookup_dir_fs(const char *path, int op) {
  char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  char tmp_path[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  pr_fs_t *fs = NULL;
  int exact = FALSE;

#ifdef PR_FS_MATCH
  pr_fs_match_t *fsm = NULL;
#endif /* PR_FS_MATCH */

  sstrncpy(buf, path, sizeof(buf));

  /* Check if the given path is an absolute path.  Since there may be
   * alternate fs roots, this is not a simple check.  If the path is
   * not absolute, prepend the current location.
   */
  if (pr_fs_valid_path(path) < 0) {
    if (pr_fs_dircat(tmp_path, sizeof(tmp_path), cwd, buf) < 0)
      return NULL;

  } else
    sstrncpy(tmp_path, buf, sizeof(tmp_path));

  /* Make sure that if this is a directory operation, the path being
   * search ends in a trailing slash -- this is how files and directories
   * are differentiated in the fs_map.
   */
  if ((FSIO_DIR_COMMON & op) && tmp_path[strlen(tmp_path) - 1] != '/')
    sstrcat(tmp_path, "/", sizeof(tmp_path));

  fs = pr_get_fs(tmp_path, &exact);

#ifdef PR_FS_MATCH
/* NOTE: what if there is a perfect matching pr_fs_t for the given path,
 *  but an fs_match with pattern of "." is registered?  At present, that
 *  fs_match will never trigger...hmmm...OK.  fs_matches are only scanned
 *  if and only if there is *not* an exactly matching pr_fs_t.
 *
 *  NOTE: this is experimental code, not yet ready for module consumption.
 *  It was present in the older FS code, hence it's presence now.
 */

  /* Is the returned pr_fs_t "close enough"? */
  if (!fs || !exact) {

    /* Look for an fs_match */
    fsm = pr_get_fs_match(tmp_path, op);

    while (fsm) {

      /* Invoke the fs_match's callback function, if set
       *
       * NOTE: what pr_fs_t is being passed to the trigger??
       */
      if (fsm->trigger) {
        if (fsm->trigger(fs, tmp_path, op) <= 0)
          pr_log_pri(PR_LOG_DEBUG, "error: fs_match '%s' trigger failed",
            fsm->name);
      }

      /* Get the next matching fs_match */
      fsm = pr_get_next_fs_match(fsm, tmp_path, op);
    }
  }

  /* Now, check for a new pr_fs_t, if any were registered by fs_match
   * callbacks.  This time, it doesn't matter if it's an exact match --
   * any pr_fs_t will do.
   */
  if (chk_fs_map)
    fs = pr_get_fs(tmp_path, &exact);
#endif /* PR_FS_MATCH */

  return (fs ? fs : root_fs);
}

/* lookup_file_fs() performs the same function as lookup_dir_fs, however
 * because we are performing a file lookup, the target is the subdirectory
 * _containing_ the actual target.  A basic optimization is used here,
 * if the path contains no '/' characters, fs_cwd is returned.
 */
static pr_fs_t *lookup_file_fs(const char *path, char **deref, int op) {

  if (!strchr(path, '/')) {
#ifdef PR_FS_MATCH
    pr_fs_match_t *fsm = NULL;

    fsm = pr_get_fs_match(path, op);

    if (!fsm || fsm->trigger(fs_cwd, path, op) <= 0) {
#else
    if (1) {
#endif /* PR_FS_MATCH */
      pr_fs_t *fs = fs_cwd;
      struct stat sbuf;
      int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;

      /* Determine which function to use, stat() or lstat(). */
      if (op == FSIO_FILE_STAT) {
        while (fs && fs->fs_next && !fs->stat)
          fs = fs->fs_next;

        mystat = fs->stat;

      } else {
        while (fs && fs->fs_next && !fs->lstat)
          fs = fs->fs_next;

        mystat = fs->lstat;
      }

      if (mystat(fs, path, &sbuf) == -1 || !S_ISLNK(sbuf.st_mode))
        return fs;

    } else {

      /* The given path is a symbolic link, in which case we need to find
       * the actual path referenced, and return an pr_fs_t for _that_ path
       */
      char linkbuf[PR_TUNABLE_PATH_MAX + 1];
      int i;

      /* Three characters are reserved at the end of linkbuf for some path
       * characters (and a trailing NUL).
       */
      if (fs_cwd->readlink &&
          (i = fs_cwd->readlink(fs_cwd, path, &linkbuf[2],
           sizeof(linkbuf)-3)) != -1) {
        linkbuf[i] = '\0';
        if (!strchr(linkbuf, '/')) {
          if (i + 3 > PR_TUNABLE_PATH_MAX)
            i = PR_TUNABLE_PATH_MAX - 3;

          memmove(&linkbuf[2], linkbuf, i + 1);

          linkbuf[i+2] = '\0';
          linkbuf[0] = '.';
          linkbuf[1] = '/';
          return lookup_file_canon_fs(linkbuf, deref, op);
        }
      }

      /* What happens if fs_cwd->readlink is NULL, or readlink() returns -1?
       * I guess, for now, we punt, and return fs_cwd.
       */
      return fs_cwd;
    }
  }

  return lookup_dir_fs(path, op);
}

static pr_fs_t *lookup_file_canon_fs(const char *path, char **deref, int op) {
  static char workpath[PR_TUNABLE_PATH_MAX + 1];

  memset(workpath,'\0',sizeof(workpath));

  if (pr_fs_resolve_partial(path, workpath, sizeof(workpath)-1,
      FSIO_FILE_OPEN) == -1) {
    if (*path == '/' || *path == '~') {
      if (pr_fs_interpolate(path, workpath, sizeof(workpath)-1) != -1)
        sstrncpy(workpath, path, sizeof(workpath));

    } else {
      if (pr_fs_dircat(workpath, sizeof(workpath), cwd, path) < 0)
        return NULL;
    }
  }

  if (deref)
    *deref = workpath;

  return lookup_file_fs(workpath, deref, op);
}

/* FS functions proper */

void pr_fs_clear_cache(void) {
  memset(&statcache, '\0', sizeof(statcache));
}

int pr_fs_copy_file(const char *src, const char *dst) {
  pr_fh_t *src_fh, *dst_fh;
  struct stat st;
  char *buf;
  size_t bufsz;
  int res;

  src_fh = pr_fsio_open(src, O_RDONLY);
  if (!src_fh) {
    pr_log_pri(PR_LOG_WARNING, "error opening source file '%s' "
      "for copying: %s", src, strerror(errno));
    return -1;
  }

  /* Do not allow copying of directories. open(2) may not fail when
   * opening the source path, since it is only doing a read-only open,
   * which does work on directories.
   */

  /* This should never fail. */
  (void) pr_fsio_fstat(src_fh, &st);
  if (S_ISDIR(st.st_mode)) {
    pr_fsio_close(src_fh);

    errno = EISDIR;
    pr_log_pri(PR_LOG_WARNING, "warning: cannot copy source '%s': %s", src,
      strerror(errno));
    return -1;
  }

  dst_fh = pr_fsio_open(dst, O_WRONLY|O_CREAT);
  if (!dst_fh) {
    int xerrno = errno;

    pr_fsio_close(src_fh);
    errno = xerrno;

    pr_log_pri(PR_LOG_WARNING, "error opening destination file '%s' "
      "for copying: %s", dst, strerror(errno));
    return -1;
  }

  /* Stat the source file to find its optimal copy block size. */
  if (pr_fsio_fstat(src_fh, &st) < 0) {
    int xerrno = errno;
    pr_log_pri(PR_LOG_WARNING, "error checking source file '%s' "
      "for copying: %s", src, strerror(errno));

    pr_fsio_close(src_fh);
    pr_fsio_close(dst_fh);
    pr_fsio_unlink(dst);

    errno = xerrno;
    return -1;
  }

  bufsz = st.st_blksize;
  buf = malloc(bufsz);
  if (!buf) {
    pr_log_pri(PR_LOG_CRIT, "Out of memory!");
    end_login(1);
  }

  /* Make sure the destination file starts with a zero size. */
  pr_fsio_truncate(dst, 0);

  while ((res = pr_fsio_read(src_fh, buf, bufsz)) > 0) {
    if (pr_fsio_write(dst_fh, buf, res) != res) {
      pr_log_pri(PR_LOG_WARNING, "error copying to '%s': %s", dst,
        strerror(errno));
      break;
    }

    pr_signals_handle();
  }

  free(buf);

#if defined(HAVE_POSIX_ACL) && defined(PR_USE_FACL)
  {
    /* Copy any ACLs from the source file to the destination file as well. */
# if defined(HAVE_BSD_POSIX_ACL)
    acl_t facl, facl_dup = NULL;
    int have_facl = FALSE, have_dup = FALSE;

    facl = acl_get_fd(PR_FH_FD(src_fh));
    if (facl)
      have_facl = TRUE;

    if (have_facl)
        facl_dup = acl_dup(facl);

    if (facl_dup)
      have_dup = TRUE;

    if (have_dup &&
        acl_set_fd(PR_FH_FD(dst_fh), facl_dup) < 0)
      pr_log_debug(DEBUG3, "error applying ACL to destination file: %s",
        strerror(errno));

    if (have_dup)
      acl_free(facl_dup);

# elif defined(HAVE_LINUX_POSIX_ACL)

    /* Linux provides the handy perm_copy_fd(3) function in its libacl
     * library just for this purpose.
     */
    if (perm_copy_fd(src, PR_FH_FD(src_fh), dst, PR_FH_FD(dst_fh), NULL) < 0)
      pr_log_debug(DEBUG3, "error copying ACL to destination file: %s",
        strerror(errno));

# elif defined(HAVE_SOLARIS_POSIX_ACL)
    int nents;

    nents = facl(PR_FH_FD(src_fh), GETACLCNT, 0, NULL);
    if (nents < 0)
      pr_log_debug(DEBUG3, "error getting source file ACL count: %s",
        strerror(errno));

    else {
      aclent_t *acls;

      acls = malloc(sizeof(aclent_t) * nents);
      if (!acls) {
        pr_log_pri(PR_LOG_CRIT, "Out of memory!");
        end_login(1);
      }

      if (facl(PR_FH_FD(src_fh), GETACL, nents, acls) < 0)
        pr_log_debug(DEBUG3, "error getting source file ACLs: %s",
          strerror(errno));

      else {
        if (facl(PR_FH_FD(dst_fh), SETACL, nents, acls) < 0)
          pr_log_debug(DEBUG3, "error setting dest file ACLs: %s",
            strerror(errno));
      }

      free(acls);
    }
# endif /* HAVE_SOLARIS_POSIX_ACL && PR_USE_FACL */
  }
#endif /* HAVE_POSIX_ACL */

  pr_fsio_close(src_fh);
  if (pr_fsio_close(dst_fh) < 0)
    pr_log_pri(PR_LOG_WARNING, "error closing '%s': %s", dst,
      strerror(errno));

  return res;
}

pr_fs_t *pr_register_fs(pool *p, const char *name, const char *path) {
  pr_fs_t *fs = NULL;

  /* Sanity check */
  if (!p || !name || !path) {
    errno = EINVAL;
    return NULL;
  }

  /* Instantiate an pr_fs_t */
  fs = pr_create_fs(p, name);
  if (fs != NULL) {

    /* Call pr_insert_fs() from here */
    if (!pr_insert_fs(fs, path)) {
      pr_log_debug(DEBUG8, "FS: error inserting fs '%s' at path '%s'",
        name, path);

      destroy_pool(fs->fs_pool);
      return NULL;
    }

  } else
    pr_log_debug(DEBUG8, "FS: error creating fs '%s'", name);

  return fs;
}

pr_fs_t *pr_create_fs(pool *p, const char *name) {
  pr_fs_t *fs = NULL;
  pool *fs_pool = NULL;

  /* Sanity check */
  if (!p || !name) {
    errno = EINVAL;
    return NULL;
  }

  /* Allocate a subpool, then allocate an pr_fs_t object from that subpool */
  fs_pool = make_sub_pool(p);
  pr_pool_tag(fs_pool, "FS Pool");

  fs = pcalloc(fs_pool, sizeof(pr_fs_t));
  if (!fs)
    return NULL;

  fs->fs_pool = fs_pool;
  fs->fs_next = fs->fs_prev = NULL;
  fs->fs_name = pstrdup(fs->fs_pool, name);
  fs->fs_next = root_fs;

  /* This is NULL until set by pr_insert_fs() */
  fs->fs_path = NULL;

  return fs;
}

int pr_insert_fs(pr_fs_t *fs, const char *path) {
  char cleaned_path[PR_TUNABLE_PATH_MAX] = {'\0'};

  if (!fs_map) {
    pool *map_pool = make_sub_pool(permanent_pool);
    pr_pool_tag(map_pool, "FSIO Map Pool");

    fs_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
  }

  /* Clean the path, but only if it starts with a '/'.  Non-local-filesystem
   * paths may not want/need to be cleaned.
   */
  if (*path == '/') {
    pr_fs_clean_path(path, cleaned_path, sizeof(cleaned_path));

    /* Cleaning the path may have removed a trailing slash, which the
     * caller may actually have wanted.  Make sure one is present in
     * the cleaned version, if it was present in the original version.
     */
    if (path[strlen(path)-1] == '/') {
      size_t len = strlen(cleaned_path);

      if (len < (PR_TUNABLE_PATH_MAX-3)) {
        cleaned_path[len] = '/';
        cleaned_path[len+1] = '\0';
      }
    }

  } else
    sstrncpy(cleaned_path, path, sizeof(cleaned_path));

  if (!fs->fs_path)
    fs->fs_path = pstrdup(fs->fs_pool, cleaned_path);

  /* Check for duplicates. */
  if (fs_map->nelts > 0) {
    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
    register int i;

    for (i = 0; i < fs_map->nelts; i++) {
      fsi = fs_objs[i];

      if (strcmp(fsi->fs_path, cleaned_path) == 0) {
        /* An entry for this path already exists.  Make sure the FS being
         * mounted is not the same as the one already present.
         */
        if (strcmp(fsi->fs_name, fs->fs_name) == 0) {
          pr_log_pri(PR_LOG_DEBUG,
            "error: duplicate fs paths not allowed: '%s'", cleaned_path);
          errno = EEXIST;
          return FALSE;
        }

        /* "Push" the given FS on top of the existing one. */
        fs->fs_next = fsi;
        fsi->fs_prev = fs;
        fs_objs[i] = fs;

        chk_fs_map = TRUE;
        return TRUE;
      }
    }
  }

  /* Push the new FS into the container, then resort the contents. */
  *((pr_fs_t **) push_array(fs_map)) = fs;

  /* Sort the FSs in the map according to their paths (if there are
   * more than one element in the array_header.
   */
  if (fs_map->nelts > 1)
    qsort(fs_map->elts, fs_map->nelts, sizeof(pr_fs_t *), fs_cmp);

  /* Set the flag so that the fs wrapper functions know that a new FS
   * has been registered.
   */
  chk_fs_map = TRUE;

  return TRUE;
}

pr_fs_t *pr_unmount_fs(const char *path, const char *name) {
  pr_fs_t *fsi = NULL, **fs_objs = NULL;
  register unsigned int i = 0;

  /* Sanity check */
  if (!path) {
    errno = EINVAL;
    return NULL;
  }

  /* This should never be called before pr_register_fs(), but, just in case...*/
  if (!fs_map) {
    errno = EACCES;
    return NULL;
  }

  fs_objs = (pr_fs_t **) fs_map->elts;

  for (i = 0; i < fs_map->nelts; i++) {
    fsi = fs_objs[i];

    if (strcmp(fsi->fs_path, path) == 0 &&
        (name ? strcmp(fsi->fs_name, name) == 0 : TRUE)) {

      /* Exact match -- remove this FS.  If there is an FS underneath, pop 
       * the top FS off the stack.  Otherwise, allocate a new map.  Then
       * iterate through the old map, pushing all other FSs into the new map.
       * Destroy the old map.  Move the new map into place.
       */

      if (fsi->fs_next == NULL) {
        register unsigned int j = 0;
        pr_fs_t *tmp_fs, **old_objs = NULL;
        pool *map_pool;
        array_header *new_map;

        /* If removing this FS would leave an empty map, don't bother
         * allocating a new one.
         */
        if (fs_map->nelts == 1) {
          destroy_pool(fs_map->pool);
          fs_map = NULL;
          fs_cwd = root_fs;

          chk_fs_map = TRUE;
          return NULL;
        }

        map_pool = make_sub_pool(permanent_pool);
        new_map = make_array(map_pool, 0, sizeof(pr_fs_t *));

        pr_pool_tag(map_pool, "FSIO Map Pool");
        old_objs = (pr_fs_t **) fs_map->elts;

        for (j = 0; j < fs_map->nelts; j++) {
          tmp_fs = old_objs[j];

          if (strcmp(tmp_fs->fs_path, path) != 0)
            *((pr_fs_t **) push_array(new_map)) = old_objs[j];
        }

        destroy_pool(fs_map->pool);
        fs_map = new_map;

        /* Don't forget to set the flag so that wrapper functions scan the
         * new map.
         */
        chk_fs_map = TRUE;

        return fsi;
      }

      /* "Pop" this FS off the stack. */
      if (fsi->fs_next)
        fsi->fs_next->fs_prev = NULL;
      fs_objs[i] = fsi->fs_next;
      fsi->fs_next = fsi->fs_prev = NULL; 

      chk_fs_map = TRUE;
      return fsi;
    }
  }

  return NULL;
}

pr_fs_t *pr_remove_fs(const char *path) {
  return pr_unmount_fs(path, NULL);
}

int pr_unregister_fs(const char *path) {
  pr_fs_t *fs = NULL;

  if (!path) {
    errno = EINVAL;
    return -1;
  }

  /* Call pr_remove_fs() to get the fs for this path removed from the
   * fs_map.
   */
  fs = pr_remove_fs(path);
  if (fs) {
    destroy_pool(fs->fs_pool);
    return 0;
  }

  errno = ENOENT;
  return -1;
}

/* This function returns the best pr_fs_t to handle the given path.  It will
 * return NULL if there are no registered pr_fs_ts to handle the given path,
 * in which case the default root_fs should be used.  This is so that
 * functions can look to see if an pr_fs_t, other than the default, for a
 * given path has been registered, if necessary.  If the return value is
 * non-NULL, that will be a registered pr_fs_t to handle the given path.  In
 * this case, if the exact argument is not NULL, it will either be TRUE,
 * signifying that the returned pr_fs_t is an exact match for the given
 * path, or FALSE, meaning the returned pr_fs_t is a "best match" -- most
 * likely the pr_fs_t that handles the directory in which the given path
 * occurs.
 */
pr_fs_t *pr_get_fs(const char *path, int *exact) {
  pr_fs_t *fs = NULL, **fs_objs = NULL, *best_match_fs = NULL;
  register unsigned int i = 0;

  /* Sanity check */
  if (!path) {
    errno = EINVAL;
    return NULL;
  }

  /* Basic optimization -- if there're no elements in the fs_map,
   * return the root_fs.
   */
  if (!fs_map ||
      fs_map->nelts == 0)
    return root_fs;

  fs_objs = (pr_fs_t **) fs_map->elts;
  best_match_fs = root_fs;

  /* In order to handle deferred-resolution paths (eg "~" paths), the given
   * path will need to be passed through dir_realpath(), if necessary.
   *
   * The chk_fs_map flag, if TRUE, should be cleared on return of this
   * function -- all that flag says is, if TRUE, that this function _might_
   * return something different than it did on a previous call.
   */

  for (i = 0; i < fs_map->nelts; i++) {
    int res = 0;

    fs = fs_objs[i];

    /* If the current pr_fs_t's path ends in a slash (meaning it is a
     * directory, and it matches the first part of the given path,
     * assume it to be the best pr_fs_t found so far.
     */
    if ((fs->fs_path)[strlen(fs->fs_path) - 1] == '/' &&
        !strncmp(path, fs->fs_path, strlen(fs->fs_path)))
      best_match_fs = fs;

    res = strcmp(fs->fs_path, path);

    if (res == 0) {

      /* Exact match */
      if (exact)
        *exact = TRUE;

      chk_fs_map = FALSE;
      return fs;

    } else if (res > 0) {

      if (exact)
        *exact = FALSE;

      chk_fs_map = FALSE;

      /* Gone too far - return the best-match pr_fs_t */
      return best_match_fs;
    }
  }

  chk_fs_map = FALSE;

  /* Return best-match by default */
  return best_match_fs;
}

#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
#ifdef PR_FS_MATCH
void pr_associate_fs(pr_fs_match_t *fsm, pr_fs_t *fs) {
  *((pr_fs_t **) push_array(fsm->fsm_fs_objs)) = fs;
}

pr_fs_match_t *pr_create_fs_match(pool *p, const char *name,
    const char *pattern, int opmask) {
  pr_fs_match_t *fsm = NULL;
  pool *match_pool = NULL;
  regex_t *regexp = NULL;
  int res = 0;
  char regerr[80] = {'\0'};

  if (!p || !name || !pattern) {
    errno = EINVAL;
    return NULL;
  }

  match_pool = make_sub_pool(p);
  fsm = (pr_fs_match_t *) pcalloc(match_pool, sizeof(pr_fs_match_t));

  if (!fsm)
    return NULL;

  fsm->fsm_next = NULL;
  fsm->fsm_prev = NULL;

  fsm->fsm_pool = match_pool;
  fsm->fsm_name = pstrdup(fsm->fsm_pool, name);
  fsm->fsm_opmask = opmask;
  fsm->fsm_pattern = pstrdup(fsm->fsm_pool, pattern);

  regexp = pr_regexp_alloc();

  if ((res = regcomp(regexp, pattern, REG_EXTENDED|REG_NOSUB)) != 0) {

    regerror(res, regexp, regerr, sizeof(regerr));
    pr_regexp_free(regexp);

    pr_log_pri(PR_LOG_ERR, "unable to compile regex '%s': %s", pattern, regerr);

    /* Destroy the just allocated pr_fs_match_t */
    destroy_pool(fsm->fsm_pool);

    return NULL;

  } else
    fsm->fsm_regex = regexp;

  /* All pr_fs_match_ts start out as null patterns, i.e. no defined callback.
   */
  fsm->trigger = NULL;

  /* Allocate an array_header, used to record the pointers of any pr_fs_ts
   * this pr_fs_match_t may register.  This array_header should be accessed
   * via associate_fs().
   */
  fsm->fsm_fs_objs = make_array(fsm->fsm_pool, 0, sizeof(pr_fs_t *));

  return fsm;
}

int pr_insert_fs_match(pr_fs_match_t *fsm) {
  pr_fs_match_t *fsmi = NULL;

  if (fs_match_list) {

    /* Find the end of the fs_match list */
    fsmi = fs_match_list;

    /* Prevent pr_fs_match_ts with duplicate names */
    if (strcmp(fsmi->fsm_name, fsm->fsm_name) == 0) {
      pr_log_pri(PR_LOG_DEBUG,
        "error: duplicate fs_match names not allowed: '%s'", fsm->fsm_name);
      return FALSE;
    }

    while (fsmi->fsm_next) {
      fsmi = fsmi->fsm_next;

      if (strcmp(fsmi->fsm_name, fsm->fsm_name) == 0) {
        pr_log_pri(PR_LOG_DEBUG,
          "error: duplicate fs_match names not allowed: '%s'", fsm->fsm_name);
        return FALSE;
      }
    }

    fsm->fsm_next = NULL;
    fsm->fsm_prev = fsmi;
    fsmi->fsm_next = fsm;

  } else

    /* This fs_match _becomes_ the start of the fs_match list */
    fs_match_list = fsm;

  return TRUE;
}

pr_fs_match_t *pr_register_fs_match(pool *p, const char *name,
    const char *pattern, int opmask) {
  pr_fs_match_t *fsm = NULL;

  /* Sanity check */
  if (!p || !name || !pattern) {
    errno = EINVAL;
    return NULL;
  }

  /* Instantiate an fs_match */
  if ((fsm = pr_create_fs_match(p, name, pattern, opmask)) != NULL) {

    /* Insert the fs_match into the list */
    if (!pr_insert_fs_match(fsm)) {
      pr_regexp_free(fsm->fsm_regex);
      destroy_pool(fsm->fsm_pool);

      return NULL;
    }
  }

  return fsm;
}

int pr_unregister_fs_match(const char *name) {
  pr_fs_match_t *fsm = NULL;
  pr_fs_t **assoc_fs_objs = NULL, *assoc_fs = NULL;
  int removed = FALSE;

  /* fs_matches are required to have duplicate names, so using the name as
   * the identifier will work.
   */

  /* Sanity check*/
  if (!name) {
    errno = EINVAL;
    return FALSE;
  }

  if (fs_match_list) {
    for (fsm = fs_match_list; fsm; fsm = fsm->fsm_next) {

      /* Search by name */
      if ((name && fsm->fsm_name && strcmp(fsm->fsm_name, name) == 0)) {

        /* Remove this fs_match from the list */
        if (fsm->fsm_prev)
          fsm->fsm_prev->fsm_next = fsm->fsm_next;

        if (fsm->fsm_next)
          fsm->fsm_next->fsm_prev = fsm->fsm_prev;

        /* Check for any pr_fs_ts this pattern may have registered, and
         * remove them as well.
         */
        assoc_fs_objs = (pr_fs_t **) fsm->fsm_fs_objs->elts;

        for (assoc_fs = *assoc_fs_objs; assoc_fs; assoc_fs++)
          pr_unregister_fs(assoc_fs->fs_path);

        pr_regexp_free(fsm->fsm_regex);
        destroy_pool(fsm->fsm_pool);

        /* If this fs_match's prev and next pointers are NULL, it is the
         * last fs_match in the list.  If this is the case, make sure
         * that fs_match_list is set to NULL, signalling that there are
         * no more registered fs_matches.
         */
        if (fsm->fsm_prev == NULL && fsm->fsm_next == NULL) {
          fs_match_list = NULL;
          fsm = NULL;
        }

        removed = TRUE;
      }
    }
  }

  return (removed ? TRUE : FALSE);
}

pr_fs_match_t *pr_get_next_fs_match(pr_fs_match_t *fsm, const char *path,
    int op) {
  pr_fs_match_t *fsmi = NULL;

  /* Sanity check */
  if (!fsm) {
    errno = EINVAL;
    return NULL;
  }

  for (fsmi = fsm->fsm_next; fsmi; fsmi = fsmi->fsm_next) {
    if ((fsmi->fsm_opmask & op) &&
        regexec(fsmi->fsm_regex, path, 0, NULL, 0) == 0)
      return fsmi;
  }

  return NULL;
}

pr_fs_match_t *pr_get_fs_match(const char *path, int op) {
  pr_fs_match_t *fsm = NULL;

  if (!fs_match_list)
    return NULL;

  /* Check the first element in the fs_match_list... */
  fsm = fs_match_list;

  if ((fsm->fsm_opmask & op) &&
      regexec(fsm->fsm_regex, path, 0, NULL, 0) == 0)
    return fsm;

  /* ...otherwise, hand the search off to pr_get_next_fs_match() */
  return pr_get_next_fs_match(fsm, path, op);
}
#endif /* PR_FS_MATCH */
#endif /* HAVE_REGEX_H && HAVE_REGCOMP */

void pr_fs_setcwd(const char *dir) {
  pr_fs_resolve_path(dir, cwd, sizeof(cwd)-1, FSIO_DIR_CHDIR);
  sstrncpy(cwd, dir, sizeof(cwd));
  fs_cwd = lookup_dir_fs(cwd, FSIO_DIR_CHDIR);
  cwd[sizeof(cwd) - 1] = '\0';
}

const char *pr_fs_getcwd(void) {
  return cwd;
}

const char *pr_fs_getvwd(void) {
  return vwd;
}

int pr_fs_dircat(char *buf, int buflen, const char *dir1, const char *dir2) {
  /* Make temporary copies so that memory areas can overlap */
  char *_dir1 = NULL, *_dir2 = NULL;
  size_t dir1len = 0;

  /* This is a test to see if we've got reasonable directories to concatenate.
   */
  if ((strlen(dir1) + strlen(dir2) + 1) >= PR_TUNABLE_PATH_MAX) {
    errno = ENAMETOOLONG;
    buf[0] = '\0';  
    return -1;
  }

  _dir1 = strdup(dir1);
  _dir2 = strdup(dir2);

  dir1len = strlen(_dir1) - 1;

  if (*_dir2 == '/') {
    sstrncpy(buf, _dir2, buflen);
    free(_dir1);
    free(_dir2);
    return 0;
  }

  sstrncpy(buf, _dir1, buflen);

  if (buflen && *(_dir1 + dir1len) != '/')
    sstrcat(buf, "/", buflen);

  sstrcat(buf, _dir2, buflen);

  if (!*buf) {
   *buf++ = '/';
   *buf = '\0';
  }

  free(_dir1);
  free(_dir2);

  return 0;
}

/* This function performs any tilde expansion needed and then returns the
 * resolved path, if any.
 *
 * Returns: -1 (errno = ENOENT): user does not exist
 *           0 : no interpolation done (path exists)
 *           1 : interpolation done
 */
int pr_fs_interpolate(const char *path, char *buf, size_t buflen) {
  pool *p = NULL;
  struct passwd *pw = NULL;
  struct stat sbuf;
  char *fname = NULL;
  char user[PR_TUNABLE_LOGIN_MAX + 1] = {'\0'};
  int len;

  if (!path) {
    errno = EINVAL;
    return -1;
  }

  if (path[0] == '~') {
    fname = strchr(path, '/');

    /* Copy over the username.
     */
    if (fname) {
      len = fname - path;
      sstrncpy(user, path + 1, len > sizeof(user) ? sizeof(user) : len);

      /* Advance past the '/'. */
      fname++;

    } else if (pr_fsio_stat(path, &sbuf) == -1) {

      /* Otherwise, this might be something like "~foo" which could be a file
       * or it could be a user.  Let's find out.
       *
       * Must be a user, if anything...otherwise it's probably a typo.
       */
      len = strlen(path);
      sstrncpy(user, path + 1, len > sizeof(user) ? sizeof(user) : len);

    } else {

      /* Otherwise, this _is_ the file in question, perform no interpolation.
       */
      fname = (char *) path;
      return 0;
    }

    /* If the user hasn't been explicitly specified, set it here.  This
     * handles cases such as files beginning with "~", "~/foo" or simply "~".
     */
    if (!*user)
      sstrncpy(user, session.user, sizeof(user));

    /* The permanent pool is used here, rather than session.pool, as path
     * interpolation can occur during startup parsing, when session.pool does
     * not exist.  It does not really matter, since the allocated sub pool
     * is destroyed shortly.
     */
    p = make_sub_pool(permanent_pool);
    pr_pool_tag(p, "pr_fs_interpolate() pool");

    pw = pr_auth_getpwnam(p, user);

    if (!pw) {
      destroy_pool(p);
      errno = ENOENT;
      return -1;
    }

    sstrncpy(buf, pw->pw_dir, buflen);

    /* Done with pw, which means we can destroy the temporary pool now. */
    destroy_pool(p);

    len = strlen(buf);

    if (fname && len < buflen && buf[len - 1] != '/')
      buf[len++] = '/';

    if (fname)
      sstrncpy(&buf[len], fname, buflen - len);

  } else
    sstrncpy(buf, path, buflen);

  return 1;
}

int pr_fs_resolve_partial(const char *path, char *buf, size_t buflen, int op) {
  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       *where = NULL, *ptr = NULL, *last = NULL;

  pr_fs_t *fs = NULL;
  int len = 0, fini = 1, link_cnt = 0;
  ino_t last_inode = 0;
  dev_t last_device = 0;
  struct stat sbuf;

  if (!path) {
    errno = EINVAL;
    return -1;
  }

  if (*path != '/') {
    if (*path == '~') {
      switch (pr_fs_interpolate(path, curpath, sizeof(curpath)-1)) {
      case -1:
        return -1;

      case 0:
        sstrncpy(curpath, path, sizeof(curpath));
        sstrncpy(workpath, cwd, sizeof(workpath));
        break;
      }

    } else {
      sstrncpy(curpath, path, sizeof(curpath));
      sstrncpy(workpath, cwd, sizeof(workpath));
    }

  } else
    sstrncpy(curpath, path, sizeof(curpath));

  while (fini--) {
    where = curpath;
    while (*where != '\0') {

      /* Handle "." */
      if (strcmp(where, ".") == 0) {
        where++;
        continue;
      }

      /* Handle ".." */
      if (strcmp(where, "..") == 0) {
        where += 2;
        ptr = last = workpath;

        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }

        *last = '\0';
        continue;
      }

      /* Handle "./" */
      if (!strncmp(where, "./", 2)) {
        where += 2;
        continue;
      }

      /* Handle "../" */
      if (!strncmp(where, "../", 3)) {
        where += 3;
        ptr = last = workpath;

        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }

        *last = '\0';
        continue;
      }

      ptr = strchr(where, '/');

      if (!ptr)
        ptr = where + strlen(where) - 1;
      else
        *ptr = '\0';

      sstrncpy(namebuf, workpath, sizeof(namebuf));

      if (*namebuf) {
        for (last = namebuf; *last; last++);
        if (*--last != '/')
          sstrcat(namebuf, "/", sizeof(namebuf)-1);

      } else
        sstrcat(namebuf, "/", sizeof(namebuf)-1);

      sstrcat(namebuf, where, sizeof(namebuf)-1);

      where = ++ptr;

      fs = lookup_dir_fs(namebuf, op);

      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1)
        return -1;

      if (S_ISLNK(sbuf.st_mode)) {
        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};

        /* Detect an obvious recursive symlink */
        if (sbuf.st_ino && (ino_t) sbuf.st_ino == last_inode &&
            sbuf.st_dev && (dev_t) sbuf.st_dev == last_device) {
          errno = ELOOP;
          return -1;
        }

        last_inode = (ino_t) sbuf.st_ino;
        last_device = (dev_t) sbuf.st_dev;

        if (++link_cnt > 32) {
          errno = ELOOP;
          return -1;
        }
	
        if (fs->readlink &&
            (len = fs->readlink(fs, namebuf, linkpath,
             sizeof(linkpath)-1)) <= 0) {
          errno = ENOENT;
          return -1;
        }

        *(linkpath + len) = '\0';
        if (*linkpath == '/')
          *workpath = '\0';

        if (*linkpath == '~') {
          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};

          *workpath = '\0';
          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));

          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) == -1)
	    return -1;
        }

        if (*where) {
          sstrcat(linkpath, "/", sizeof(linkpath)-1);
          sstrcat(linkpath, where, sizeof(linkpath)-1);
        }

        sstrncpy(curpath, linkpath, sizeof(curpath));
        fini++;
        break; /* continue main loop */
      }

      if (S_ISDIR(sbuf.st_mode)) {
        sstrncpy(workpath, namebuf, sizeof(workpath));
        continue;
      }

      if (*where) {
        errno = ENOENT;
        return -1;               /* path/notadir/morepath */

      } else {
        sstrncpy(workpath, namebuf, sizeof(workpath));
      }
    }
  }

  if (!workpath[0])
    sstrncpy(workpath, "/", sizeof(workpath));

  sstrncpy(buf, workpath, buflen);

  return 0;
}

int pr_fs_resolve_path(const char *path, char *buf, size_t buflen, int op) {
  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       *where = NULL, *ptr = NULL, *last = NULL;

  pr_fs_t *fs = NULL;

  int len = 0, fini = 1, link_cnt = 0;
  ino_t last_inode = 0;
  dev_t last_device = 0;
  struct stat sbuf;

  if (!path) {
    errno = EINVAL;
    return -1;
  }

  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1)
    sstrncpy(curpath, path, sizeof(curpath));

  if (curpath[0] != '/')
    sstrncpy(workpath, cwd, sizeof(workpath));
  else
    workpath[0] = '\0';

  while (fini--) {
    where = curpath;

    while (*where != '\0') {
      if (strcmp(where, ".") == 0) {
        where++;
        continue;
      }

      /* handle "./" */
      if (!strncmp(where, "./", 2)) {
        where += 2;
        continue;
      }

      /* handle "../" */
      if (!strncmp(where, "../", 3)) {
        where += 3;
        ptr = last = workpath;
        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }

        *last = '\0';
        continue;
      }

      ptr = strchr(where, '/');

      if (!ptr)
        ptr = where + strlen(where) - 1;
      else
        *ptr = '\0';

      sstrncpy(namebuf, workpath, sizeof(namebuf));

      if (*namebuf) {
        for (last = namebuf; *last; last++);

        if (*--last != '/')
          sstrcat(namebuf, "/", sizeof(namebuf)-1);

      } else
        sstrcat(namebuf, "/", sizeof(namebuf)-1);

      sstrcat(namebuf, where, sizeof(namebuf)-1);

      where = ++ptr;

      fs = lookup_dir_fs(namebuf, op);

      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1) {
        errno = ENOENT;
        return -1;
      }

      if (S_ISLNK(sbuf.st_mode)) {
        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};

        /* Detect an obvious recursive symlink */
        if (sbuf.st_ino && (ino_t) sbuf.st_ino == last_inode &&
            sbuf.st_dev && (dev_t) sbuf.st_dev == last_device) {
          errno = ELOOP;
          return -1;
        }

        last_inode = (ino_t) sbuf.st_ino;
        last_device = (dev_t) sbuf.st_dev;

        if (++link_cnt > 32) {
          errno = ELOOP;
          return -1;
        }

        if (fs->readlink &&
            (len = fs->readlink(fs, namebuf, linkpath,
             sizeof(linkpath)-1)) <= 0) {
          errno = ENOENT;
          return -1;
        }

        *(linkpath+len) = '\0';

        if (*linkpath == '/')
          *workpath = '\0';

        if (*linkpath == '~') {
          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
          *workpath = '\0';

          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));

          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) == -1)
	    return -1;
        }

        if (*where) {
          sstrcat(linkpath, "/", sizeof(linkpath)-1);
          sstrcat(linkpath, where, sizeof(linkpath)-1);
        }

        sstrncpy(curpath, linkpath, sizeof(curpath));
        fini++;
        break; /* continue main loop */
      }

      if (S_ISDIR(sbuf.st_mode)) {
        sstrncpy(workpath, namebuf, sizeof(workpath));
        continue;
      }

      if (*where) {
        errno = ENOENT;
        return -1;               /* path/notadir/morepath */

      } else
        sstrncpy(workpath, namebuf, sizeof(workpath));
    }
  }

  if (!workpath[0])
    sstrncpy(workpath, "/", sizeof(workpath));

  sstrncpy(buf, workpath, buflen);

  return 0;
}

void pr_fs_clean_path(const char *path, char *buf, size_t buflen) {
  char workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
  char namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
  char *where = NULL, *ptr = NULL, *last = NULL;
  int fini = 1;

  if (!path)
    return;

  sstrncpy(curpath, path, sizeof(curpath));

  /* main loop */
  while (fini--) {
    where = curpath;
    while (*where != '\0') {
      if (strcmp(where, ".") == 0) {
        where++;
        continue;
      }

      /* handle "./" */
      if (!strncmp(where, "./", 2)) {
        where += 2;
        continue;
      }

      /* handle ".." */
      if (strcmp(where, "..") == 0) {
        where += 2;
        ptr = last = workpath;

        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }

        *last = '\0';
        continue;
      }

      /* handle "../" */
      if (!strncmp(where, "../", 3)) {
        where += 3;
        ptr = last = workpath;

        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }
        *last = '\0';
        continue;
      }
      ptr = strchr(where, '/');

      if (!ptr)
        ptr = where + strlen(where) - 1;
      else
        *ptr = '\0';

      sstrncpy(namebuf, workpath, sizeof(namebuf));

      if (*namebuf) {
        for (last = namebuf; *last; last++);
        if (*--last != '/')
          sstrcat(namebuf, "/", sizeof(namebuf)-1);

      } else
        sstrcat(namebuf, "/", sizeof(namebuf)-1);

      sstrcat(namebuf, where, sizeof(namebuf)-1);
      namebuf[sizeof(namebuf)-1] = '\0';

      where = ++ptr;

      sstrncpy(workpath, namebuf, sizeof(workpath));
    }
  }

  if (!workpath[0])
    sstrncpy(workpath, "/", sizeof(workpath));

  sstrncpy(buf, workpath, buflen);
}

/* This function checks the given path's prefix against the paths that
 * have been registered.  If no matching path prefix has been registered,
 * the path is considered invalid.
 */
int pr_fs_valid_path(const char *path) {

  if (fs_map && fs_map->nelts > 0) {
    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
    register int i;

    for (i = 0; i < fs_map->nelts; i++) {
      fsi = fs_objs[i];

      if (strncmp(fsi->fs_path, path, strlen(fsi->fs_path)) == 0)
        return 0;
    }
  }

  /* Also check the path against the default '/' path. */
  if (*path == '/')
    return 0;

  errno = EINVAL;
  return -1;
}

void pr_fs_virtual_path(const char *path, char *buf, size_t buflen) {
  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
       *where = NULL, *ptr = NULL, *last = NULL;

  int fini = 1;

  if (!path)
    return;

  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1)
    sstrncpy(curpath, path, sizeof(curpath));

  if (curpath[0] != '/')
    sstrncpy(workpath, vwd, sizeof(workpath));
  else
    workpath[0] = '\0';

  /* curpath is path resolving */
  /* linkpath is path a symlink pointed to */
  /* workpath is the path we've resolved */

  /* main loop */
  while (fini--) {
    where = curpath;
    while (*where != '\0') {
      if (strcmp(where, ".") == 0) {
        where++;
        continue;
      }

      /* handle "./" */
      if (!strncmp(where, "./", 2)) {
        where += 2;
        continue;
      }

      /* handle ".." */
      if (strcmp(where, "..") == 0) {
        where += 2;
        ptr = last = workpath;
        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }

        *last = '\0';
        continue;
      }

      /* handle "../" */
      if (!strncmp(where, "../", 3)) {
        where += 3;
        ptr = last = workpath;
        while (*ptr) {
          if (*ptr == '/')
            last = ptr;
          ptr++;
        }
        *last = '\0';
        continue;
      }
      ptr = strchr(where, '/');

      if (!ptr)
        ptr = where + strlen(where) - 1;
      else
        *ptr = '\0';

      sstrncpy(namebuf, workpath, sizeof(namebuf));

      if (*namebuf) {
        for (last = namebuf; *last; last++);
        if (*--last != '/')
          sstrcat(namebuf, "/", sizeof(namebuf)-1);

      } else
        sstrcat(namebuf, "/", sizeof(namebuf)-1);

      sstrcat(namebuf, where, sizeof(namebuf)-1);

      where = ++ptr;

      sstrncpy(workpath, namebuf, sizeof(workpath));
    }
  }

  if (!workpath[0])
    sstrncpy(workpath, "/", sizeof(workpath));

  sstrncpy(buf, workpath, buflen);
}

int pr_fsio_chdir_canon(const char *path, int hidesymlink) {
  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  pr_fs_t *fs = NULL;
  int res = 0;

  if (pr_fs_resolve_partial(path, resbuf, sizeof(resbuf)-1,
      FSIO_DIR_CHDIR) == -1)
    return -1;

  fs = lookup_dir_fs(resbuf, FSIO_DIR_CHDIR);

  /* Find the first non-NULL custom chdir handler.  If there are none,
   * use the system chdir.
   */
  while (fs && fs->fs_next && !fs->chdir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chdir()", fs->fs_name);
  res = fs->chdir(fs, resbuf);

  if (res != -1) {
    /* chdir succeeded, so we set fs_cwd for future references. */
     fs_cwd = fs ? fs : root_fs;

     if (hidesymlink)
       pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
     else
       sstrncpy(vwd, resbuf, sizeof(vwd));
  }

  return res;
}

int pr_fsio_chdir(const char *path, int hidesymlink) {
  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
  pr_fs_t *fs = NULL;
  int res;

  pr_fs_clean_path(path, resbuf, sizeof(resbuf)-1);

  fs = lookup_dir_fs(path, FSIO_DIR_CHDIR);

  /* Find the first non-NULL custom chdir handler.  If there are none,
   * use the system chdir.
   */
  while (fs && fs->fs_next && !fs->chdir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chdir()", fs->fs_name);
  res = fs->chdir(fs, resbuf);

  if (res != -1) {
    /* chdir succeeded, so we set fs_cwd for future references. */
     fs_cwd = fs;

     if (hidesymlink)
       pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
     else
       sstrncpy(vwd, resbuf, sizeof(vwd));
  }

  return res;
}

/* fs_opendir, fs_closedir and fs_readdir all use a nifty
 * optimization, caching the last-recently-used pr_fs_t, and
 * avoid future pr_fs_t lookups when iterating via readdir.
 */
void *pr_fsio_opendir(const char *path) {
  pr_fs_t *fs = NULL;
  fsopendir_t *fsod = NULL, *fsodi = NULL;
  pool *fsod_pool = NULL;
  DIR *res = NULL;

  if (strchr(path, '/') == NULL) {
    fs = fs_cwd;

  } else {
    char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};

    if (pr_fs_resolve_partial(path, buf, sizeof(buf)-1, FSIO_DIR_OPENDIR) == -1)
      return NULL;

    fs = lookup_dir_fs(buf, FSIO_DIR_OPENDIR);
  }

  /* Find the first non-NULL custom opendir handler.  If there are none,
   * use the system opendir.
   */
  while (fs && fs->fs_next && !fs->opendir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s opendir()", fs->fs_name);
  res = fs->opendir(fs, path);

  if (!res)
    return NULL;

  /* Cache it here */
  fs_cache_dir = res;
  fs_cache_fsdir = fs;

  fsod_pool = make_sub_pool(permanent_pool);
  pr_pool_tag(fsod_pool, "fsod subpool");

  fsod = pcalloc(fsod_pool, sizeof(fsopendir_t));

  if (!fsod) {

    if (fs->closedir) {
      fs->closedir(fs, res);
      errno = ENOMEM;
      return NULL;

    } else {
      sys_closedir(fs, res);
      errno = ENOMEM;
      return NULL;
    }
  }

  fsod->pool = fsod_pool;
  fsod->dir = res;
  fsod->fsdir = fs;
  fsod->next = NULL;
  fsod->prev = NULL;

  if (fsopendir_list) {

    /* find the end of the fsopendir list */
    fsodi = fsopendir_list;
    while (fsodi->next)
      fsodi = fsodi->next;

    fsod->next = NULL;
    fsod->prev = fsodi;
    fsodi->next = fsod;

  } else

    /* This fsopendir _becomes_ the start of the fsopendir list */
    fsopendir_list = fsod;

  return res;
}

static pr_fs_t *find_opendir(void *dir, int closing) {
  pr_fs_t *fs = NULL;

  if (fsopendir_list) {
    fsopendir_t *fsod;

    for (fsod = fsopendir_list; fsod; fsod = fsod->next) {
      if (fsod->dir && fsod->dir == dir) {
        fs = fsod->fsdir;
        break;
      }
    }
   
    if (closing && fsod) {
      if (fsod->prev)
        fsod->prev->next = fsod->next;
 
      if (fsod->next)
        fsod->next->prev = fsod->prev;

      if (fsod == fsopendir_list)
        fsopendir_list = fsod->next;

      destroy_pool(fsod->pool);
    }
  }

  if (dir == fs_cache_dir) {
    fs = fs_cache_fsdir;

    if (closing) {
      fs_cache_dir = NULL;
      fs_cache_fsdir = NULL;
    }
  }

  return fs;
}

int pr_fsio_closedir(void *dir) {
  int res;
  pr_fs_t *fs = find_opendir(dir, TRUE);

  if (!fs)
    return -1;

  /* Find the first non-NULL custom closedir handler.  If there are none,
   * use the system closedir.
   */
  while (fs && fs->fs_next && !fs->closedir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s closedir()", fs->fs_name);
  res = fs->closedir(fs, dir);

  return res;
}

struct dirent *pr_fsio_readdir(void *dir) {
  struct dirent *res;
  pr_fs_t *fs = find_opendir(dir, FALSE);

  if (!fs)
    return NULL;

  /* Find the first non-NULL custom readdir handler.  If there are none,
   * use the system readdir.
   */
  while (fs && fs->fs_next && !fs->readdir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s readdir()", fs->fs_name);
  res = fs->readdir(fs, dir);

  return res;
}

int pr_fsio_mkdir(const char *path, mode_t mode) {
  int res;
  pr_fs_t *fs = lookup_dir_fs(path, FSIO_DIR_MKDIR);

  /* Find the first non-NULL custom mkdir handler.  If there are none,
   * use the system mkdir.
   */
  while (fs && fs->fs_next && !fs->mkdir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s mkdir()", fs->fs_name);
  res = fs->mkdir(fs, path, mode);

  return res;
}

int pr_fsio_rmdir(const char *path) {
  int res;
  pr_fs_t *fs = lookup_dir_fs(path, FSIO_DIR_RMDIR);

  /* Find the first non-NULL custom rmdir handler.  If there are none,
   * use the system rmdir.
   */
  while (fs && fs->fs_next && !fs->rmdir)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s rmdir()", fs->fs_name);
  res = fs->rmdir(fs, path);

  return res;
}

int pr_fsio_stat_canon(const char *path, struct stat *sbuf) {
  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_STAT);

  /* Find the first non-NULL custom stat handler.  If there are none,
   * use the system stat.
   */
  while (fs && fs->fs_next && !fs->stat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s stat()", fs ? fs->fs_name : "system");
  return fs_cache_stat(fs ? fs : root_fs, path, sbuf);
}

int pr_fsio_stat(const char *path, struct stat *sbuf) {
  pr_fs_t *fs = lookup_file_fs(path, NULL, FSIO_FILE_STAT);

  /* Find the first non-NULL custom stat handler.  If there are none,
   * use the system stat.
   */
  while (fs && fs->fs_next && !fs->stat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s stat()", fs->fs_name);
  return fs_cache_stat(fs ? fs : root_fs, path, sbuf);
}

int pr_fsio_fstat(pr_fh_t *fh, struct stat *sbuf) {
  int res;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom fstat handler.  If there are none,
   * use the system fstat.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->fstat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s fstat()", fs->fs_name);
  res = fs->fstat(fh, fh->fh_fd, sbuf);

  return res;
}

int pr_fsio_lstat_canon(const char *path, struct stat *sbuf) {
  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_LSTAT);

  /* Find the first non-NULL custom lstat handler.  If there are none,
   * use the system lstat.
   */
  while (fs && fs->fs_next && !fs->lstat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s lstat()", fs ? fs->fs_name : "system");
  return fs_cache_lstat(fs ? fs : root_fs, path, sbuf);
}

int pr_fsio_lstat(const char *path, struct stat *sbuf) {
  pr_fs_t *fs = lookup_file_fs(path, NULL, FSIO_FILE_LSTAT);

  /* Find the first non-NULL custom lstat handler.  If there are none,
   * use the system lstat.
   */
  while (fs && fs->fs_next && !fs->lstat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s lstat()", fs->fs_name);
  return fs_cache_lstat(fs ? fs : root_fs, path, sbuf);
}

int pr_fsio_readlink_canon(const char *path, char *buf, size_t buflen) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_READLINK);

  /* Find the first non-NULL custom readlink handler.  If there are none,
   * use the system readlink.
   */
  while (fs && fs->fs_next && !fs->readlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s readlink()", fs->fs_name);
  res = fs->readlink(fs, path, buf, buflen);

  return res;
}

int pr_fsio_readlink(const char *path, char *buf, size_t buflen) {
  int res;
  pr_fs_t *fs = lookup_file_fs(path, NULL, FSIO_FILE_READLINK);

  /* Find the first non-NULL custom readlink handler.  If there are none,
   * use the system readlink.
   */
  while (fs && fs->fs_next && !fs->readlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s readlink()", fs->fs_name);
  res = fs->readlink(fs, path, buf, buflen);

  return res;
}

/* pr_fs_glob() is just a wrapper for glob(3), setting the various gl_
 * callbacks to our fs functions.
 */
int pr_fs_glob(const char *pattern, int flags,
    int (*errfunc)(const char *, int), glob_t *pglob) {

  if (pglob) {
    flags |= GLOB_ALTDIRFUNC;

    pglob->gl_closedir = (void (*)(void *)) pr_fsio_closedir;
    pglob->gl_readdir = pr_fsio_readdir;
    pglob->gl_opendir = pr_fsio_opendir;
    pglob->gl_lstat = pr_fsio_lstat;
    pglob->gl_stat = pr_fsio_stat;
  }

  return glob(pattern, flags, errfunc, pglob);
}

void pr_fs_globfree(glob_t *pglob) {
  globfree(pglob);
}

int pr_fsio_rename_canon(const char *rfrom, const char *rto) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(rfrom, NULL, FSIO_FILE_RENAME);

  if (fs != lookup_file_canon_fs(rto, NULL, FSIO_FILE_RENAME)) {
    errno = EXDEV;
    return -1;
  }

  /* Find the first non-NULL custom rename handler.  If there are none,
   * use the system rename.
   */
  while (fs && fs->fs_next && !fs->rename)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s rename()", fs->fs_name);
  res = fs->rename(fs, rfrom, rto);

  return res;
}

int pr_fsio_rename(const char *rnfm, const char *rnto) {
  int res;
  pr_fs_t *fs = lookup_file_fs(rnfm, NULL, FSIO_FILE_RENAME);

  if (fs != lookup_file_fs(rnto, NULL, FSIO_FILE_RENAME)) {
    errno = EXDEV;
    return -1;
  }

  /* Find the first non-NULL custom rename handler.  If there are none,
   * use the system rename.
   */
  while (fs && fs->fs_next && !fs->rename)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s rename()", fs->fs_name);
  res = fs->rename(fs, rnfm, rnto);

  return res;
}

int pr_fsio_unlink_canon(const char *name) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_UNLINK);

  /* Find the first non-NULL custom unlink handler.  If there are none,
   * use the system unlink.
   */
  while (fs && fs->fs_next && !fs->unlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s unlink()", fs->fs_name);
  res = fs->unlink(fs, name);

  return res;
}
	
int pr_fsio_unlink(const char *name) {
  int res;
  pr_fs_t *fs = lookup_file_fs(name, NULL, FSIO_FILE_UNLINK);

  /* Find the first non-NULL custom unlink handler.  If there are none,
   * use the system unlink.
   */
  while (fs && fs->fs_next && !fs->unlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s unlink()", fs->fs_name);
  res = fs->unlink(fs, name);

  return res;
}

pr_fh_t *pr_fsio_open_canon(const char *name, int flags) {
  char *deref = NULL;
  pool *tmp_pool = NULL;
  pr_fh_t *fh = NULL;

  pr_fs_t *fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_OPEN);

  /* Allocate a filehandle. */
  tmp_pool = make_sub_pool(fs->fs_pool);
  pr_pool_tag(tmp_pool, "pr_fsio_open_canon() subpool");

  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
  fh->fh_pool = tmp_pool;
  fh->fh_path = pstrdup(fh->fh_pool, name);
  fh->fh_fd = -1;
  fh->fh_buf = NULL;
  fh->fh_fs = fs;

  /* Find the first non-NULL custom open handler.  If there are none,
   * use the system open.
   */
  while (fs && fs->fs_next && !fs->open)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s open()", fs->fs_name);
  fh->fh_fd = fs->open(fh, deref, flags);

  if (fh->fh_fd == -1) {
    destroy_pool(fh->fh_pool);
    return NULL;
  }

  return fh;
}

pr_fh_t *pr_fsio_open(const char *name, int flags) {
  pool *tmp_pool = NULL;
  pr_fh_t *fh = NULL;
  pr_fs_t *fs = NULL;

  if (!name) {
    errno = EINVAL;
    return NULL;
  }

  fs = lookup_file_fs(name, NULL, FSIO_FILE_OPEN);

  /* Allocate a filehandle. */
  tmp_pool = make_sub_pool(fs->fs_pool);
  pr_pool_tag(tmp_pool, "pr_fsio_open() subpool");

  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
  fh->fh_pool = tmp_pool;
  fh->fh_path = pstrdup(fh->fh_pool, name);
  fh->fh_fd = -1;
  fh->fh_buf = NULL;
  fh->fh_fs = fs;

  /* Find the first non-NULL custom open handler.  If there are none,
   * use the system open.
   */
  while (fs && fs->fs_next && !fs->open)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s open()", fs->fs_name);
  fh->fh_fd = fs->open(fh, name, flags);

  if (fh->fh_fd == -1) {
    destroy_pool(fh->fh_pool);
    return NULL;
  }

  return fh;
}

pr_fh_t *pr_fsio_creat_canon(const char *name, mode_t mode) {
  char *deref = NULL;
  pool *tmp_pool = NULL;
  pr_fh_t *fh = NULL;
  pr_fs_t *fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_CREAT);

  /* Allocate a filehandle. */
  tmp_pool = make_sub_pool(fs->fs_pool);
  pr_pool_tag(tmp_pool, "pr_fsio_creat_canon() subpool");

  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
  fh->fh_pool = tmp_pool;
  fh->fh_path = pstrdup(fh->fh_pool, name);
  fh->fh_fd = -1;
  fh->fh_buf = NULL;
  fh->fh_fs = fs;

  /* Find the first non-NULL custom creat handler.  If there are none,
   * use the system creat.
   */
  while (fs && fs->fs_next && !fs->creat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s creat()", fs->fs_name);
  fh->fh_fd = fs->creat(fh, deref, mode);

  if (fh->fh_fd == -1) {
    destroy_pool(fh->fh_pool);
    return NULL;
  }

  return fh;
}

pr_fh_t *pr_fsio_creat(const char *name, mode_t mode) {
  pool *tmp_pool = NULL;
  pr_fh_t *fh = NULL;
  pr_fs_t *fs = lookup_file_fs(name, NULL, FSIO_FILE_CREAT);

  /* Allocate a filehandle. */
  tmp_pool = make_sub_pool(fs->fs_pool);
  pr_pool_tag(tmp_pool, "pr_fsio_creat() subpool");

  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
  fh->fh_pool = tmp_pool;
  fh->fh_path = pstrdup(fh->fh_pool, name);
  fh->fh_fd = -1;
  fh->fh_buf = NULL;
  fh->fh_fs = fs;

  /* Find the first non-NULL custom creat handler.  If there are none,
   * use the system creat.
   */
  while (fs && fs->fs_next && !fs->creat)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s creat()", fs->fs_name);
  fh->fh_fd = fs->creat(fh, name, mode);

  if (fh->fh_fd == -1) {
    destroy_pool(fh->fh_pool);
    return NULL;
  }

  return fh;
}

int pr_fsio_close(pr_fh_t *fh) {
  int res = 0;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom close handler.  If there are none,
   * use the system close.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->close)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s close()", fs->fs_name);
  res = fs->close(fh, fh->fh_fd);

  destroy_pool(fh->fh_pool);
  return res;
}

int pr_fsio_read(pr_fh_t *fh, char *buf, size_t size) {
  int res;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom read handler.  If there are none,
   * use the system read.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->read)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s read()", fs->fs_name);
  res = fs->read(fh, fh->fh_fd, buf, size);

  return res;
}

int pr_fsio_write(pr_fh_t *fh, const char *buf, size_t size) {
  int res;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom write handler.  If there are none,
   * use the system write.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->write)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s write()", fs->fs_name);
  res = fs->write(fh, fh->fh_fd, buf, size);

  return res;
}

off_t pr_fsio_lseek(pr_fh_t *fh, off_t offset, int whence) {
  off_t res;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom lseek handler.  If there are none,
   * use the system lseek.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->lseek)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s lseek()", fs->fs_name);
  res = fs->lseek(fh, fh->fh_fd, offset, whence);

  return res;
}

int pr_fsio_link_canon(const char *lfrom, const char *lto) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(lfrom, NULL, FSIO_FILE_LINK);

  if (fs != lookup_file_canon_fs(lto, NULL, FSIO_FILE_LINK)) {
    errno = EXDEV;
    return -1;
  }

  /* Find the first non-NULL custom link handler.  If there are none,
   * use the system link.
   */
  while (fs && fs->fs_next && !fs->link)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s link()", fs->fs_name);
  res = fs->link(fs, lfrom, lto);

  return res;
}

int pr_fsio_link(const char *lfrom, const char *lto) {
  int res;
  pr_fs_t *fs = lookup_file_fs(lfrom, NULL, FSIO_FILE_LINK);

  if (fs != lookup_file_fs(lto, NULL, FSIO_FILE_LINK)) {
    errno = EXDEV;
    return -1;
  }

  /* Find the first non-NULL custom link handler.  If there are none,
   * use the system link.
   */
  while (fs && fs->fs_next && !fs->link)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s link()", fs->fs_name);
  res = fs->link(fs, lfrom, lto);

  return res;
}

int pr_fsio_symlink_canon(const char *lfrom, const char *lto) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(lto, NULL, FSIO_FILE_SYMLINK);

  /* Find the first non-NULL custom symlink handler.  If there are none,
   * use the system symlink
   */
  while (fs && fs->fs_next && !fs->symlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s symlink()", fs->fs_name);
  res = fs->symlink(fs, lfrom, lto);

  return res;
}

int pr_fsio_symlink(const char *lfrom, const char *lto) {
  int res;
  pr_fs_t *fs = lookup_file_fs(lto, NULL, FSIO_FILE_SYMLINK);

  /* Find the first non-NULL custom symlink handler.  If there are none,
   * use the system symlink.
   */
  while (fs && fs->fs_next && !fs->symlink)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s symlink()", fs->fs_name);
  res = fs->symlink(fs, lfrom, lto);

  return res;
}

int pr_fsio_ftruncate(pr_fh_t *fh, off_t len) {
  int res;
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom ftruncate handler.  If there are none,
   * use the system ftruncate.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->ftruncate)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s ftruncate()", fs->fs_name);
  res = fs->ftruncate(fh, fh->fh_fd, len);

  return res;
}

int pr_fsio_truncate_canon(const char *path, off_t len) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_TRUNC);

  /* Find the first non-NULL custom truncate handler.  If there are none,
   * use the system truncate.
   */
  while (fs && fs->fs_next && !fs->truncate)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s truncate()", fs->fs_name);
  res = fs->truncate(fs, path, len);

  return res;
}

int pr_fsio_truncate(const char *path, off_t len) {
  int res;
  pr_fs_t *fs = lookup_file_fs(path, NULL, FSIO_FILE_TRUNC);

  /* Find the first non-NULL custom truncate handler.  If there are none,
   * use the system truncate.
   */
  while (fs && fs->fs_next && !fs->truncate)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s truncate()", fs->fs_name);
  res = fs->truncate(fs, path, len);
  
  return res;
}

int pr_fsio_chmod_canon(const char *name, mode_t mode) {
  int res;
  char *deref = NULL;
  pr_fs_t *fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_CHMOD);

  /* Find the first non-NULL custom chmod handler.  If there are none,
   * use the system chmod.
   */
  while (fs && fs->fs_next && !fs->chmod)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chmod()", fs->fs_name);
  res = fs->chmod(fs, deref, mode);

  return res;
}

int pr_fsio_chmod(const char *name, mode_t mode) {
  int res;
  pr_fs_t *fs = lookup_file_fs(name, NULL, FSIO_FILE_CHMOD);

  /* Find the first non-NULL custom chmod handler.  If there are none,
   * use the system chmod.
   */
  while (fs && fs->fs_next && !fs->chmod)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chmod()", fs->fs_name);
  res = fs->chmod(fs, name, mode);
  
  return res;
}

int pr_fsio_chown_canon(const char *name, uid_t uid, gid_t gid) {
  int res;
  pr_fs_t *fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_CHOWN);

  /* Find the first non-NULL custom chown handler.  If there are none,
   * use the system chown.
   */
  while (fs && fs->fs_next && !fs->chown)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chown()", fs->fs_name);
  res = fs->chown(fs, name, uid, gid);
  
  return res;
}

int pr_fsio_chown(const char *name, uid_t uid, gid_t gid) {
  int res;
  pr_fs_t *fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN);

  /* Find the first non-NULL custom chown handler.  If there are none,
   * use the system chown.
   */
  while (fs && fs->fs_next && !fs->chown)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chown()", fs->fs_name);
  res = fs->chown(fs, name, uid, gid);
  
  return res;
}

int pr_fsio_access(const char *path, int mode, uid_t uid, gid_t gid,
    array_header *suppl_gids) {
  pr_fs_t *fs;

  if (!path) {
    errno = EINVAL;
    return -1;
  }

  fs = lookup_file_fs(path, NULL, FSIO_FILE_ACCESS);

  /* Find the first non-NULL custom access handler.  If there are none,
   * use the system access.
   */
  while (fs && fs->fs_next && !fs->access)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s access()", fs->fs_name);
  return fs->access(fs, path, mode, uid, gid, suppl_gids);
}

int pr_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
    array_header *suppl_gids) {
  pr_fs_t *fs;

  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  /* Find the first non-NULL custom faccess handler.  If there are none,
   * use the system faccess.
   */
  fs = fh->fh_fs;
  while (fs && fs->fs_next && !fs->faccess)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s faccess()", fs->fs_name);
  return fh->fh_fs->faccess(fh, mode, uid, gid, suppl_gids);
}

/* If the wrapped chroot() function suceeds (eg returns 0), then all
 * pr_fs_ts currently registered in the fs_map will have their paths
 * rewritten to reflect the new root.
 */
int pr_fsio_chroot(const char *path) {
  int res = 0;
  pr_fs_t *fs = lookup_dir_fs(path, FSIO_DIR_CHROOT);

  /* Find the first non-NULL custom chroot handler.  If there are none,
   * use the system chroot.
   */
  while (fs && fs->fs_next && !fs->chroot)
    fs = fs->fs_next;

  pr_log_debug(DEBUG9, "FS: using %s chroot()", fs->fs_name);
  res = fs->chroot(fs, path);
  if (res == 0) {
    unsigned int iter_start = 0;

    /* The filesystem handles in fs_map need to be readjusted to the new root.
     */
    register unsigned int i = 0;
    pool *map_pool = make_sub_pool(permanent_pool);
    array_header *new_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
    pr_fs_t **fs_objs = NULL;

    pr_pool_tag(map_pool, "FSIO Map Pool");

    if (fs_map)
      fs_objs = (pr_fs_t **) fs_map->elts;

    if (fs != root_fs) {
      if (strncmp(fs->fs_path, path, strlen(path)) == 0) {
        memmove(fs->fs_path, fs->fs_path + strlen(path),
          strlen(fs->fs_path) - strlen(path) + 1);
      }

      *((pr_fs_t **) push_array(new_map)) = fs;
      iter_start = 1;
    }

    for (i = iter_start; i < (fs_map ? fs_map->nelts : 0); i++) {
      pr_fs_t *tmpfs = fs_objs[i];

      /* The memory for this field has already been allocated, so futzing
       * with it like this should be fine.  Watch out for any paths that
       * may be different, e.g. added manually, not through pr_register_fs().
       * Any absolute paths that are outside of the chroot path are discarded.
       * Deferred-resolution paths (eg "~" paths) and relative paths are kept.
       */

      if (strncmp(tmpfs->fs_path, path, strlen(path)) == 0) {
        pr_fs_t *next;

        memmove(tmpfs->fs_path, tmpfs->fs_path + strlen(path),
          strlen(tmpfs->fs_path) - strlen(path) + 1);

        /* Need to do this for any stacked FSs as well. */
        next = tmpfs->fs_next;
        while (next) {
          memmove(next->fs_path, next->fs_path + strlen(path),
            strlen(next->fs_path) - strlen(path) + 1);

          next = next->fs_next;
        }
      }

      /* Add this FS to the new fs_map. */
      *((pr_fs_t **) push_array(new_map)) = tmpfs;
    }

    /* Sort the new map */
    qsort(new_map->elts, new_map->nelts, sizeof(pr_fs_t *), fs_cmp);

    /* Destroy the old map */
    if (fs_map)
      destroy_pool(fs_map->pool);

    fs_map = new_map;
    chk_fs_map = TRUE;
  }

  return res;
}

char *pr_fsio_gets(char *buf, size_t size, pr_fh_t *fh) {
  char *bp = NULL;
  int toread = 0;
  pr_buffer_t *pbuf = NULL;

  if (!buf || !fh || size <= 0) {
    errno = EINVAL;
    return NULL;
  }

  if (!fh->fh_buf) {
    fh->fh_buf = pcalloc(fh->fh_pool, sizeof(pr_buffer_t));
    fh->fh_buf->buf = fh->fh_buf->current = pcalloc(fh->fh_pool,
      PR_TUNABLE_BUFFER_SIZE);
    fh->fh_buf->remaining = fh->fh_buf->buflen = PR_TUNABLE_BUFFER_SIZE;
  }

  pbuf = fh->fh_buf;
  bp = buf;

  while (size) {
    if (!pbuf->current ||
        pbuf->remaining == pbuf->buflen) { /* empty buffer */

      toread = pr_fsio_read(fh, pbuf->buf,
        size < pbuf->buflen ? size : pbuf->buflen);

      if (toread <= 0) {
        if (bp != buf) {
          *bp = '\0';
          return buf;

        } else
          return NULL;
      }

      pbuf->remaining = pbuf->buflen - toread;
      pbuf->current = pbuf->buf;

    } else
      toread = pbuf->buflen - pbuf->remaining;

    while (size && toread > 0 && *pbuf->current != '\n' && toread--) {
      *bp++ = *pbuf->current++;
      size--;
      pbuf->remaining++;
    }

    if (size && toread && *pbuf->current == '\n') {
      size--;
      toread--;
      *bp++ = *pbuf->current++;
      pbuf->remaining++;
      break;
    }

    if (!toread)
      pbuf->current = NULL;
  }

  *bp = '\0';
  return buf;
}

/* pr_fsio_getline() is an fgets() with backslash-newline stripping, copied from
 * Wietse Venema's tcpwrapppers-7.6 code.  The extra *lineno argument is
 * needed, at the moment, to properly track which line of the configuration
 * file is being read in, so that errors can be reported with line numbers
 * correctly.
 */
char *pr_fsio_getline(char *buf, int buflen, pr_fh_t *fh,
    unsigned int *lineno) {
  int inlen;
  char *start = buf;

  while (pr_fsio_gets(buf, buflen, fh)) {

    /* while() loops should always handle signals. */
    pr_signals_handle();

    inlen = strlen(buf);

    if (inlen >= 1) {
      if (buf[inlen - 1] == '\n') {
        (*lineno)++;

        if (inlen >= 2 && buf[inlen - 2] == '\\') {
          char *bufp;

          inlen -= 2;
      
          /* Watch for commented lines when handling line continuations.
           * Advance past any leading whitespace, to see if the first
           * non-whitespace character is the comment character.
           */
          for (bufp = buf; *bufp && isspace((int) *bufp); bufp++);

          if (*bufp == '#')
             continue;
 
        } else
          return start;
      }
    }

    /* Be careful of reading too much. */
    if (buflen - inlen == 0)
      return buf;

    buf += inlen;
    buflen -= inlen;
    buf[0] = 0;
  }

  return (buf > start ? start : 0);
}

#if defined(HAVE_SYS_STATVFS_H) || defined(HAVE_SYS_VFS_H) || defined(HAVE_STATFS)

/* Simple multiplication and division doesn't work with very large
 * filesystems (overflows 32 bits).  This code should handle it.
 */
static off_t calc_fs_size(size_t blocks, size_t bsize) {
  off_t bl_lo, bl_hi;
  off_t res_lo, res_hi, tmp;

  bl_lo = blocks & 0x0000ffff;
  bl_hi = blocks & 0xffff0000;

  tmp = (bl_hi >> 16) * bsize;
  res_hi = tmp & 0xffff0000;
  res_lo = (tmp & 0x0000ffff) << 16;
  res_lo += bl_lo * bsize;

  if (res_hi & 0xfc000000)
    /* Overflow */
    return 0;

  return (res_lo >> 10) | (res_hi << 6);
}

off_t pr_fs_getsize(char *path) {
# if defined(HAVE_SYS_STATVFS_H)

#  if _FILE_OFFSET_BITS == 64 && defined(SOLARIS2) && \
   !defined(SOLARIS2_5_1) && !defined(SOLARIS2_6) && !defined(SOLARIS2_7)
  /* Note: somewhere along the way, Sun decided that the prototype for
   * its statvfs64(2) function would include a statvfs64_t rather than
   * struct statvfs64.  In 2.6 and 2.7, it's struct statvfs64, and
   * in 8, 9 it's statvfs64_t.  This should silence compiler warnings.
   * (The statvfs_t will be redefined to a statvfs64_t as appropriate on
   * LFS systems).
   */
  statvfs_t fs;
#  else
  struct statvfs fs;
#  endif /* LFS && !Solaris 2.5.1 && !Solaris 2.6 && !Solaris 2.7 */

  if (statvfs(path, &fs) != 0)
    return 0;

  return calc_fs_size(fs.f_bavail, fs.f_frsize);

# elif defined(HAVE_SYS_VFS_H)
  struct statfs fs;

  if (statfs(path, &fs) != 0)
    return 0;

  return calc_fs_size(fs.f_bavail, fs.f_bsize);
# elif defined(HAVE_STATFS)
  struct statfs fs;

  if (statfs(path, &fs) != 0)
    return 0;

  return calc_fs_size(fs.f_bavail, fs.f_bsize);
# endif /* !HAVE_STATFS && !HAVE_SYS_STATVFS && !HAVE_SYS_VFS */
}
#endif /* !HAVE_STATFS && !HAVE_SYS_STATVFS && !HAVE_SYS_VFS */

int pr_fsio_puts(const char *buf, pr_fh_t *fh) {
  if (!fh) {
    errno = EINVAL;
    return -1;
  }

  return pr_fsio_write(fh, buf, strlen(buf));
}

void pr_resolve_fs_map(void) {
  register unsigned int i = 0;

  if (!fs_map)
    return;

  for (i = 0; i < fs_map->nelts; i++) {
    char *newpath = NULL;
    unsigned char add_slash = FALSE;
    pr_fs_t *tmpfs = ((pr_fs_t **) fs_map->elts)[i];

    /* Skip if this fs is the root fs. */
    if (tmpfs == root_fs)
      continue;

    /* Note that dir_realpath() does _not_ handle "../blah" paths
     * well, so...at least for now, hope that such paths are screened
     * by the code adding such paths into the fs_map.  Check for
     * a trailing slash in the unadjusted path, so that I know if I need
     * to re-add that slash to the adjusted path -- these trailing slashes
     * are important!
     */
    if ((strcmp(tmpfs->fs_path, "/") != 0 &&
        (tmpfs->fs_path)[strlen(tmpfs->fs_path) - 1] == '/'))
      add_slash = TRUE;

    newpath = dir_realpath(tmpfs->fs_pool, tmpfs->fs_path);

    if (add_slash)
      newpath = pstrcat(tmpfs->fs_pool, newpath, "/", NULL);

    /* Note that this does cause a slightly larger memory allocation from
     * the pr_fs_t's pool, as the original path value was also allocated
     * from that pool, and that original pointer is being overwritten.
     * However, as this function is only called once, and that pool
     * is freed later, I think this may be acceptable.
     */
    tmpfs->fs_path = newpath;
  }

  /* Resort the map */
  qsort(fs_map->elts, fs_map->nelts, sizeof(pr_fs_t *), fs_cmp);

  return;
}

int init_fs(void) {
  char cwdbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};

  /* Establish the default pr_fs_t that will handle any path */
  root_fs = pr_create_fs(permanent_pool, "system");
  if (root_fs == NULL) {

    /* Do not insert this fs into the FS map.  This will allow other
     * modules to insert filesystems at "/", if they want.
     */
    pr_log_pri(PR_LOG_ERR, "error: unable to initialize default fs");
    exit(1);
  }

  root_fs->fs_path = pstrdup(root_fs->fs_pool, "/");

  /* Set the root FSIO handlers. */
  root_fs->stat = sys_stat;
  root_fs->fstat = sys_fstat;
  root_fs->lstat = sys_lstat;
  root_fs->rename = sys_rename;
  root_fs->unlink = sys_unlink;
  root_fs->open = sys_open;
  root_fs->creat = sys_creat;
  root_fs->close = sys_close;
  root_fs->read = sys_read;
  root_fs->write = sys_write;
  root_fs->lseek = sys_lseek;
  root_fs->link = sys_link;
  root_fs->readlink = sys_readlink;
  root_fs->symlink = sys_symlink;
  root_fs->ftruncate = sys_ftruncate;
  root_fs->truncate = sys_truncate;
  root_fs->chmod = sys_chmod;
  root_fs->chown = sys_chown;
  root_fs->access = sys_access;
  root_fs->faccess = sys_faccess;

  root_fs->chdir = sys_chdir;
  root_fs->chroot = sys_chroot;
  root_fs->opendir = sys_opendir;
  root_fs->closedir = sys_closedir;
  root_fs->readdir = sys_readdir;
  root_fs->mkdir = sys_mkdir;
  root_fs->rmdir = sys_rmdir;

  if (getcwd(cwdbuf, sizeof(cwdbuf)-1)) {
    cwdbuf[sizeof(cwdbuf)-1] = '\0';
    pr_fs_setcwd(cwdbuf);

  } else {
    pr_fsio_chdir("/", FALSE);
    pr_fs_setcwd("/");
  }

  return 0;
}
