| /* filesys.c -- filesystem specific functions. |
| |
| Copyright 1993-2022 Free Software Foundation, Inc. |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/>. |
| |
| Originally written by Brian Fox. */ |
| |
| #include "info.h" |
| #include "tilde.h" |
| #include "filesys.h" |
| #include "tag.h" |
| #include "session.h" |
| |
| /* Local to this file. */ |
| static char *info_file_in_path (char *filename, struct stat *finfo); |
| char *info_add_extension (char *dirname, char *fname, |
| struct stat *finfo); |
| |
| static char *filesys_read_compressed (char *pathname, size_t *filesize); |
| |
| /* Return the command string that would be used to decompress FILENAME. */ |
| static char *filesys_decompressor_for_file (char *filename); |
| static int compressed_filename_p (char *filename); |
| |
| typedef struct |
| { |
| char *suffix; |
| char *decompressor; |
| } COMPRESSION_ALIST; |
| |
| static char *info_suffixes[] = { |
| ".info", |
| "-info", |
| "/index", |
| ".inf", /* 8+3 file on filesystem which supports long file names */ |
| #ifdef __MSDOS__ |
| /* 8+3 file names strike again... */ |
| ".in", /* for .inz, .igz etc. */ |
| ".i", |
| #endif |
| "", |
| NULL |
| }; |
| |
| static COMPRESSION_ALIST compress_suffixes[] = { |
| #if STRIP_DOT_EXE |
| { ".gz", "gunzip" }, |
| { ".lz", "lunzip" }, |
| #else |
| { ".gz", "gzip -d" }, |
| { ".lz", "lzip -d" }, |
| #endif |
| { ".xz", "unxz" }, |
| { ".bz2", "bunzip2" }, |
| { ".z", "gunzip" }, |
| { ".lzma", "unlzma" }, |
| { ".Z", "uncompress" }, |
| { ".zst", "unzstd --rm -q" }, |
| { ".Y", "unyabba" }, |
| #ifdef __MSDOS__ |
| { "gz", "gunzip" }, |
| { "z", "gunzip" }, |
| #endif |
| { NULL, NULL } |
| }; |
| |
| /* Look for the filename PARTIAL in INFOPATH in order to find the correct file. |
| Return file name and set *FINFO with information about file. If it |
| can't find the file, it returns NULL, and sets filesys_error_number. |
| Return value should be freed by caller. */ |
| char * |
| info_find_fullpath (char *partial, struct stat *finfo) |
| { |
| char *fullpath = 0; |
| struct stat dummy; |
| |
| debug(1, (_("looking for file \"%s\""), partial)); |
| |
| if (!finfo) |
| finfo = &dummy; |
| |
| filesys_error_number = 0; |
| |
| if (!partial || !*partial) |
| return 0; |
| |
| /* IS_SLASH and IS_ABSOLUTE defined in ../system.h. */ |
| |
| /* If path is absolute already, see if it needs an extension. */ |
| if (IS_ABSOLUTE (partial) |
| || partial[0] == '.' && IS_SLASH(partial[1])) |
| { |
| fullpath = info_add_extension (0, partial, finfo); |
| } |
| |
| /* Tilde expansion. Could come from user input in echo area. */ |
| else if (partial[0] == '~') |
| { |
| partial = tilde_expand_word (partial); |
| fullpath = info_add_extension (0, partial, finfo); |
| } |
| |
| /* If just a simple name element, look for it in the path. */ |
| else |
| fullpath = info_file_in_path (partial, finfo); |
| |
| if (!fullpath) |
| filesys_error_number = ENOENT; |
| |
| return fullpath; |
| } |
| |
| /* Scan the directories in search path looking for FILENAME. If we find |
| one that is a regular file, return it as a new string. Otherwise, return |
| a NULL pointer. Set *FINFO with information about file. */ |
| char * |
| info_file_find_next_in_path (char *filename, int *path_index, struct stat *finfo) |
| { |
| struct stat dummy; |
| |
| /* Used for output of stat in case the caller doesn't care about |
| its value. */ |
| if (!finfo) |
| finfo = &dummy; |
| |
| /* Reject ridiculous cases up front, to prevent infinite recursion |
| later on. E.g., someone might say "info '(.)foo'"... */ |
| if (!*filename || STREQ (filename, ".") || STREQ (filename, "..")) |
| return NULL; |
| |
| while (1) |
| { |
| char *dirname, *with_extension = 0; |
| |
| dirname = infopath_next (path_index); |
| if (!dirname) |
| break; |
| |
| debug(1, (_("looking for file %s in %s"), filename, dirname)); |
| |
| /* Expand a leading tilde if one is present. */ |
| if (*dirname == '~') |
| { |
| char *expanded_dirname = tilde_expand_word (dirname); |
| dirname = expanded_dirname; |
| } |
| |
| with_extension = info_add_extension (dirname, filename, finfo); |
| |
| if (with_extension) |
| { |
| if (!IS_ABSOLUTE (with_extension)) |
| { |
| /* Prefix "./" to it. */ |
| char *s; |
| xasprintf (&s, "%s%s", "./", with_extension); |
| free (with_extension); |
| return s; |
| } |
| else |
| return with_extension; |
| } |
| } |
| return NULL; |
| } |
| |
| /* Return full path of first Info file known as FILENAME in |
| search path. If relative to current directory, precede it with './'. */ |
| static char * |
| info_file_in_path (char *filename, struct stat *finfo) |
| { |
| int i = 0; |
| return info_file_find_next_in_path (filename, &i, finfo); |
| } |
| |
| /* Check if TRY_FILENAME exists, possibly compressed. If so, return |
| filename in TRY_FILENAME. */ |
| char * |
| info_check_compressed (char *try_filename, struct stat *finfo) |
| { |
| int statable = (stat (try_filename, finfo) == 0); |
| |
| if (statable) |
| { |
| if (S_ISREG (finfo->st_mode)) |
| { |
| debug(1, (_("found file %s"), try_filename)); |
| return try_filename; |
| } |
| } |
| else |
| { |
| /* Add various compression suffixes to the name to see if |
| the file is present in compressed format. */ |
| register int j, pre_compress_suffix_length; |
| |
| pre_compress_suffix_length = strlen (try_filename); |
| |
| for (j = 0; compress_suffixes[j].suffix; j++) |
| { |
| strcpy (try_filename + pre_compress_suffix_length, |
| compress_suffixes[j].suffix); |
| |
| statable = (stat (try_filename, finfo) == 0); |
| if (statable && (S_ISREG (finfo->st_mode))) |
| { |
| debug(1, (_("found file %s"), try_filename)); |
| return try_filename; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /* Look for a file called FILENAME in a directory called DIRNAME, adding file |
| extensions if necessary. FILENAME can be an absolute path or a path |
| relative to the current directory, in which case DIRNAME should be |
| null. Return it as a new string; otherwise return a NULL pointer. */ |
| char * |
| info_add_extension (char *dirname, char *filename, struct stat *finfo) |
| { |
| char *try_filename; |
| register int i, pre_suffix_length = 0; |
| struct stat dummy; |
| |
| if (!finfo) |
| finfo = &dummy; |
| |
| if (dirname) |
| pre_suffix_length += strlen (dirname); |
| |
| pre_suffix_length += strlen (filename); |
| |
| /* Add enough space for any file extensions at end. */ |
| try_filename = xmalloc (pre_suffix_length + 30); |
| try_filename[0] = '\0'; |
| |
| if (dirname) |
| { |
| strcpy (try_filename, dirname); |
| if (!IS_SLASH (try_filename[(strlen (try_filename)) - 1])) |
| { |
| strcat (try_filename, "/"); |
| pre_suffix_length++; |
| } |
| } |
| |
| strcat (try_filename, filename); |
| |
| for (i = 0; info_suffixes[i]; i++) |
| { |
| char *result; |
| strcpy (try_filename + pre_suffix_length, info_suffixes[i]); |
| |
| result = info_check_compressed (try_filename, finfo); |
| if (result) |
| return result; |
| } |
| /* Nothing was found. */ |
| free (try_filename); |
| return 0; |
| } |
| |
| #if defined (__MSDOS__) || defined (__MINGW32__) |
| /* Given a chunk of text and its length, convert all CRLF pairs at every |
| end-of-line into a single Newline character. Return the length of |
| produced text. |
| |
| This is required because the rest of code is too entrenched in having |
| a single newline at each EOL; in particular, searching for various |
| Info headers and cookies can become extremely tricky if that assumption |
| breaks. */ |
| static long |
| convert_eols (char *text, long int textlen) |
| { |
| register char *s = text; |
| register char *d = text; |
| |
| while (textlen--) |
| { |
| if (*s == '\r' && textlen && s[1] == '\n') |
| { |
| s++; |
| textlen--; |
| } |
| *d++ = *s++; |
| } |
| |
| return d - text; |
| } |
| #endif |
| |
| /* Read the contents of PATHNAME, returning a buffer with the contents of |
| that file in it, and returning the size of that buffer in FILESIZE. |
| If the file turns out to be compressed, set IS_COMPRESSED to non-zero. |
| If the file cannot be read, set filesys_error_number and return a NULL |
| pointer. Set *FINFO with information about file. */ |
| char * |
| filesys_read_info_file (char *pathname, size_t *filesize, |
| struct stat *finfo, int *is_compressed) |
| { |
| size_t fsize; |
| char *contents; |
| |
| fsize = filesys_error_number = 0; |
| |
| stat (pathname, finfo); |
| fsize = (long) finfo->st_size; |
| |
| if (compressed_filename_p (pathname)) |
| { |
| *is_compressed = 1; |
| contents = filesys_read_compressed (pathname, &fsize); |
| } |
| else |
| { |
| int descriptor; |
| |
| *is_compressed = 0; |
| descriptor = open (pathname, O_RDONLY | O_BINARY, 0666); |
| |
| /* If the file couldn't be opened, give up. */ |
| if (descriptor < 0) |
| { |
| filesys_error_number = errno; |
| return NULL; |
| } |
| |
| /* Try to read the contents of this file. */ |
| contents = xmalloc (1 + fsize); |
| if ((read (descriptor, contents, fsize)) != fsize) |
| { |
| filesys_error_number = errno; |
| close (descriptor); |
| free (contents); |
| return NULL; |
| } |
| contents[fsize] = 0; |
| close (descriptor); |
| } |
| |
| #if defined (__MSDOS__) || defined (__MINGW32__) |
| /* Old versions of makeinfo on MS-DOS or MS-Windows generated Info files |
| with CR-LF line endings which are only counted as one byte in the file |
| tag table. Convert any of these DOS-style CRLF EOLs into Unix-style NL |
| so that these files can be read correctly on such operating systems. |
| |
| Don't do this on GNU/Linux (or other Unix-type operating system), so |
| as not to encourage Info files with CR-LF line endings to be distributed |
| widely beyond their native operating system, which would cause only |
| problems. (If someone really needs to, they can convert the line endings |
| themselves with a separate program.) |
| Also, this will allow any Info files that contain any CR-LF endings by |
| mistake to work as expected (except on MS-DOS/Windows). */ |
| |
| fsize = convert_eols (contents, fsize); |
| |
| /* EOL conversion can shrink the text quite a bit. We don't |
| want to waste storage. */ |
| contents = xrealloc (contents, 1 + fsize); |
| contents[fsize] = '\0'; |
| #endif |
| |
| *filesize = fsize; |
| |
| return contents; |
| } |
| |
| /* Typically, pipe buffers are 4k. */ |
| #define BASIC_PIPE_BUFFER (4 * 1024) |
| |
| /* We use some large multiple of that. */ |
| #define FILESYS_PIPE_BUFFER_SIZE (16 * BASIC_PIPE_BUFFER) |
| |
| static char * |
| filesys_read_compressed (char *pathname, size_t *filesize) |
| { |
| FILE *stream; |
| char *command, *decompressor; |
| char *contents = NULL; |
| |
| *filesize = filesys_error_number = 0; |
| |
| decompressor = filesys_decompressor_for_file (pathname); |
| |
| if (!decompressor) |
| return NULL; |
| |
| command = xmalloc (15 + strlen (pathname) + strlen (decompressor)); |
| /* Explicit .exe suffix makes the diagnostics of `popen' |
| better on systems where COMMAND.COM is the stock shell. */ |
| sprintf (command, "%s%s < %s", |
| decompressor, STRIP_DOT_EXE ? ".exe" : "", pathname); |
| |
| if (info_windows_initialized_p) |
| { |
| char *temp; |
| |
| temp = xmalloc (5 + strlen (command)); |
| sprintf (temp, "%s...", command); |
| message_in_echo_area ("%s", temp); |
| free (temp); |
| } |
| |
| stream = popen (command, FOPEN_RBIN); |
| free (command); |
| |
| /* Read chunks from this file until there are none left to read. */ |
| if (stream) |
| { |
| size_t offset, size; |
| char *chunk; |
| |
| offset = size = 0; |
| chunk = xmalloc (FILESYS_PIPE_BUFFER_SIZE); |
| |
| while (1) |
| { |
| size_t bytes_read; |
| |
| bytes_read = fread (chunk, 1, FILESYS_PIPE_BUFFER_SIZE, stream); |
| |
| if (bytes_read + offset >= size) |
| contents = xrealloc |
| (contents, size += (2 * FILESYS_PIPE_BUFFER_SIZE)); |
| |
| memcpy (contents + offset, chunk, bytes_read); |
| offset += bytes_read; |
| if (bytes_read != FILESYS_PIPE_BUFFER_SIZE) |
| break; |
| } |
| |
| free (chunk); |
| if (pclose (stream) == -1) |
| { |
| if (contents) |
| free (contents); |
| contents = NULL; |
| filesys_error_number = errno; |
| } |
| else |
| { |
| contents = xrealloc (contents, 1 + offset); |
| contents[offset] = '\0'; |
| *filesize = offset; |
| } |
| } |
| else |
| { |
| filesys_error_number = errno; |
| } |
| |
| if (info_windows_initialized_p) |
| unmessage_in_echo_area (); |
| return contents; |
| } |
| |
| /* Return non-zero if FILENAME belongs to a compressed file. */ |
| static int |
| compressed_filename_p (char *filename) |
| { |
| char *decompressor; |
| |
| /* Find the final extension of this filename, and see if it matches one |
| of our known ones. */ |
| decompressor = filesys_decompressor_for_file (filename); |
| |
| if (decompressor) |
| return 1; |
| else |
| return 0; |
| } |
| |
| /* Return the command string that would be used to decompress FILENAME. */ |
| static char * |
| filesys_decompressor_for_file (char *filename) |
| { |
| register int i; |
| char *extension = NULL; |
| |
| /* Find the final extension of FILENAME, and see if it appears in our |
| list of known compression extensions. */ |
| for (i = strlen (filename) - 1; i > 0; i--) |
| if (filename[i] == '.') |
| { |
| extension = filename + i; |
| break; |
| } |
| |
| if (!extension) |
| return NULL; |
| |
| for (i = 0; compress_suffixes[i].suffix; i++) |
| if (FILENAME_CMP (extension, compress_suffixes[i].suffix) == 0) |
| return compress_suffixes[i].decompressor; |
| |
| #if defined (__MSDOS__) |
| /* If no other suffix matched, allow any extension which ends |
| with `z' to be decompressed by gunzip. Due to limited 8+3 DOS |
| file namespace, we can expect many such cases, and supporting |
| every weird suffix thus produced would be a pain. */ |
| if (extension[strlen (extension) - 1] == 'z' || |
| extension[strlen (extension) - 1] == 'Z') |
| return "gunzip"; |
| #endif |
| |
| return NULL; |
| } |
| |
| /* The number of the most recent file system error. */ |
| int filesys_error_number = 0; |
| |
| /* A function which returns a pointer to a static buffer containing |
| an error message for FILENAME and ERROR_NUM. */ |
| static char *errmsg_buf = NULL; |
| static int errmsg_buf_size = 0; |
| |
| /* Return string for ERROR_NUM when opening file. Return value should not |
| be freed by caller. */ |
| char * |
| filesys_error_string (char *filename, int error_num) |
| { |
| int len; |
| const char *result; |
| |
| if (error_num == 0) |
| return NULL; |
| |
| result = strerror (error_num); |
| |
| len = 4 + strlen (filename) + strlen (result); |
| if (len >= errmsg_buf_size) |
| errmsg_buf = xrealloc (errmsg_buf, (errmsg_buf_size = 2 + len)); |
| |
| sprintf (errmsg_buf, "%s: %s", filename, result); |
| return errmsg_buf; |
| } |
| |
| |
| /* Check for "dir" with all the possible info and compression suffixes, |
| in combination. */ |
| |
| int |
| is_dir_name (char *filename) |
| { |
| unsigned i; |
| |
| for (i = 0; info_suffixes[i]; i++) |
| { |
| unsigned c; |
| char trydir[50]; |
| strcpy (trydir, "dir"); |
| strcat (trydir, info_suffixes[i]); |
| |
| if (mbscasecmp (filename, trydir) == 0) |
| return 1; |
| |
| for (c = 0; compress_suffixes[c].suffix; c++) |
| { |
| char dir_compressed[50]; /* can be short */ |
| strcpy (dir_compressed, trydir); |
| strcat (dir_compressed, compress_suffixes[c].suffix); |
| if (mbscasecmp (filename, dir_compressed) == 0) |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |