summaryrefslogtreecommitdiffstats
path: root/libtecla/pcache.c
diff options
context:
space:
mode:
Diffstat (limited to 'libtecla/pcache.c')
-rw-r--r--libtecla/pcache.c1710
1 files changed, 1710 insertions, 0 deletions
diff --git a/libtecla/pcache.c b/libtecla/pcache.c
new file mode 100644
index 0000000..5a489b3
--- /dev/null
+++ b/libtecla/pcache.c
@@ -0,0 +1,1710 @@
+/*
+ * Copyright (c) 2000, 2001, 2002, 2003, 2004, 2012 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.
+ */
+
+/*
+ * If file-system access is to be excluded, this module has no function,
+ * so all of its code should be excluded.
+ */
+#ifndef WITHOUT_FILE_SYSTEM
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "libtecla.h"
+#include "pathutil.h"
+#include "homedir.h"
+#include "freelist.h"
+#include "direader.h"
+#include "stringrp.h"
+#include "errmsg.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 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 {
+ ErrMsg *err; /* The error reporting buffer */
+ 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. */
+};
+
+/*
+ * 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) {
+ errno = ENOMEM;
+ 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->err = NULL;
+ 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';
+/*
+ * Allocate a place to record error messages.
+ */
+ pc->err = _new_ErrMsg();
+ if(!pc->err)
+ return del_PathCache(pc);
+/*
+ * Allocate the freelist of directory list nodes.
+ */
+ pc->node_mem = _new_FreeList(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 error message buffer.
+ */
+ pc->err = _del_ErrMsg(pc->err);
+/*
+ * Delete the memory of the list of path nodes.
+ */
+ pc->node_mem = _del_FreeList(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 ? _err_get_msg(pc->err) : "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->err.
+ */
+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) {
+ _err_record_msg(pc->err, "Insufficient memory to record directory name",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to record directory name",
+ END_ERR_MSG);
+ 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->err.
+ */
+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) {
+ _err_record_msg(pc->err, "Username too long", END_ERR_MSG);
+ 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) {
+ errno = ENOMEM;
+ 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) {
+ errno = ENOMEM;
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to cache new directory.",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to store directory name.",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to record filename",
+ END_ERR_MSG);
+ return -1;
+ };
+/*
+ * Store the filename.
+ */
+ copy = _sg_store_string(mem->sg, pc->path->name, 0);
+ if(!copy) {
+ _err_record_msg(pc->err, "Insufficient memory to cache file name.",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err,
+ "Insufficient memory to extend filename cache.",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory.", END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to complete file name",
+ END_ERR_MSG);
+ 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) {
+ _err_record_msg(pc->err, "Insufficient memory to complete file name",
+ END_ERR_MSG);
+ return 1;
+ };
+/*
+ * If the suffix doesn't need any escapes, copy it directly into the
+ * work buffer.
+ */
+ if(nbsl==0) {
+ strcpy(pc->path->name, suffix);
+ } 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->err.
+ */
+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) {
+ _err_record_msg(pc->err, "Insufficient memory to complete filename",
+ END_ERR_MSG);
+ 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->err).
+ */
+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) {
+ _err_record_msg(pc->err, _hd_last_home_dir_error(pc->home), END_ERR_MSG);
+ return 1;
+ };
+/*
+ * Append the home directory to the pathname string.
+ */
+ if(_pn_append_to_path(pc->path, homedir, -1, 0) == NULL) {
+ _err_record_msg(pc->err,
+ "Insufficient memory for home directory expansion",
+ END_ERR_MSG);
+ 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;
+}
+
+#endif /* ifndef WITHOUT_FILE_SYSTEM */