/*
 * Copyright (c) 2000, 2001 by Martin C. Shepherd.
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, provided that the above
 * copyright notice(s) and this permission notice appear in all copies of
 * the Software and that both the above copyright notice(s) and this
 * permission notice appear in supporting documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
 * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
 * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder
 * shall not be used in advertising or otherwise to promote the sale, use
 * or other dealings in this Software without prior written authorization
 * of the copyright holder.
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "libtecla.h"
#include "pathutil.h"
#include "homedir.h"
#include "freelist.h"
#include "direader.h"
#include "stringrp.h"

/*
 * The new_PcaPathConf() constructor sets the integer first member of
 * the returned object to the following magic number. This is then
 * checked for by pca_path_completions() as a sanity check.
 */
#define PPC_ID_CODE 4567

/*
 * A pointer to a structure of the following type can be passed to
 * the builtin path-completion callback function to modify its behavior.
 */
struct PcaPathConf {
  int id;          /* This is set to PPC_ID_CODE by new_PcaPathConf() */
  PathCache *pc;   /* The path-list cache in which to look up the executables */
  int escaped;     /* If non-zero, backslashes in the input line are */
                   /*  interpreted as escaping special characters and */
                   /*  spaces, and any special characters and spaces in */
                   /*  the listed completions will also be escaped with */
                   /*  added backslashes. This is the default behaviour. */
                   /* If zero, backslashes are interpreted as being */
                   /*  literal parts of the file name, and none are added */
                   /*  to the completion suffixes. */
  int file_start;  /* The index in the input line of the first character */
                   /*  of the file name. If you specify -1 here, */
                   /*  pca_path_completions() identifies the */
                   /*  the start of the file by looking backwards for */
                   /*  an unescaped space, or the beginning of the line. */
};

/*
 * Prepended to each chached filename is a character which contains
 * one of the following status codes. When a given filename (minus
 * this byte) is passed to the application's check_fn(), the result
 * is recorded in this byte, such that the next time it is looked
 * up, we don't have to call check_fn() again. These codes are cleared
 * whenever the path is scanned and whenever the check_fn() callback
 * is changed.
 */
typedef enum {
  PCA_F_ENIGMA='?', /* The file remains to be checked */
  PCA_F_WANTED='+', /* The file has been selected by the caller's callback */
  PCA_F_IGNORE='-'  /* The file has been rejected by the caller's callback */
} PcaFileStatus;

/*
 * Encapsulate the memory management objects which supply memoy for
 * the arrays of filenames.
 */
typedef struct {
  StringGroup *sg;       /* The memory used to record the names of files */
  size_t files_dim;      /* The allocated size of files[] */
  char **files;          /* Memory for 'files_dim' pointers to files */
  size_t nfiles;         /* The number of filenames currently in files[] */
} CacheMem;

static CacheMem *new_CacheMem(void);
static CacheMem *del_CacheMem(CacheMem *cm);
static void rst_CacheMem(CacheMem *cm);

/*
 * Lists of nodes of the following type are used to record the
 * names and contents of individual directories.
 */
typedef struct PathNode PathNode;
struct PathNode {
  PathNode *next;   /* The next directory in the path */
  int relative;     /* True if the directory is a relative pathname */
  CacheMem *mem;    /* The memory used to store dir[] and files[] */
  char *dir;        /* The directory pathname (stored in pc->sg) */
  int nfile;        /* The number of filenames stored in 'files' */
  char **files;     /* Files of interest in the current directory, */
                    /*  or NULL if dir[] is a relative pathname */
                    /*  who's contents can't be cached. This array */
                    /*  and its contents are taken from pc->abs_mem */
                    /*  or pc->rel_mem */
};

/*
 * Append a new node to the list of directories in the path.
 */
static int add_PathNode(PathCache *pc, const char *dirname);

/*
 * Set the max length of the error-reporting string. There is no point
 * in this being longer than the width of a typical terminal window.
 * In composing error messages, I have assumed that this number is
 * at least 80, so you don't decrease it below this number.
 */
#define ERRLEN 200

/*
 * Set the maximum length allowed for usernames.
 * names.
 */
#define USR_LEN 100

/*
 * PathCache objects encapsulate the resources needed to record
 * files of interest from comma-separated lists of directories.
 */
struct PathCache {
  FreeList *node_mem;    /* A free-list of PathNode objects */
  CacheMem *abs_mem;     /* Memory for the filenames of absolute paths */
  CacheMem *rel_mem;     /* Memory for the filenames of relative paths */
  PathNode *head;        /* The head of the list of directories in the */
                         /*  path, or NULL if no path has been scanned yet. */
  PathNode *tail;        /* The tail of the list of directories in the */
                         /*  path, or NULL if no path has been scanned yet. */
  PathName *path;        /* The fully qualified name of a file */
  HomeDir *home;         /* Home-directory lookup object */
  DirReader *dr;         /* A portable directory reader */
  CplFileConf *cfc;      /* Configuration parameters to pass to */
                         /*  cpl_file_completions() */
  CplCheckFn *check_fn;  /* The callback used to determine if a given */
                         /*  filename should be recorded in the cache. */
  void *data;            /* Annonymous data to be passed to pc->check_fn() */
  char usrnam[USR_LEN+1];/* The buffer used when reading the names of */
                         /*  users. */
  char errmsg[ERRLEN+1]; /* Error-report buffer */
};

/*
 * Empty the cache.
 */
static void pca_clear_cache(PathCache *pc);

/*
 * Read a username from string[] and record it in pc->usrnam[].
 */
static int pca_read_username(PathCache *pc, const char *string, int slen,
			     int literal, const char **nextp);

/*
 * Extract the next component of a colon separated list of directory
 * paths.
 */
static int pca_extract_dir(PathCache *pc, const char *path,
			   const char **nextp);

/*
 * Scan absolute directories for files of interest, recording their names
 * in mem->sg and recording pointers to these names in mem->files[].
 */
static int pca_scan_dir(PathCache *pc, const char *dirname, CacheMem *mem);

/*
 * A qsort() comparison function for comparing the cached filename
 * strings pointed to by two (char **) array elements. Note that
 * this ignores the initial cache-status byte of each filename.
 */
static int pca_cmp_matches(const void *v1, const void *v2);

/*
 * A qsort() comparison function for comparing a filename
 * against an element of an array of pointers to filename cache
 * entries.
 */
static int pca_cmp_file(const void *v1, const void *v2);

/*
 * Initialize a PcaPathConf configuration objects with the default
 * options.
 */
static int pca_init_PcaPathConf(PcaPathConf *ppc, PathCache *pc);

/*
 * Make a copy of a completion suffix, suitable for passing to
 * cpl_add_completion().
 */
static int pca_prepare_suffix(PathCache *pc, const char *suffix,
			      int add_escapes);

/*
 * Return non-zero if the specified string appears to start with a pathname.
 */
static int cpa_cmd_contains_path(const char *prefix, int prefix_len);

/*
 * Return a given prefix with escapes optionally removed.
 */
static const char *pca_prepare_prefix(PathCache *pc, const char *prefix,
				      size_t prefix_len, int escaped);

/*
 * If there is a tilde expression at the beginning of the specified path,
 * place the corresponding home directory into pc->path. Otherwise
 * just clear pc->path.
 */
static int pca_expand_tilde(PathCache *pc, const char *path, int pathlen,
			    int literal, const char **endp);

/*
 * Clear the filename status codes that are recorded before each filename
 * in the cache.
 */
static void pca_remove_marks(PathCache *pc);

/*
 * Specify how many PathNode's to allocate at a time.
 */
#define PATH_NODE_BLK 30

/*
 * Specify the amount by which the files[] arrays are to be extended
 * whenever they are found to be too small.
 */
#define FILES_BLK_FACT 256

/*.......................................................................
 * Create a new object who's function is to maintain a cache of
 * filenames found within a list of directories, and provide quick
 * lookup and completion of selected files in this cache.
 *
 * Output:
 *  return     PathCache *  The new, initially empty cache, or NULL
 *                          on error.
 */
PathCache *new_PathCache(void)
{
  PathCache *pc;  /* The object to be returned */
/*
 * Allocate the container.
 */
  pc = (PathCache *)malloc(sizeof(PathCache));
  if(!pc) {
    fprintf(stderr, "new_PathCache: Insufficient memory.\n");
    return NULL;
  };
/*
 * Before attempting any operation that might fail, initialize the
 * container at least up to the point at which it can safely be passed
 * to del_PathCache().
 */
  pc->node_mem = NULL;
  pc->abs_mem = NULL;
  pc->rel_mem = NULL;
  pc->head = NULL;
  pc->tail = NULL;
  pc->path = NULL;
  pc->home = NULL;
  pc->dr = NULL;
  pc->cfc = NULL;
  pc->check_fn = 0;
  pc->data = NULL;
  pc->usrnam[0] = '\0';
  pc->errmsg[0] = '\0';
/*
 * Allocate the freelist of directory list nodes.
 */
  pc->node_mem = _new_FreeList("new_PathCache", sizeof(PathNode),
			       PATH_NODE_BLK);
  if(!pc->node_mem)
    return del_PathCache(pc);
/*
 * Allocate memory for recording names of files in absolute paths.
 */
  pc->abs_mem = new_CacheMem();
  if(!pc->abs_mem)
    return del_PathCache(pc);
/*
 * Allocate memory for recording names of files in relative paths.
 */
  pc->rel_mem = new_CacheMem();
  if(!pc->rel_mem)
    return del_PathCache(pc);
/*
 * Allocate a pathname buffer.
 */
  pc->path = _new_PathName();
  if(!pc->path)
    return del_PathCache(pc);
/*
 * Allocate an object for looking up home-directories.
 */
  pc->home = _new_HomeDir();
  if(!pc->home)
    return del_PathCache(pc);
/*
 * Allocate an object for reading directories.
 */
  pc->dr = _new_DirReader();
  if(!pc->dr)
    return del_PathCache(pc);
/*
 * Allocate a cpl_file_completions() configuration object.
 */
  pc->cfc = new_CplFileConf();
  if(!pc->cfc)
    return del_PathCache(pc);
/*
 * Configure cpl_file_completions() to use check_fn() to select
 * files of interest.
 */
  cfc_set_check_fn(pc->cfc, pc->check_fn, pc->data);
/*
 * Return the cache, ready for use.
 */
  return pc;
}

/*.......................................................................
 * Delete a given cache of files, returning the resources that it
 * was using to the system.
 *
 * Input:
 *  pc      PathCache *  The cache to be deleted (can be NULL).
 * Output:
 *  return  PathCache *  The deleted object (ie. allways NULL).
 */
PathCache *del_PathCache(PathCache *pc)
{
  if(pc) {
/*
 * Delete the memory of the list of path nodes.
 */
    pc->node_mem = _del_FreeList(NULL, pc->node_mem, 1);
/*
 * Delete the memory used to record filenames.
 */
    pc->abs_mem = del_CacheMem(pc->abs_mem);
    pc->rel_mem = del_CacheMem(pc->rel_mem);
/*
 * The list of PathNode's was already deleted when node_mem was
 * deleted.
 */
    pc->head = NULL;
    pc->tail = NULL;
/*
 * Delete the pathname buffer.
 */
    pc->path = _del_PathName(pc->path);
/*
 * Delete the home-directory lookup object.
 */
    pc->home = _del_HomeDir(pc->home);
/*
 * Delete the directory reader.
 */
    pc->dr = _del_DirReader(pc->dr);
/*
 * Delete the cpl_file_completions() config object.
 */
    pc->cfc = del_CplFileConf(pc->cfc);
/*
 * Delete the container.
 */
    free(pc);
  };
  return NULL;
}

/*.......................................................................
 * If you want subsequent calls to pca_lookup_file() and
 * pca_path_completions() to only return the filenames of certain
 * types of files, for example executables, or filenames ending in
 * ".ps", call this function to register a file-selection callback
 * function. This callback function takes the full pathname of a file,
 * plus application-specific data, and returns 1 if the file is of
 * interest, and zero otherwise.
 *
 * Input:
 *  pc         PathCache *  The filename cache.
 *  check_fn  CplCheckFn *  The function to call to see if the name of
 *                          a given file should be included in the
 *                          cache. This determines what type of files
 *                          will reside in the cache. To revert to
 *                          selecting all files, regardless of type,
 *                          pass 0 here.
 *  data            void *  You can pass a pointer to anything you
 *                          like here, including NULL. It will be
 *                          passed to your check_fn() callback
 *                          function, for its private use.
 */
void pca_set_check_fn(PathCache *pc, CplCheckFn *check_fn, void *data)
{
  if(pc) {
/*
 * If the callback or its data pointer have changed, clear the cached
 * statuses of files that were accepted or rejected by the previous
 * calback.
 */
    if(check_fn != pc->check_fn || data != pc->data)
      pca_remove_marks(pc);
/*
 * Record the new callback locally.
 */
    pc->check_fn = check_fn;
    pc->data = data;
/*
 * Configure cpl_file_completions() to use the same callback to
 * select files of interest.
 */
    cfc_set_check_fn(pc->cfc, check_fn, data);
  };
  return;
}

/*.......................................................................
 * Return a description of the last path-caching error that occurred.
 *
 * Input:
 *  pc     PathCache *   The filename cache that suffered the error.
 * Output:
 *  return      char *   The description of the last error.
 */
const char *pca_last_error(PathCache *pc)
{
  return pc ? pc->errmsg : "NULL PathCache argument";
}

/*.......................................................................
 * Discard all cached filenames.
 *
 * Input:
 *  pc   PathCache *  The cache to be cleared.
 */
static void pca_clear_cache(PathCache *pc)
{
  if(pc) {
/*
 * Return all path-nodes to the freelist.
 */
    _rst_FreeList(pc->node_mem);
    pc->head = pc->tail = NULL;
/*
 * Delete all filename strings.
 */
    rst_CacheMem(pc->abs_mem);
    rst_CacheMem(pc->rel_mem);
  };
  return;
}

/*.......................................................................
 * Build the list of files of interest contained in a given
 * colon-separated list of directories.
 *
 * Input:
 *  pc         PathCache *  The cache in which to store the names of
 *                          the files that are found in the list of
 *                          directories.
 *  path      const char *  A colon-separated list of directory
 *                          paths. Under UNIX, when searching for
 *                          executables, this should be the return
 *                          value of getenv("PATH").
 * Output:
 *  return           int    0 - OK.
 *                          1 - An error occurred. A description of
 *                              the error can be acquired by calling
 *                              pca_last_error(pc).
 */
int pca_scan_path(PathCache *pc, const char *path)
{
  const char *pptr; /* A pointer to the next unprocessed character in path[] */
  PathNode *node;   /* A node in the list of directory paths */
  char **fptr;      /* A pointer into pc->abs_mem->files[] */
/*
 * Check the arguments.
 */
  if(!pc)
    return 1;
/*
 * Clear the outdated contents of the cache.
 */
  pca_clear_cache(pc);
/*
 * If no path list was provided, there is nothing to be added to the
 * cache.
 */
  if(!path)
    return 0;
/*
 * Extract directories from the path list, expanding tilde expressions
 * on the fly into pc->pathname, then add them to the list of path
 * nodes, along with a sorted list of the filenames of interest that
 * the directories hold.
 */
  pptr = path;
  while(*pptr) {
/*
 * Extract the next pathname component into pc->path->name.
 */
    if(pca_extract_dir(pc, pptr, &pptr))
      return 1;
/*
 * Add a new node to the list of paths, containing both the
 * directory name and, if not a relative pathname, the list of
 * files of interest in the directory.
 */
    if(add_PathNode(pc, pc->path->name))
      return 1;
  };
/*
 * The file arrays in each absolute directory node are sections of
 * pc->abs_mem->files[]. Record pointers to the starts of each
 * of these sections in each directory node. Note that this couldn't
 * be done in add_PathNode(), because pc->abs_mem->files[] may
 * get reallocated in subsequent calls to add_PathNode(), thus
 * invalidating any pointers to it.
 */
  fptr = pc->abs_mem->files;
  for(node=pc->head; node; node=node->next) {
    node->files = fptr;
    fptr += node->nfile;
  };
  return 0;
}

/*.......................................................................
 * Extract the next directory path from a colon-separated list of
 * directories, expanding tilde home-directory expressions where needed.
 *
 * Input:
 *  pc      PathCache *   The cache of filenames.
 *  path   const char *   A pointer to the start of the next component
 *                        in the path list.
 * Input/Output:
 *  nextp  const char **  A pointer to the next unprocessed character
 *                        in path[] will be assigned to *nextp.
 * Output:
 *  return        int     0 - OK. The extracted path is in pc->path->name.
 *                        1 - Error. A description of the error will
 *                            have been left in pc->errmsg.
 */
static int pca_extract_dir(PathCache *pc, const char *path, const char **nextp)
{
  const char *pptr;         /* A pointer into path[] */
  const char *sptr;         /* The path following tilde expansion */
  int escaped = 0;          /* True if the last character was a backslash */
/*
 * If there is a tilde expression at the beginning of the specified path,
 * place the corresponding home directory into pc->path. Otherwise
 * just clear pc->path.
 */
  if(pca_expand_tilde(pc, path, strlen(path), 0, &pptr))
    return 1;
/*
 * Keep a record of the current location in the path.
 */
  sptr = pptr;
/*
 * Locate the end of the directory name in the pathname string, stopping
 * when either the end of the string is reached, or an un-escaped colon
 * separator is seen.
 */
  while(*pptr && (escaped || *pptr != ':'))
    escaped = !escaped && *pptr++ == '\\';
/*
 * Append the rest of the directory path to the pathname buffer.
 */
  if(_pn_append_to_path(pc->path, sptr, pptr - sptr, 1) == NULL) {
    strncpy(pc->errmsg, "Insufficient memory to record directory name", sizeof(pc->errmsg));
    return 1;
  };
/*
 * To facilitate subsequently appending filenames to the directory
 * path name, make sure that the recorded directory name ends in a
 * directory separator.
 */
  {
    int dirlen = strlen(pc->path->name);
    if(dirlen < FS_DIR_SEP_LEN ||
       strncmp(pc->path->name + dirlen - FS_DIR_SEP_LEN, FS_DIR_SEP,
	       FS_DIR_SEP_LEN) != 0) {
      if(_pn_append_to_path(pc->path, FS_DIR_SEP, FS_DIR_SEP_LEN, 0) == NULL) {
	strncpy(pc->errmsg, "Insufficient memory to record directory name", sizeof(pc->errmsg));
	return 1;
      };
    };
  };
/*
 * Skip the separator unless we have reached the end of the path.
 */
  if(*pptr==':')
    pptr++;
/*
 * Return the unprocessed tail of the path-list string.
 */
  *nextp = pptr;
  return 0;
}

/*.......................................................................
 * Read a username, stopping when a directory separator is seen, a colon
 * separator is seen, the end of the string is reached, or the username
 * buffer overflows.
 *
 * Input:
 *  pc   PathCache *   The cache of filenames.
 *  string    char *   The string who's prefix contains the name.
 *  slen       int     The max number of characters to read from string[].
 *  literal    int     If true, treat backslashes as literal characters
 *                     instead of escapes.
 * Input/Output:
 *  nextp     char **  A pointer to the next unprocessed character
 *                     in string[] will be assigned to *nextp.
 * Output:
 *  return     int     0 - OK. The username can be found in pc->usrnam.
 *                     1 - Error. A description of the error message
 *                         can be found in pc->errmsg.
 */
static int pca_read_username(PathCache *pc, const char *string, int slen,
			     int literal, const char **nextp)
{
  int usrlen;         /* The number of characters in pc->usrnam[] */
  const char *sptr;   /* A pointer into string[] */
  int escaped = 0;    /* True if the last character was a backslash */
/*
 * Extract the username.
 */
  for(sptr=string,usrlen=0; usrlen < USR_LEN && (sptr-string) < slen; sptr++) {
/*
 * Stop if the end of the string is reached, or a directory separator
 * or un-escaped colon separator is seen.
 */
    if(!*sptr || strncmp(sptr, FS_DIR_SEP, FS_DIR_SEP_LEN)==0 ||
       (!escaped && *sptr == ':'))
      break;
/*
 * Escape the next character?
 */
    if(!literal && !escaped && *sptr == '\\') {
      escaped = 1;
    } else {
      escaped = 0;
      pc->usrnam[usrlen++] = *sptr;
    };
  };
/*
 * Did the username overflow the buffer?
 */
  if(usrlen >= USR_LEN) {
    strncpy(pc->errmsg, "Username too long", sizeof(pc->errmsg));
    return 1;
  };
/*
 * Terminate the string.
 */
  pc->usrnam[usrlen] = '\0';
/*
 * Indicate where processing of the input string should continue.
 */
  *nextp = sptr;
  return 0;
}


/*.......................................................................
 * Create a new CacheMem object.
 *
 * Output:
 *  return  CacheMem *  The new object, or NULL on error.
 */
static CacheMem *new_CacheMem(void)
{
  CacheMem *cm;  /* The object to be returned */
/*
 * Allocate the container.
 */
  cm = (CacheMem *)malloc(sizeof(CacheMem));
  if(!cm) {
    fprintf(stderr, "new_CacheMem: Insufficient memory.\n");
    return NULL;
  };
/*
 * Before attempting any operation that might fail, initialize the
 * container at least up to the point at which it can safely be passed
 * to del_CacheMem().
 */
  cm->sg = NULL;
  cm->files_dim = 0;
  cm->files = NULL;
  cm->nfiles = 0;
/*
 * Allocate a list of string segments for storing filenames.
 */
  cm->sg = _new_StringGroup(_pu_pathname_dim());
  if(!cm->sg)
    return del_CacheMem(cm);
/*
 * Allocate an array of pointers to filenames.
 * This will be extended later if needed.
 */
  cm->files_dim = FILES_BLK_FACT;
  cm->files = (char **) malloc(sizeof(*cm->files) * cm->files_dim);
  if(!cm->files) {
    fprintf(stderr,
        "new_CacheMem: Insufficient memory to allocate array of files.\n");
    return del_CacheMem(cm);
  };
  return cm;
}

/*.......................................................................
 * Delete a CacheMem object.
 *
 * Input:
 *  cm   CacheMem *  The object to be deleted.
 * Output:
 *  return CacheMem *  The deleted object (always NULL).
 */
static CacheMem *del_CacheMem(CacheMem *cm)
{
  if(cm) {
/*
 * Delete the memory that was used to record filename strings.
 */
    cm->sg = _del_StringGroup(cm->sg);
/*
 * Delete the array of pointers to filenames.
 */
    cm->files_dim = 0;
    if(cm->files) {
      free(cm->files);
      cm->files = NULL;
    };
/*
 * Delete the container.
 */
    free(cm);
  };
  return NULL;
}

/*.......................................................................
 * Re-initialize the memory used to allocate filename strings.
 *
 * Input:
 *  cm     CacheMem *  The memory cache to be cleared.
 */
static void rst_CacheMem(CacheMem *cm)
{
  _clr_StringGroup(cm->sg);
  cm->nfiles = 0;
  return;
}

/*.......................................................................
 * Append a new directory node to the list of directories read from the
 * path.
 *
 * Input:
 *  pc        PathCache *  The filename cache.
 *  dirname  const char *  The name of the new directory.
 * Output:
 *  return          int    0 - OK.
 *                         1 - Error.
 */
static int add_PathNode(PathCache *pc, const char *dirname)
{
  PathNode *node;  /* The new directory list node */
  int relative;    /* True if dirname[] is a relative pathname */
/*
 * Have we been passed a relative pathname or an absolute pathname?
 */
  relative = strncmp(dirname, FS_ROOT_DIR, FS_ROOT_DIR_LEN) != 0;
/*
 * If it's an absolute pathname, ignore it if the corresponding
 * directory doesn't exist.
 */
  if(!relative && !_pu_path_is_dir(dirname))
    return 0;
/*
 * Allocate a new list node to record the specifics of the new directory.
 */
  node = (PathNode *) _new_FreeListNode(pc->node_mem);
  if(!node) {
    snprintf(pc->errmsg, sizeof(pc->errmsg),
	     "Insufficient memory to cache new directory.");
    return 1;
  };
/*
 * Initialize the node.
 */
  node->next = NULL;
  node->relative = relative;
  node->mem = relative ? pc->rel_mem : pc->abs_mem;
  node->dir = NULL;
  node->nfile = 0;
  node->files = NULL;
/*
 * Make a copy of the directory pathname.
 */
  node->dir = _sg_store_string(pc->abs_mem->sg, dirname, 0);
  if(!node->dir) {
    strncpy(pc->errmsg, "Insufficient memory to store directory name.", sizeof(pc->errmsg));
    return 1;
  };
/*
 * Scan absolute directories for files of interest, recording their names
 * in node->mem->sg and appending pointers to these names to the
 * node->mem->files[] array.
 */
  if(!node->relative) {
    int nfile = node->nfile = pca_scan_dir(pc, node->dir, node->mem);
    if(nfile < 1) {  /* No files matched or an error occurred */
      node = (PathNode *) _del_FreeListNode(pc->node_mem, node);
      return nfile < 0;
    };
  };
/*
 * Append the new node to the list.
 */
  if(pc->head) {
    pc->tail->next = node;
    pc->tail = node;
  } else {
    pc->head = pc->tail = node;
  };
  return 0;
}

/*.......................................................................
 * Scan a given directory for files of interest, record their names
 * in mem->sg and append pointers to them to the mem->files[] array.
 *
 * Input:
 *  pc        PathCache *  The filename cache.
 *  dirname  const char *  The pathname of the directory to be scanned.
 *  mem        CacheMem *  The memory in which to store filenames of
 *                         interest.
 * Output:
 *  return          int    The number of files recorded, or -1 if a
 *                         memory error occurs. Note that the
 *                         inability to read the contents of the
 *                         directory is not counted as an error.
 */
static int pca_scan_dir(PathCache *pc, const char *dirname, CacheMem *mem)
{
  int nfile = 0;        /* The number of filenames recorded */
  const char *filename; /* The name of the file being looked at */
/*
 * Attempt to open the directory. If the directory can't be read then
 * there are no accessible files of interest in the directory.
 */
  if(_dr_open_dir(pc->dr, dirname, NULL))
    return 0;
/*
 * Record the names of all files in the directory in the cache.
 */
  while((filename = _dr_next_file(pc->dr))) {
    char *copy;        /* A copy of the filename */
/*
 * Make a temporary copy of the filename with an extra byte prepended.
 */
    _pn_clear_path(pc->path);
    if(_pn_append_to_path(pc->path, " ", 1, 0) == NULL ||
       _pn_append_to_path(pc->path, filename, -1, 1) == NULL) {
      strncpy(pc->errmsg, "Insufficient memory to record filename", sizeof(pc->errmsg));
      return -1;
    };
/*
 * Store the filename.
 */
    copy = _sg_store_string(mem->sg, pc->path->name, 0);
    if(!copy) {
      strncpy(pc->errmsg, "Insufficient memory to cache file name.", sizeof(pc->errmsg));
      return -1;
    };
/*
 * Mark the filename as unchecked.
 */
    copy[0] = PCA_F_ENIGMA;
/*
 * Make room to store a pointer to the copy in mem->files[].
 */
    if(mem->nfiles + 1 > mem->files_dim) {
      int needed = mem->files_dim + FILES_BLK_FACT;
      char **files = (char **) realloc(mem->files, sizeof(*mem->files)*needed);
      if(!files) {
	strncpy(pc->errmsg, "Insufficient memory to extend filename cache.", sizeof(pc->errmsg));
	return 1;
      };
      mem->files = files;
      mem->files_dim = needed;
    };
/*
 * Record a pointer to the copy of the filename at the end of the files[]
 * array.
 */
    mem->files[mem->nfiles++] = copy;
/*
 * Keep a record of the number of files matched so far.
 */
    nfile++;
  };
/*
 * Sort the list of files into lexical order.
 */
  qsort(mem->files + mem->nfiles - nfile, nfile, sizeof(*mem->files),
	pca_cmp_matches);
/*
 * Return the number of files recorded in mem->files[].
 */
  return nfile;
}

/*.......................................................................
 * A qsort() comparison function for comparing the cached filename
 * strings pointed to by two (char **) array elements. Note that
 * this ignores the initial cache-status byte of each filename.
 *
 * Input:
 *  v1, v2   void *  Pointers to the pointers of two strings to be compared.
 * Output:
 *  return    int    -1 -> v1 < v2.
 *                    0 -> v1 == v2
 *                    1 -> v1 > v2
 */
static int pca_cmp_matches(const void *v1, const void *v2)
{
  const char **s1 = (const char **) v1;
  const char **s2 = (const char **) v2;
  return strcmp(*s1+1, *s2+1);
}

/*.......................................................................
 * Given the simple name of a file, search the cached list of files
 * in the order in which they where found in the list of directories
 * previously presented to pca_scan_path(), and return the pathname
 * of the first file which has this name. If a pathname to a file is
 * given instead of a simple filename, this is returned without being
 * looked up in the cache, but with any initial ~username expression
 * expanded, and optionally, unescaped backslashes removed.
 *
 * Input:
 *  pc     PathCache *  The cached list of files.
 *  name  const char *  The name of the file to lookup.
 *  name_len     int    The length of the filename string at the
 *                      beginning of name[], or -1 to indicate that
 *                      the filename occupies the whole of the
 *                      string.
 *  literal      int    If this argument is zero, lone backslashes
 *                      in name[] are ignored during comparison
 *                      with filenames in the cache, under the
 *                      assumption that they were in the input line
 *                      soley to escape the special significance of
 *                      characters like spaces. To have them treated
 *                      as normal characters, give this argument a
 *                      non-zero value, such as 1.
 * Output:
 *  return      char *  The pathname of the first matching file,
 *                      or NULL if not found. Note that the returned
 *                      pointer points to memory owned by *pc, and
 *                      will become invalid on the next call to any
 *                      function in the PathCache module.
 */
char *pca_lookup_file(PathCache *pc, const char *name, int name_len,
		      int literal)
{
  PathNode *node;   /* A node in the list of directories in the path */
  char **match;     /* A pointer to a matching filename string in the cache */
/*
 * Check the arguments.
 */
  if(!pc || !name || name_len==0)
    return NULL;
/*
 * If no length was specified, determine the length of the string to
 * be looked up.
 */
  if(name_len < 0)
    name_len = strlen(name);
/*
 * If the word starts with a ~username expression, the root directory,
 * of it contains any directory separators, then treat it isn't a simple
 * filename that can be looked up in the cache, but rather appears to
 * be the pathname of a file. If so, return a copy of this pathname with
 * escapes removed, if requested, and any initial ~username expression
 * expanded.
 */
  if(cpa_cmd_contains_path(name, name_len)) {
    const char *nptr;
    if(pca_expand_tilde(pc, name, name_len, literal, &nptr) || 
       _pn_append_to_path(pc->path, nptr, name_len - (nptr-name),
			  !literal) == NULL)
      return NULL;
    return pc->path->name;
  };
/*
 * Look up the specified filename in each of the directories of the path,
 * in the same order that they were listed in the path, and stop as soon
 * as an instance of the file is found.
 */
  for(node=pc->head; node; node=node->next) {
/*
 * If the directory of the latest node is a relative pathname,
 * scan it for files of interest.
 */
    if(node->relative) {
      rst_CacheMem(node->mem);
      if(pca_scan_dir(pc, node->dir, node->mem) < 1)
	continue;
      node->files = node->mem->files;
      node->nfile = node->mem->nfiles;
    };
/*
 * Copy the filename into a temporary buffer, while interpretting
 * escape characters if needed.
 */
    _pn_clear_path(pc->path);
    if(_pn_append_to_path(pc->path, name, name_len, !literal) == NULL)
      return NULL;
/*
 * Perform a binary search for the requested filename.
 */
    match = (char **)bsearch(pc->path->name, node->files, node->nfile,
		             sizeof(*node->files), pca_cmp_file);
    if(match) {
/*
 * Prepend the pathname in which the directory was found, which we have
 * guaranteed to end in a directory separator, to the located filename.
 */
      if(_pn_prepend_to_path(pc->path, node->dir, -1, 0) == NULL)
	return NULL;
/*
 * Return the matching pathname unless it is rejected by the application.
 */
      if(!pc->check_fn || (*match)[0] == PCA_F_WANTED ||
	 ((*match)[0]==PCA_F_ENIGMA && pc->check_fn(pc->data, pc->path->name))){
	(*match)[0] = PCA_F_WANTED;
	return pc->path->name;
      } else {
	*(match)[0] = PCA_F_IGNORE;
      };
    };
  };
/*
 * File not found.
 */
  return NULL;
}

/*.......................................................................
 * A qsort() comparison function for comparing a filename string to
 * a cached filename string pointed to by a (char **) array element.
 * This ignores the initial code byte at the start of the cached filename
 * string.
 *
 * Input:
 *  v1, v2   void *  Pointers to the pointers of two strings to be compared.
 * Output:
 *  return    int    -1 -> v1 < v2.
 *                    0 -> v1 == v2
 *                    1 -> v1 > v2
 */
static int pca_cmp_file(const void *v1, const void *v2)
{
  const char *file_name = (const char *) v1;
  const char **cache_name = (const char **) v2;
  return strcmp(file_name, *cache_name + 1);
}

/*.......................................................................
 * The PcaPathConf structure may have options added to it in the future.
 * To allow your application to be linked against a shared version of the
 * tecla library, without these additions causing your application to
 * crash, you should use new_PcaPathConf() to allocate such structures.
 * This will set all of the configuration options to their default values,
 * which you can then change before passing the structure to
 * pca_path_completions().
 *
 * Input:
 *  pc         PathCache *  The filename cache in which to look for
 *                          file name completions.
 * Output:
 *  return   PcaPathConf *  The new configuration structure, or NULL
 *                          on error. A descripition of the error
 *                          can be found by calling pca_last_error(pc).
 */
PcaPathConf *new_PcaPathConf(PathCache *pc)
{
  PcaPathConf *ppc;  /* The object to be returned */
/*
 * Check the arguments.
 */
  if(!pc)
    return NULL;
/*
 * Allocate the container.
 */
  ppc = (PcaPathConf *)malloc(sizeof(PcaPathConf));
  if(!ppc) {
    strncpy(pc->errmsg, "Insufficient memory.", sizeof(pc->errmsg));
    return NULL;
  };
/*
 * Before attempting any operation that might fail, initialize the
 * container at least up to the point at which it can safely be passed
 * to del_PcaPathConf().
 */
  if(pca_init_PcaPathConf(ppc, pc))
    return del_PcaPathConf(ppc);
  return ppc;
}

/*.......................................................................
 * Initialize a PcaPathConf configuration structure with defaults.
 *
 * Input:
 *  ppc   PcaPathConf *  The structre to be initialized.
 *  pc      PathCache *  The cache in which completions will be looked up.
 * Output:
 *  return        int    0 - OK.
 *                       1 - Error. A description of the error can be
 *                           obtained by calling pca_last_error(pc).
 */
static int pca_init_PcaPathConf(PcaPathConf *ppc, PathCache *pc)
{
/*
 * Check the arguments.
 */
  if(!pc)
    return 1;
/*
 * Set the default options.
 */
  ppc->id = PPC_ID_CODE;
  ppc->pc = pc;
  ppc->escaped = 1;
  ppc->file_start = -1;
  return 0;
}

/*.......................................................................
 * Delete a PcaPathConf object.
 *
 * Input:
 *  ppc    PcaPathConf *  The object to be deleted.
 * Output:
 *  return PcaPathConf *  The deleted object (always NULL).
 */
PcaPathConf *del_PcaPathConf(PcaPathConf *ppc)
{
  if(ppc) {
    ppc->pc = NULL;  /* It is up to the caller to delete the cache */
/*
 * Delete the container.
 */
    free(ppc);
  };
  return NULL;
}

/*.......................................................................
 * pca_path_completions() is a completion callback function for use
 * directly with cpl_complete_word() or gl_customize_completions(), or
 * indirectly from your own completion callback function. It requires
 * that a CpaPathArgs object be passed via its 'void *data' argument.
 */
CPL_MATCH_FN(pca_path_completions)
{
  PcaPathConf *ppc;       /* The configuration arguments */
  PathCache *pc;          /* The cache in which to look for completions */
  PathNode *node;         /* A node in the list of directories in the path */
  const char *filename;   /* The name of the file being looked at */
  const char *start_path; /* The pointer to the start of the pathname */
                          /*  in line[]. */
  int word_start;         /* The index in line[] corresponding to start_path */
  const char *prefix;     /* The file-name prefix being searched for */
  size_t prefix_len;      /* The length of the prefix being completed */
  int bot;                /* The lowest index of the array not searched yet */
  int top;                /* The highest index of the array not searched yet */
/*
 * Check the arguments.
 */
  if(!cpl)
    return 1;
  if(!line || word_end < 0 || !data) {
    cpl_record_error(cpl, "pca_path_completions: Invalid arguments.");
    return 1;
  };
/*
 * Get the configuration arguments.
 */
  ppc = (PcaPathConf *) data;
/*
 * Check that the callback data is a PcaPathConf structure returned
 * by new_PcaPathConf().
 */
  if(ppc->id != PPC_ID_CODE) {
    cpl_record_error(cpl,
		     "Invalid callback data passed to pca_path_completions()");
    return 1;
  };
/*
 * Get the filename cache.
 */
  pc = ppc->pc;
/*
 * Get the start of the file name. If not specified by the caller,
 * identify it by searching backwards in the input line for an
 * unescaped space or the start of the line.
 */
  if(ppc->file_start < 0) {
    start_path = _pu_start_of_path(line, word_end);
    if(!start_path) {
      cpl_record_error(cpl, "Unable to find the start of the file name.");
      return 1;
    };
  } else {
    start_path = line + ppc->file_start;
  };
/*
 * Get the index of the start of the word being completed.
 */
  word_start = start_path - line;
/*
 * Work out the length of the prefix that is bein completed.
 */
  prefix_len = word_end - word_start;
/*
 * If the word starts with a ~username expression or the root directory,
 * of it contains any directory separators, then completion must be
 * delegated to cpl_file_completions().
 */
  if(cpa_cmd_contains_path(start_path, prefix_len)) {
    cfc_file_start(pc->cfc, word_start);
    return cpl_file_completions(cpl, pc->cfc, line, word_end);
  };
/*
 * Look up the specified file name in each of the directories of the path,
 * in the same order that they were listed in the path, and stop as soon
 * as an instance of the file is found.
 */
  for(node=pc->head; node; node=node->next) {
/*
 * If the directory of the latest node is a relative pathname,
 * scan it for files of interest.
 */
    if(node->relative) {
      rst_CacheMem(node->mem);
      if(pca_scan_dir(pc, node->dir, node->mem) < 1)
	continue;
      node->files = node->mem->files;
      node->nfile = node->mem->nfiles;
    };
/*
 * If needed, make a copy of the file-name being matched, with
 * escapes removed. Note that we need to do this anew every loop
 * iteration, because the above call to pca_scan_dir() uses
 * pc->path.
 */
    prefix = pca_prepare_prefix(pc, start_path, prefix_len, ppc->escaped);
    if(!prefix)
      return 1;
/*
 * The directory entries are sorted, so we can perform a binary
 * search for an instance of the prefix being searched for.
 */
    bot = 0;
    top = node->nfile - 1;
    while(top >= bot) {
      int mid = (top + bot)/2;
      int test = strncmp(node->files[mid]+1, prefix, prefix_len);
      if(test > 0)
	top = mid - 1;
      else if(test < 0)
	bot = mid + 1;
      else {
	top = bot = mid;
	break;
      };
    };
/*
 * If we found a match, look to see if any of its neigbors also match.
 */
    if(top == bot) {
      while(--bot >= 0 && strncmp(node->files[bot]+1, prefix, prefix_len) == 0)
	;
      while(++top < node->nfile &&
	    strncmp(node->files[top]+1, prefix, prefix_len) == 0)
	;
/*
 * We will have gone one too far in each direction.
 */
      bot++;
      top--;
/*
 * Add the completions to the list after checking them against the
 * callers requirements.
 */
      for( ; bot<=top; bot++) {
	char *match = node->files[bot];
/*
 * Form the full pathname of the file.
 */
	_pn_clear_path(pc->path);
	if(_pn_append_to_path(pc->path, node->dir, -1, 0) == NULL ||
	   _pn_append_to_path(pc->path, match+1, -1, 0) == NULL) {
	  strncpy(pc->errmsg, "Insufficient memory to complete file name", sizeof(pc->errmsg));
	  return 1;
	};
/*
 * Should the file be included in the list of completions?
 */
	if(!pc->check_fn || match[0] == PCA_F_WANTED ||
	   (match[0]==PCA_F_ENIGMA && pc->check_fn(pc->data, pc->path->name))) {
	  match[0] = PCA_F_WANTED;
/*
 * Copy the completion suffix into the work pathname pc->path->name,
 * adding backslash escapes if needed.
 */
	  if(pca_prepare_suffix(pc, match + 1 + prefix_len,
				ppc->escaped))
	    return 1;
/*
 * Record the completion.
 */
	  if(cpl_add_completion(cpl, line, word_start, word_end, pc->path->name,
				"", " "))
	    return 1;
/*
 * The file was rejected by the application.
 */
	} else {
	  match[0] = PCA_F_IGNORE;
	};
      };
    };
  };
/*
 * We now need to search for subdirectories of the current directory which
 * have matching prefixes. First, if needed, make a copy of the word being
 * matched, with escapes removed.
 */
  prefix = pca_prepare_prefix(pc, start_path, prefix_len, ppc->escaped);
  if(!prefix)
    return 1;
/*
 * Now open the current directory.
 */
  if(_dr_open_dir(pc->dr, FS_PWD, NULL))
    return 0;
/*
 * Scan the current directory for sub-directories whos names start with
 * the prefix that we are completing.
 */
  while((filename = _dr_next_file(pc->dr))) {
/*
 * Does the latest filename match the prefix, and is it a directory?
 */
    if(strncmp(filename, prefix, prefix_len) == 0 && _pu_path_is_dir(filename)){
/*
 * Record the completion.
 */
      if(pca_prepare_suffix(pc, filename + prefix_len, ppc->escaped) ||
	 cpl_add_completion(cpl, line, word_start, word_end, pc->path->name,
			    FS_DIR_SEP, FS_DIR_SEP))
	return 1;
/*
 * The prefix in pc->path->name will have been overwritten by
 * pca_prepare_suffix(). Restore it here.
 */
      prefix = pca_prepare_prefix(pc, start_path, prefix_len, ppc->escaped);
      if(!prefix)
	return 1;
    };
  };
  _dr_close_dir(pc->dr);
  return 0;
}

/*.......................................................................
 * Using the work buffer pc->path, make a suitably escaped copy of a
 * given completion suffix, ready to be passed to cpl_add_completion().
 *
 * Input:
 *  pc      PathCache *  The filename cache resource object.
 *  suffix       char *  The suffix to be copied.
 *  add_escapes   int    If true, escape special characters.
 * Output:
 *  return        int    0 - OK.
 *                       1 - Error.
 */
static int pca_prepare_suffix(PathCache *pc, const char *suffix,
			      int add_escapes)
{
  const char *sptr; /* A pointer into suffix[] */
  int nbsl;         /* The number of backslashes to add to the suffix */
  int i;
/*
 * How long is the suffix?
 */
  int suffix_len = strlen(suffix);
/*
 * Clear the work buffer.
 */
  _pn_clear_path(pc->path);
/*
 * Count the number of backslashes that will have to be added to
 * escape spaces, tabs, backslashes and wildcard characters.
 */
  nbsl = 0;
  if(add_escapes) {
    for(sptr = suffix; *sptr; sptr++) {
      switch(*sptr) {
      case ' ': case '\t': case '\\': case '*': case '?': case '[':
	nbsl++;
	break;
      };
    };
  };
/*
 * Arrange for the output path buffer to have sufficient room for the
 * both the suffix and any backslashes that have to be inserted.
 */
  if(_pn_resize_path(pc->path, suffix_len + nbsl) == NULL) {
    strncpy(pc->errmsg, "Insufficient memory to complete file name", sizeof(pc->errmsg));
    return 1;
  };
/*
 * If the suffix doesn't need any escapes, copy it directly into the
 * work buffer.
 */
  if(nbsl==0) {
    strncpy(pc->path->name, suffix, pc->path->dim);
  } else {
/*
 * Make a copy with special characters escaped?
 */
    if(nbsl > 0) {
      const char *src = suffix;
      char *dst = pc->path->name;
      for(i=0; i<suffix_len; i++) {
	switch(*src) {
	case ' ': case '\t': case '\\': case '*': case '?': case '[':
	  *dst++ = '\\';
	};
	*dst++ = *src++;
      };
      *dst = '\0';
    };
  };
  return 0;
}

/*.......................................................................
 * Return non-zero if the specified string appears to start with a pathname.
 *
 * Input:
 *  prefix  const char *  The filename prefix to check.
 *  prefix_len     int    The length of the prefix.
 * Output:
 *  return         int    0 - Doesn't start with a path name.
 *                        1 - Does start with a path name.
 */
static int cpa_cmd_contains_path(const char *prefix, int prefix_len)
{
  int i;
/*
 * If the filename starts with a ~, then this implies a ~username
 * expression, which constitutes a pathname.
 */
  if(*prefix == '~')
    return 1;
/*
 * If the filename starts with the root directory, then it obviously
 * starts with a pathname.
 */
  if(prefix_len >= FS_ROOT_DIR_LEN && 
     strncmp(prefix, FS_ROOT_DIR, FS_ROOT_DIR_LEN) == 0)
    return 1;
/*
 * Search the prefix for directory separators, returning as soon as
 * any are found, since their presence indicates that the filename
 * starts with a pathname specification (valid or otherwise).
 */
  for(i=0; i<prefix_len; i++) {
    if(prefix_len - i >= FS_DIR_SEP_LEN &&
       strncmp(prefix + i, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0)
      return 1;
  };
/*
 * The file name doesn't appear to start with a pathname specification.
 */
  return 0;
}

/*.......................................................................
 * If needed make a new copy of the prefix being matched, in pc->path->name,
 * but with escapes removed. If no escapes are to be removed, simply return
 * the original prefix string.
 *
 * Input:
 *  pc      PathCache *   The cache being searched.
 *  prefix const char *   The prefix to be processed.
 *  prefix_len size_t     The length of the prefix.
 *  escaped       int     If true, return a copy with escapes removed.
 * Output:
 *  return const char *   The prepared prefix, or NULL on error, in
 *                        which case an error message will have been
 *                        left in pc->errmsg.
 */
static const char *pca_prepare_prefix(PathCache *pc, const char *prefix,
				      size_t prefix_len, int escaped)
{
/*
 * Make a copy with escapes removed?
 */
  if(escaped) {
    _pn_clear_path(pc->path);
    if(_pn_append_to_path(pc->path, prefix, prefix_len, 1) == NULL) {
      strncpy(pc->errmsg, "Insufficient memory to complete filename", sizeof(pc->errmsg));
      return NULL;
    };
    return pc->path->name;
  };
  return prefix;
}

/*.......................................................................
 * If backslashes in the filename should be treated as literal
 * characters, call the following function with literal=1. Otherwise
 * the default is to treat them as escape characters, used for escaping
 * spaces etc..
 *
 * Input:
 *  ppc    PcaPathConf *  The pca_path_completions() configuration object
 *                        to be configured.
 *  literal        int    Pass non-zero here to enable literal interpretation
 *                        of backslashes. Pass 0 to turn off literal
 *                        interpretation.
 */
void ppc_literal_escapes(PcaPathConf *ppc, int literal)
{
  if(ppc)
    ppc->escaped = !literal;
}

/*.......................................................................
 * Call this function if you know where the index at which the
 * filename prefix starts in the input line. Otherwise by default,
 * or if you specify start_index to be -1, the filename is taken
 * to start after the first unescaped space preceding the cursor,
 * or the start of the line, which ever comes first.
 *
 * Input:
 *  ppc    PcaPathConf *  The pca_path_completions() configuration object
 *                        to be configured.
 *  start_index    int    The index of the start of the filename in
 *                        the input line, or -1 to select the default.
 */
void ppc_file_start(PcaPathConf *ppc, int start_index)
{
  if(ppc)
    ppc->file_start = start_index;
}

/*.......................................................................
 * Expand any ~user expression found at the start of a path, leaving
 * either an empty string in pc->path if there is no ~user expression,
 * or the corresponding home directory.
 *
 * Input:
 *  pc     PathCache *  The filename cache.
 *  path  const char *  The path to expand.
 *  pathlen      int    The max number of characters to look at in path[].
 *  literal      int    If true, treat backslashes as literal characters
 *                      instead of escapes.
 * Input/Output:
 *  endp  const char *  A pointer to the next unprocessed character in
 *                      path[] will be assigned to *endp.
 * Output:
 *  return       int    0 - OK
 *                      1 - Error (a description will have been placed
 *                                 in pc->errmsg[]).
 */
static int pca_expand_tilde(PathCache *pc, const char *path, int pathlen,
			    int literal, const char **endp)
{
  const char *pptr = path;  /* A pointer into path[] */
  const char *homedir=NULL; /* A home directory */
/*
 * Clear the pathname buffer.
 */
  _pn_clear_path(pc->path);
/*
 * If the first character is a tilde, then perform home-directory
 * interpolation.
 */
  if(*pptr == '~') {
/*
 * Skip the tilde character and attempt to read the username that follows
 * it, into pc->usrnam[].
 */
    if(pca_read_username(pc, ++pptr, pathlen-1, literal, &pptr))
      return 1;
/*
 * Attempt to lookup the home directory of the user.
 */
    homedir = _hd_lookup_home_dir(pc->home, pc->usrnam);
    if(!homedir) {
      strncpy(pc->errmsg, _hd_last_home_dir_error(pc->home), ERRLEN);
      pc->errmsg[ERRLEN] = '\0';
      return 1;
    };
/*
 * Append the home directory to the pathname string.
 */
    if(_pn_append_to_path(pc->path, homedir, -1, 0) == NULL) {
      strncpy(pc->errmsg, "Insufficient memory for home directory expansion", sizeof(pc->errmsg));
      return 1;
    };
  };
/*
 * ~user and ~ are usually followed by a directory separator to
 * separate them from the file contained in the home directory.
 * If the home directory is the root directory, then we don't want
 * to follow the home directory by a directory separator, so we should
 * skip over it so that it doesn't get copied into the output pathname
 */
  if(homedir && strcmp(homedir, FS_ROOT_DIR) == 0 &&
     (pptr-path) + FS_DIR_SEP_LEN < pathlen &&
     strncmp(pptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) {
    pptr += FS_DIR_SEP_LEN;
  };
/*
 * Return a pointer to the next unprocessed character.
 */
  *endp = pptr;
  return 0;
}

/*.......................................................................
 * Clear the filename status codes that are recorded before each filename
 * in the cache.
 *
 * Input:
 *  pc     PathCache *  The filename cache.
 */
static void pca_remove_marks(PathCache *pc)
{
  PathNode *node;         /* A node in the list of directories in the path */
  int i;
/*
 * Traverse the absolute directories of the path, clearing the
 * filename status marks that precede each filename.
 */
  for(node=pc->head; node; node=node->next) {
    if(!node->relative) {
      for(i=0; i<node->nfile; i++)
	*node->files[i] = PCA_F_ENIGMA;
    };
  };
  return;
}
