/* * 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 /* * Standard includes. */ #include #include #include #include #include #include /* * Local includes. */ #include "libtecla.h" #include "direader.h" #include "homedir.h" #include "pathutil.h" #include "cplfile.h" #include "errmsg.h" /* * Set the maximum length allowed for usernames. * names. */ #define USR_LEN 100 /* * Set the maximum length allowed for environment variable names. */ #define ENV_LEN 100 /* * The resources needed to complete a filename are maintained in objects * of the following type. */ struct CompleteFile { ErrMsg *err; /* The error reporting buffer */ DirReader *dr; /* A directory reader */ HomeDir *home; /* A home directory expander */ PathName *path; /* The buffer in which to accumulate the path */ PathName *buff; /* A pathname work buffer */ char usrnam[USR_LEN+1]; /* The buffer used when reading the names of */ /* users. */ char envnam[ENV_LEN+1]; /* The buffer used when reading the names of */ /* environment variables. */ }; static int cf_expand_home_dir(CompleteFile *cf, const char *user); static int cf_complete_username(CompleteFile *cf, WordCompletion *cpl, const char *prefix, const char *line, int word_start, int word_end, int escaped); static HOME_DIR_FN(cf_homedir_callback); static int cf_complete_entry(CompleteFile *cf, WordCompletion *cpl, const char *line, int word_start, int word_end, int escaped, CplCheckFn *check_fn, void *check_data); static char *cf_read_name(CompleteFile *cf, const char *type, const char *string, int slen, char *nambuf, int nammax); static int cf_prepare_suffix(CompleteFile *cf, const char *suffix, int add_escapes); /* * A stack based object of the following type is used to pass data to the * cf_homedir_callback() function. */ typedef struct { CompleteFile *cf; /* The file-completion resource object */ WordCompletion *cpl; /* The string-completion rsource object */ size_t prefix_len; /* The length of the prefix being completed */ const char *line; /* The line from which the prefix was extracted */ int word_start; /* The index in line[] of the start of the username */ int word_end; /* The index in line[] following the end of the prefix */ int escaped; /* If true, add escapes to the completion suffixes */ } CfHomeArgs; /*....................................................................... * Create a new file-completion object. * * Output: * return CompleteFile * The new object, or NULL on error. */ CompleteFile *_new_CompleteFile(void) { CompleteFile *cf; /* The object to be returned */ /* * Allocate the container. */ cf = (CompleteFile *) malloc(sizeof(CompleteFile)); if(!cf) { 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_CompleteFile(). */ cf->err = NULL; cf->dr = NULL; cf->home = NULL; cf->path = NULL; cf->buff = NULL; cf->usrnam[0] = '\0'; cf->envnam[0] = '\0'; /* * Allocate a place to record error messages. */ cf->err = _new_ErrMsg(); if(!cf->err) return _del_CompleteFile(cf); /* * Create the object that is used for reading directories. */ cf->dr = _new_DirReader(); if(!cf->dr) return _del_CompleteFile(cf); /* * Create the object that is used to lookup home directories. */ cf->home = _new_HomeDir(); if(!cf->home) return _del_CompleteFile(cf); /* * Create the buffer in which the completed pathname is accumulated. */ cf->path = _new_PathName(); if(!cf->path) return _del_CompleteFile(cf); /* * Create a pathname work buffer. */ cf->buff = _new_PathName(); if(!cf->buff) return _del_CompleteFile(cf); return cf; } /*....................................................................... * Delete a file-completion object. * * Input: * cf CompleteFile * The object to be deleted. * Output: * return CompleteFile * The deleted object (always NULL). */ CompleteFile *_del_CompleteFile(CompleteFile *cf) { if(cf) { cf->err = _del_ErrMsg(cf->err); cf->dr = _del_DirReader(cf->dr); cf->home = _del_HomeDir(cf->home); cf->path = _del_PathName(cf->path); cf->buff = _del_PathName(cf->buff); free(cf); }; return NULL; } /*....................................................................... * Look up the possible completions of the incomplete filename that * lies between specified indexes of a given command-line string. * * Input: * cpl WordCompletion * The object in which to record the completions. * cf CompleteFile * The filename-completion resource object. * line const char * The string containing the incomplete filename. * word_start int The index of the first character in line[] * of the incomplete filename. * word_end int The index of the character in line[] that * follows the last character of the incomplete * filename. * escaped int If true, backslashes in line[] are * interpreted as escaping the characters * that follow them, and any spaces, tabs, * backslashes, or wildcard characters in the * returned suffixes will be similarly escaped. * If false, backslashes will be interpreted as * literal parts of the file name, and no * backslashes will be added to the returned * suffixes. * check_fn CplCheckFn * If not zero, this argument specifies a * function to call to ask whether a given * file should be included in the list * of completions. * check_data void * Anonymous data to be passed to check_fn(). * Output: * return int 0 - OK. * 1 - Error. A description of the error can be * acquired by calling _cf_last_error(cf). */ int _cf_complete_file(WordCompletion *cpl, CompleteFile *cf, const char *line, int word_start, int word_end, int escaped, CplCheckFn *check_fn, void *check_data) { const char *lptr; /* A pointer into line[] */ int nleft; /* The number of characters still to be processed */ /* in line[]. */ /* * Check the arguments. */ if(!cpl || !cf || !line || word_end < word_start) { if(cf) { _err_record_msg(cf->err, "_cf_complete_file: Invalid arguments", END_ERR_MSG); }; return 1; }; /* * Clear the buffer in which the filename will be constructed. */ _pn_clear_path(cf->path); /* * How many characters are to be processed? */ nleft = word_end - word_start; /* * Get a pointer to the start of the incomplete filename. */ lptr = line + word_start; /* * If the first character is a tilde, then perform home-directory * interpolation. */ if(nleft > 0 && *lptr == '~') { int slen; if(!cf_read_name(cf, "User", ++lptr, --nleft, cf->usrnam, USR_LEN)) return 1; /* * Advance over the username in the input line. */ slen = strlen(cf->usrnam); lptr += slen; nleft -= slen; /* * If we haven't hit the end of the input string then we have a complete * username to translate to the corresponding home directory. */ if(nleft > 0) { if(cf_expand_home_dir(cf, cf->usrnam)) 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 filename. */ if(strcmp(cf->path->name, FS_ROOT_DIR) == 0 && strncmp(lptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) { lptr += FS_DIR_SEP_LEN; nleft -= FS_DIR_SEP_LEN; }; /* * If we have reached the end of the input string, then the username * may be incomplete, and we should attempt to complete it. */ } else { /* * Look up the possible completions of the username. */ return cf_complete_username(cf, cpl, cf->usrnam, line, word_start+1, word_end, escaped); }; }; /* * Copy the rest of the path, stopping to expand $envvar expressions * where encountered. */ while(nleft > 0) { int seglen; /* The length of the next segment to be copied */ /* * Find the length of the next segment to be copied, stopping if an * unescaped '$' is seen, or the end of the path is reached. */ for(seglen=0; seglen < nleft; seglen++) { int c = lptr[seglen]; if(escaped && c == '\\') seglen++; else if(c == '$') break; /* * We will be completing the last component of the file name, * so whenever a directory separator is seen, assume that it * might be the start of the last component, and mark the character * that follows it as the start of the name that is to be completed. */ if(nleft >= FS_DIR_SEP_LEN && strncmp(lptr + seglen, FS_DIR_SEP, FS_DIR_SEP_LEN)==0) { word_start = (lptr + seglen) - line + FS_DIR_SEP_LEN; }; }; /* * We have reached either the end of the filename or the start of * $environment_variable expression. Record the newly checked * segment of the filename in the output filename, removing * backslash-escapes where needed. */ if(_pn_append_to_path(cf->path, lptr, seglen, escaped) == NULL) { _err_record_msg(cf->err, "Insufficient memory to complete filename", END_ERR_MSG); return 1; }; lptr += seglen; nleft -= seglen; /* * If the above loop finished before we hit the end of the filename, * then this was because an unescaped $ was seen. In this case, interpolate * the value of the environment variable that follows it into the output * filename. */ if(nleft > 0) { char *value; /* The value of the environment variable */ int vlen; /* The length of the value string */ int nlen; /* The length of the environment variable name */ /* * Read the name of the environment variable. */ if(!cf_read_name(cf, "Environment", ++lptr, --nleft, cf->envnam, ENV_LEN)) return 1; /* * Advance over the environment variable name in the input line. */ nlen = strlen(cf->envnam); lptr += nlen; nleft -= nlen; /* * Get the value of the environment variable. */ value = getenv(cf->envnam); if(!value) { _err_record_msg(cf->err, "Unknown environment variable: ", cf->envnam, END_ERR_MSG); return 1; }; vlen = strlen(value); /* * If we are at the start of the filename and the first character of the * environment variable value is a '~', attempt home-directory * interpolation. */ if(cf->path->name[0] == '\0' && value[0] == '~') { if(!cf_read_name(cf, "User", value+1, vlen-1, cf->usrnam, USR_LEN) || cf_expand_home_dir(cf, cf->usrnam)) return 1; /* * If the home directory is the root directory, and the ~usrname expression * was followed by a directory separator, prevent the directory separator * from being appended to the root directory by skipping it in the * input line. */ if(strcmp(cf->path->name, FS_ROOT_DIR) == 0 && strncmp(lptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) { lptr += FS_DIR_SEP_LEN; nleft -= FS_DIR_SEP_LEN; }; } else { /* * Append the value of the environment variable to the output path. */ if(_pn_append_to_path(cf->path, value, strlen(value), escaped)==NULL) { _err_record_msg(cf->err, "Insufficient memory to complete filename", END_ERR_MSG); return 1; }; /* * Prevent extra directory separators from being added. */ if(nleft >= FS_DIR_SEP_LEN && strcmp(cf->path->name, FS_ROOT_DIR) == 0 && strncmp(lptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) { lptr += FS_DIR_SEP_LEN; nleft -= FS_DIR_SEP_LEN; } else if(vlen > FS_DIR_SEP_LEN && strcmp(value + vlen - FS_DIR_SEP_LEN, FS_DIR_SEP)==0) { cf->path->name[vlen-FS_DIR_SEP_LEN] = '\0'; }; }; /* * If adding the environment variable didn't form a valid directory, * we can't complete the line, since there is no way to separate append * a partial filename to an environment variable reference without * that appended part of the name being seen later as part of the * environment variable name. Thus if the currently constructed path * isn't a directory, quite now with no completions having been * registered. */ if(!_pu_path_is_dir(cf->path->name)) return 0; /* * For the reasons given above, if we have reached the end of the filename * with the expansion of an environment variable, the only allowed * completion involves the addition of a directory separator. */ if(nleft == 0) { if(cpl_add_completion(cpl, line, lptr-line, word_end, FS_DIR_SEP, "", "")) { _err_record_msg(cf->err, cpl_last_error(cpl), END_ERR_MSG); return 1; }; return 0; }; }; }; /* * Complete the filename if possible. */ return cf_complete_entry(cf, cpl, line, word_start, word_end, escaped, check_fn, check_data); } /*....................................................................... * Return a description of the last path-completion error that occurred. * * Input: * cf CompleteFile * The path-completion resource object. * Output: * return const char * The description of the last error. */ const char *_cf_last_error(CompleteFile *cf) { return cf ? _err_get_msg(cf->err) : "NULL CompleteFile argument"; } /*....................................................................... * Lookup the home directory of the specified user, or the current user * if no name is specified, appending it to output pathname. * * Input: * cf CompleteFile * The pathname completion resource object. * user const char * The username to lookup, or "" to lookup the * current user. * Output: * return int 0 - OK. * 1 - Error. */ static int cf_expand_home_dir(CompleteFile *cf, const char *user) { /* * Attempt to lookup the home directory. */ const char *home_dir = _hd_lookup_home_dir(cf->home, user); /* * Failed? */ if(!home_dir) { _err_record_msg(cf->err, _hd_last_home_dir_error(cf->home), END_ERR_MSG); return 1; }; /* * Append the home directory to the pathname string. */ if(_pn_append_to_path(cf->path, home_dir, -1, 0) == NULL) { _err_record_msg(cf->err, "Insufficient memory for home directory expansion", END_ERR_MSG); return 1; }; return 0; } /*....................................................................... * Lookup and report all completions of a given username prefix. * * Input: * cf CompleteFile * The filename-completion resource object. * cpl WordCompletion * The object in which to record the completions. * prefix const char * The prefix of the usernames to lookup. * line const char * The command-line in which the username appears. * word_start int The index within line[] of the start of the * username that is being completed. * word_end int The index within line[] of the character which * follows the incomplete username. * escaped int True if the completions need to have special * characters escaped. * Output: * return int 0 - OK. * 1 - Error. */ static int cf_complete_username(CompleteFile *cf, WordCompletion *cpl, const char *prefix, const char *line, int word_start, int word_end, int escaped) { /* * Set up a container of anonymous arguments to be sent to the * username-lookup iterator. */ CfHomeArgs args; args.cf = cf; args.cpl = cpl; args.prefix_len = strlen(prefix); args.line = line; args.word_start = word_start; args.word_end = word_end; args.escaped = escaped; /* * Iterate through the list of users, recording those which start * with the specified prefix. */ if(_hd_scan_user_home_dirs(cf->home, prefix, &args, cf_homedir_callback)) { _err_record_msg(cf->err, _hd_last_home_dir_error(cf->home), END_ERR_MSG); return 1; }; return 0; } /*....................................................................... * The user/home-directory scanner callback function (see homedir.h) * used by cf_complete_username(). */ static HOME_DIR_FN(cf_homedir_callback) { /* * Get the file-completion resources from the anonymous data argument. */ CfHomeArgs *args = (CfHomeArgs *) data; WordCompletion *cpl = args->cpl; CompleteFile *cf = args->cf; /* * Copy the username into the pathname work buffer, adding backslash * escapes where needed. */ if(cf_prepare_suffix(cf, usrnam+args->prefix_len, args->escaped)) { strncpy(errmsg, _err_get_msg(cf->err), maxerr); errmsg[maxerr] = '\0'; return 1; }; /* * Report the completion suffix that was copied above. */ if(cpl_add_completion(cpl, args->line, args->word_start, args->word_end, cf->buff->name, FS_DIR_SEP, FS_DIR_SEP)) { strncpy(errmsg, cpl_last_error(cpl), maxerr); errmsg[maxerr] = '\0'; return 1; }; return 0; } /*....................................................................... * Report possible completions of the filename in cf->path->name[]. * * Input: * cf CompleteFile * The file-completion resource object. * cpl WordCompletion * The object in which to record the completions. * line const char * The input line, as received by the callback * function. * word_start int The index within line[] of the start of the * last component of the filename that is being * completed. * word_end int The index within line[] of the character which * follows the incomplete filename. * escaped int If true, escape special characters in the * completion suffixes. * check_fn CplCheckFn * If not zero, this argument specifies a * function to call to ask whether a given * file should be included in the list * of completions. * check_data void * Anonymous data to be passed to check_fn(). * Output: * return int 0 - OK. * 1 - Error. */ static int cf_complete_entry(CompleteFile *cf, WordCompletion *cpl, const char *line, int word_start, int word_end, int escaped, CplCheckFn *check_fn, void *check_data) { const char *dirpath; /* The name of the parent directory */ int start; /* The index of the start of the last filename */ /* component in the transcribed filename. */ const char *prefix; /* The filename prefix to be completed */ int prefix_len; /* The length of the filename prefix */ const char *file_name; /* The lastest filename being compared */ int waserr = 0; /* True after errors */ int terminated=0; /* True if the directory part had to be terminated */ /* * Get the pathname string and its current length. */ char *pathname = cf->path->name; int pathlen = strlen(pathname); /* * Locate the start of the final component of the pathname. */ for(start=pathlen - 1; start >= 0 && strncmp(pathname + start, FS_DIR_SEP, FS_DIR_SEP_LEN) != 0; start--) ; /* * Is the parent directory the root directory? */ if(start==0 || (start < 0 && strncmp(pathname, FS_ROOT_DIR, FS_ROOT_DIR_LEN) == 0)) { dirpath = FS_ROOT_DIR; start += FS_ROOT_DIR_LEN; /* * If we found a directory separator then the part which precedes the * last component is the name of the directory to be opened. */ } else if(start > 0) { /* * The _dr_open_dir() function requires the directory name to be '\0' * terminated, so temporarily do this by overwriting the first character * of the directory separator. */ pathname[start] = '\0'; dirpath = pathname; terminated = 1; /* * We reached the start of the pathname before finding a directory * separator, so arrange to open the current working directory. */ } else { start = 0; dirpath = FS_PWD; }; /* * Attempt to open the directory. */ if(_dr_open_dir(cf->dr, dirpath, NULL)) { _err_record_msg(cf->err, "Can't open directory: ", dirpath, END_ERR_MSG); return 1; }; /* * If removed above, restore the directory separator and skip over it * to the start of the filename. */ if(terminated) { memcpy(pathname + start, FS_DIR_SEP, FS_DIR_SEP_LEN); start += FS_DIR_SEP_LEN; }; /* * Get the filename prefix and its length. */ prefix = pathname + start; prefix_len = strlen(prefix); /* * Traverse the directory, looking for files who's prefixes match the * last component of the pathname. */ while((file_name = _dr_next_file(cf->dr)) != NULL && !waserr) { int name_len = strlen(file_name); /* * Is the latest filename a possible completion of the filename prefix? */ if(name_len >= prefix_len && strncmp(prefix, file_name, prefix_len)==0) { /* * When listing all files in a directory, don't list files that start * with '.'. This is how hidden files are denoted in UNIX. */ if(prefix_len > 0 || file_name[0] != '.') { /* * Copy the completion suffix into the work pathname cf->buff->name, * adding backslash escapes if needed. */ if(cf_prepare_suffix(cf, file_name + prefix_len, escaped)) { waserr = 1; } else { /* * We want directories to be displayed with directory suffixes, * and other fully completed filenames to be followed by spaces. * To check the type of the file, append the current suffix * to the path being completed, check the filetype, then restore * the path to its original form. */ const char *cont_suffix = ""; /* The suffix to add if fully */ /* completed. */ const char *type_suffix = ""; /* The suffix to add when listing */ if(_pn_append_to_path(cf->path, file_name + prefix_len, -1, escaped) == NULL) { _err_record_msg(cf->err, "Insufficient memory to complete filename.", END_ERR_MSG); return 1; }; /* * Specify suffixes according to the file type. */ if(_pu_path_is_dir(cf->path->name)) { cont_suffix = FS_DIR_SEP; type_suffix = FS_DIR_SEP; } else if(!check_fn || check_fn(check_data, cf->path->name)) { cont_suffix = " "; } else { cf->path->name[pathlen] = '\0'; continue; }; /* * Remove the temporarily added suffix. */ cf->path->name[pathlen] = '\0'; /* * Record the latest completion. */ if(cpl_add_completion(cpl, line, word_start, word_end, cf->buff->name, type_suffix, cont_suffix)) waserr = 1; }; }; }; }; /* * Close the directory. */ _dr_close_dir(cf->dr); return waserr; } /*....................................................................... * Read a username or environment variable name, stopping when a directory * separator is seen, when the end of the string is reached, or the * output buffer overflows. * * Input: * cf CompleteFile * The file-completion resource object. * type char * The capitalized name of the type of name being read. * string char * The string who's prefix contains the name. * slen int The number of characters in string[]. * nambuf char * The output name buffer. * nammax int The longest string that will fit in nambuf[], excluding * the '\0' terminator. * Output: * return char * A pointer to nambuf on success. On error NULL is * returned and a description of the error is recorded * in cf->err. */ static char *cf_read_name(CompleteFile *cf, const char *type, const char *string, int slen, char *nambuf, int nammax) { int namlen; /* The number of characters in nambuf[] */ const char *sptr; /* A pointer into string[] */ /* * Work out the max number of characters that should be copied. */ int nmax = nammax < slen ? nammax : slen; /* * Get the environment variable name that follows the dollar. */ for(sptr=string,namlen=0; namlen < nmax && (slen-namlen < FS_DIR_SEP_LEN || strncmp(sptr, FS_DIR_SEP, FS_DIR_SEP_LEN) != 0); namlen++) { nambuf[namlen] = *sptr++; }; /* * Did the name overflow the buffer? */ if(namlen >= nammax) { _err_record_msg(cf->err, type, " name too long", END_ERR_MSG); return NULL; }; /* * Terminate the string. */ nambuf[namlen] = '\0'; return nambuf; } /*....................................................................... * Using the work buffer cf->buff, make a suitably escaped copy of a * given completion suffix, ready to be passed to cpl_add_completion(). * * Input: * cf CompleteFile * The file-completion 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 cf_prepare_suffix(CompleteFile *cf, 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(cf->buff); /* * 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(cf->buff, suffix_len + nbsl) == NULL) { _err_record_msg(cf->err, "Insufficient memory to complete filename", END_ERR_MSG); return 1; }; /* * If the suffix doesn't need any escapes, copy it directly into the * work buffer. */ if(nbsl==0) { strcpy(cf->buff->name, suffix); } else { /* * Make a copy with special characters escaped? */ if(nbsl > 0) { const char *src = suffix; char *dst = cf->buff->name; for(i=0; i