1 /* filesys.c -- filesystem specific functions.
2
3 Copyright 1993-2023 Free Software Foundation, Inc.
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 Originally written by Brian Fox. */
19
20 #include "info.h"
21 #include "tilde.h"
22 #include "filesys.h"
23 #include "tag.h"
24 #include "session.h"
25
26 /* Local to this file. */
27 static char *info_file_in_path (char *filename, struct stat *finfo);
28 char *info_add_extension (char *dirname, char *fname,
29 struct stat *finfo);
30
31 static char *filesys_read_compressed (char *pathname, size_t *filesize);
32
33 /* Return the command string that would be used to decompress FILENAME. */
34 static char *filesys_decompressor_for_file (char *filename);
35 static int compressed_filename_p (char *filename);
36
37 typedef struct
38 {
39 char *suffix;
40 char *decompressor;
41 } COMPRESSION_ALIST;
42
43 static char *info_suffixes[] = {
44 ".info",
45 "-info",
46 ".inf", /* 8+3 file on filesystem which supports long file names */
47 #ifdef __MSDOS__
48 /* 8+3 file names strike again... */
49 ".in", /* for .inz, .igz etc. */
50 ".i",
51 #endif
52 "",
53 NULL
54 };
55
56 static COMPRESSION_ALIST compress_suffixes[] = {
57 #if STRIP_DOT_EXE
58 { ".gz", "gunzip" },
59 { ".lz", "lunzip" },
60 #else
61 { ".gz", "gzip -d" },
62 { ".lz", "lzip -d" },
63 #endif
64 { ".xz", "unxz" },
65 { ".bz2", "bunzip2" },
66 { ".z", "gunzip" },
67 { ".lzma", "unlzma" },
68 { ".Z", "uncompress" },
69 { ".zst", "unzstd --rm -q" },
70 { ".Y", "unyabba" },
71 #ifdef __MSDOS__
72 { "gz", "gunzip" },
73 { "z", "gunzip" },
74 #endif
75 { NULL, NULL }
76 };
77
78 /* Look for the filename PARTIAL in INFOPATH in order to find the correct file.
79 Return file name and set *FINFO with information about file. If it
80 can't find the file, it returns NULL, and sets filesys_error_number.
81 Return value should be freed by caller. */
82 char *
83 info_find_fullpath (char *partial, struct stat *finfo)
84 {
85 char *fullpath = 0;
86 struct stat dummy;
87
88 debug(1, (_("looking for file \"%s\""), partial));
89
90 if (!finfo)
91 finfo = &dummy;
92
93 filesys_error_number = 0;
94
95 if (!partial || !*partial)
96 return 0;
97
98 /* IS_SLASH and IS_ABSOLUTE defined in ../system.h. */
99
100 /* If path is absolute already, see if it needs an extension. */
101 if (IS_ABSOLUTE (partial)
102 || partial[0] == '.' && IS_SLASH(partial[1]))
103 {
104 fullpath = info_add_extension (0, partial, finfo);
105 }
106
107 /* Tilde expansion. Could come from user input in echo area. */
108 else if (partial[0] == '~')
109 {
110 partial = tilde_expand_word (partial);
111 fullpath = info_add_extension (0, partial, finfo);
112 }
113
114 /* If just a simple name element, look for it in the path. */
115 else
116 fullpath = info_file_in_path (partial, finfo);
117
118 if (!fullpath)
119 filesys_error_number = ENOENT;
120
121 return fullpath;
122 }
123
124 /* Scan the directories in search path looking for FILENAME. If we find
125 one that is a regular file, return it as a new string. Otherwise, return
126 a NULL pointer. Set *FINFO with information about file. */
127 char *
128 info_file_find_next_in_path (char *filename, int *path_index, struct stat *finfo)
129 {
130 struct stat dummy;
131
132 /* Used for output of stat in case the caller doesn't care about
133 its value. */
134 if (!finfo)
135 finfo = &dummy;
136
137 /* Reject ridiculous cases up front, to prevent infinite recursion
138 later on. E.g., someone might say "info '(.)foo'"... */
139 if (!*filename || STREQ (filename, ".") || STREQ (filename, ".."))
140 return NULL;
141
142 while (1)
143 {
144 char *dirname, *with_extension = 0;
145
146 dirname = infopath_next (path_index);
147 if (!dirname)
148 break;
149
150 debug(1, (_("looking for file %s in %s"), filename, dirname));
151
152 /* Expand a leading tilde if one is present. */
153 if (*dirname == '~')
154 {
155 char *expanded_dirname = tilde_expand_word (dirname);
156 dirname = expanded_dirname;
157 }
158
159 with_extension = info_add_extension (dirname, filename, finfo);
160
161 if (with_extension)
162 {
163 if (!IS_ABSOLUTE (with_extension))
164 {
165 /* Prefix "./" to it. */
166 char *s;
167 xasprintf (&s, "%s%s", "./", with_extension);
168 free (with_extension);
169 return s;
170 }
171 else
172 return with_extension;
173 }
174 }
175 return NULL;
176 }
177
178 /* Return full path of first Info file known as FILENAME in
179 search path. If relative to current directory, precede it with './'. */
180 static char *
181 info_file_in_path (char *filename, struct stat *finfo)
182 {
183 int i = 0;
184 return info_file_find_next_in_path (filename, &i, finfo);
185 }
186
187 /* Check if TRY_FILENAME exists, possibly compressed. If so, return
188 filename in TRY_FILENAME. */
189 char *
190 info_check_compressed (char *try_filename, struct stat *finfo)
191 {
192 int statable = (stat (try_filename, finfo) == 0);
193
194 if (statable)
195 {
196 if (S_ISREG (finfo->st_mode))
197 {
198 debug(1, (_("found file %s"), try_filename));
199 return try_filename;
200 }
201 }
202 else
203 {
204 /* Add various compression suffixes to the name to see if
205 the file is present in compressed format. */
206 register int j, pre_compress_suffix_length;
207
208 pre_compress_suffix_length = strlen (try_filename);
209
210 for (j = 0; compress_suffixes[j].suffix; j++)
211 {
212 strcpy (try_filename + pre_compress_suffix_length,
213 compress_suffixes[j].suffix);
214
215 statable = (stat (try_filename, finfo) == 0);
216 if (statable && (S_ISREG (finfo->st_mode)))
217 {
218 debug(1, (_("found file %s"), try_filename));
219 return try_filename;
220 }
221 }
222 }
223 return 0;
224 }
225
226 /* Look for a file called FILENAME in a directory called DIRNAME, adding file
227 extensions if necessary. FILENAME can be an absolute path or a path
228 relative to the current directory, in which case DIRNAME should be
229 null. Return it as a new string; otherwise return a NULL pointer. */
230 char *
231 info_add_extension (char *dirname, char *filename, struct stat *finfo)
232 {
233 char *try_filename;
234 register int i, pre_suffix_length = 0;
235 struct stat dummy;
236
237 if (!finfo)
238 finfo = &dummy;
239
240 if (dirname)
241 pre_suffix_length += strlen (dirname);
242
243 pre_suffix_length += strlen (filename);
244
245 /* Add enough space for any file extensions at end. */
246 try_filename = xmalloc (pre_suffix_length + 30);
247 try_filename[0] = '\0';
248
249 if (dirname)
250 {
251 strcpy (try_filename, dirname);
252 if (!IS_SLASH (try_filename[(strlen (try_filename)) - 1]))
253 {
254 strcat (try_filename, "/");
255 pre_suffix_length++;
256 }
257 }
258
259 strcat (try_filename, filename);
260
261 for (i = 0; info_suffixes[i]; i++)
262 {
263 char *result;
264 strcpy (try_filename + pre_suffix_length, info_suffixes[i]);
265
266 result = info_check_compressed (try_filename, finfo);
267 if (result)
268 return result;
269 }
270 /* Nothing was found. */
271 free (try_filename);
272 return 0;
273 }
274
275 #if defined (__MSDOS__) || defined (__MINGW32__)
276 /* Given a chunk of text and its length, convert all CRLF pairs at every
277 end-of-line into a single Newline character. Return the length of
278 produced text.
279
280 This is required because the rest of code is too entrenched in having
281 a single newline at each EOL; in particular, searching for various
282 Info headers and cookies can become extremely tricky if that assumption
283 breaks. */
284 static long
285 convert_eols (char *text, long int textlen)
286 {
287 register char *s = text;
288 register char *d = text;
289
290 while (textlen--)
291 {
292 if (*s == '\r' && textlen && s[1] == '\n')
293 {
294 s++;
295 textlen--;
296 }
297 *d++ = *s++;
298 }
299
300 return d - text;
301 }
302 #endif
303
304 /* Read the contents of PATHNAME, returning a buffer with the contents of
305 that file in it, and returning the size of that buffer in FILESIZE.
306 If the file turns out to be compressed, set IS_COMPRESSED to non-zero.
307 If the file cannot be read, set filesys_error_number and return a NULL
308 pointer. Set *FINFO with information about file. */
309 char *
310 filesys_read_info_file (char *pathname, size_t *filesize,
311 struct stat *finfo, int *is_compressed)
312 {
313 size_t fsize;
314 char *contents;
315
316 fsize = filesys_error_number = 0;
317
318 stat (pathname, finfo);
319 fsize = (long) finfo->st_size;
320
321 if (compressed_filename_p (pathname))
322 {
323 *is_compressed = 1;
324 contents = filesys_read_compressed (pathname, &fsize);
325 }
326 else
327 {
328 int descriptor;
329
330 *is_compressed = 0;
331 descriptor = open (pathname, O_RDONLY | O_BINARY, 0666);
332
333 /* If the file couldn't be opened, give up. */
334 if (descriptor < 0)
335 {
336 filesys_error_number = errno;
337 return NULL;
338 }
339
340 /* Try to read the contents of this file. */
341 contents = xmalloc (1 + fsize);
342 if ((read (descriptor, contents, fsize)) != fsize)
343 {
344 filesys_error_number = errno;
345 close (descriptor);
346 free (contents);
347 return NULL;
348 }
349 contents[fsize] = 0;
350 close (descriptor);
351 }
352
353 #if defined (__MSDOS__) || defined (__MINGW32__)
354 /* Old versions of makeinfo on MS-DOS or MS-Windows generated Info files
355 with CR-LF line endings which are only counted as one byte in the file
356 tag table. Convert any of these DOS-style CRLF EOLs into Unix-style NL
357 so that these files can be read correctly on such operating systems.
358
359 Don't do this on GNU/Linux (or other Unix-type operating system), so
360 as not to encourage Info files with CR-LF line endings to be distributed
361 widely beyond their native operating system, which would cause only
362 problems. (If someone really needs to, they can convert the line endings
363 themselves with a separate program.)
364 Also, this will allow any Info files that contain any CR-LF endings by
365 mistake to work as expected (except on MS-DOS/Windows). */
366
367 fsize = convert_eols (contents, fsize);
368
369 /* EOL conversion can shrink the text quite a bit. We don't
370 want to waste storage. */
371 contents = xrealloc (contents, 1 + fsize);
372 contents[fsize] = '\0';
373 #endif
374
375 *filesize = fsize;
376
377 return contents;
378 }
379
380 /* Typically, pipe buffers are 4k. */
381 #define BASIC_PIPE_BUFFER (4 * 1024)
382
383 /* We use some large multiple of that. */
384 #define FILESYS_PIPE_BUFFER_SIZE (16 * BASIC_PIPE_BUFFER)
385
386 static char *
387 filesys_read_compressed (char *pathname, size_t *filesize)
388 {
389 FILE *stream;
390 char *command, *decompressor;
391 char *contents = NULL;
392
393 *filesize = filesys_error_number = 0;
394
395 decompressor = filesys_decompressor_for_file (pathname);
396
397 if (!decompressor)
398 return NULL;
399
400 command = xmalloc (15 + strlen (pathname) + strlen (decompressor));
401 /* Explicit .exe suffix makes the diagnostics of `popen'
402 better on systems where COMMAND.COM is the stock shell. */
403 sprintf (command, "%s%s < %s",
404 decompressor, STRIP_DOT_EXE ? ".exe" : "", pathname);
405
406 if (info_windows_initialized_p)
407 {
408 char *temp;
409
410 temp = xmalloc (5 + strlen (command));
411 sprintf (temp, "%s...", command);
412 message_in_echo_area ("%s", temp);
413 free (temp);
414 }
415
416 stream = popen (command, FOPEN_RBIN);
417 free (command);
418
419 /* Read chunks from this file until there are none left to read. */
420 if (stream)
421 {
422 size_t offset, size;
423 char *chunk;
424
425 offset = size = 0;
426 chunk = xmalloc (FILESYS_PIPE_BUFFER_SIZE);
427
428 while (1)
429 {
430 size_t bytes_read;
431
432 bytes_read = fread (chunk, 1, FILESYS_PIPE_BUFFER_SIZE, stream);
433
434 if (bytes_read + offset >= size)
435 contents = xrealloc
436 (contents, size += (2 * FILESYS_PIPE_BUFFER_SIZE));
437
438 memcpy (contents + offset, chunk, bytes_read);
439 offset += bytes_read;
440 if (bytes_read != FILESYS_PIPE_BUFFER_SIZE)
441 break;
442 }
443
444 free (chunk);
445 if (pclose (stream) == -1)
446 {
447 if (contents)
448 free (contents);
449 contents = NULL;
450 filesys_error_number = errno;
451 }
452 else
453 {
454 contents = xrealloc (contents, 1 + offset);
455 contents[offset] = '\0';
456 *filesize = offset;
457 }
458 }
459 else
460 {
461 filesys_error_number = errno;
462 }
463
464 if (info_windows_initialized_p)
465 unmessage_in_echo_area ();
466 return contents;
467 }
468
469 /* Return non-zero if FILENAME belongs to a compressed file. */
470 static int
471 compressed_filename_p (char *filename)
472 {
473 char *decompressor;
474
475 /* Find the final extension of this filename, and see if it matches one
476 of our known ones. */
477 decompressor = filesys_decompressor_for_file (filename);
478
479 if (decompressor)
480 return 1;
481 else
482 return 0;
483 }
484
485 /* Return the command string that would be used to decompress FILENAME. */
486 static char *
487 filesys_decompressor_for_file (char *filename)
488 {
489 register int i;
490 char *extension = NULL;
491
492 /* Find the final extension of FILENAME, and see if it appears in our
493 list of known compression extensions. */
494 for (i = strlen (filename) - 1; i > 0; i--)
495 if (filename[i] == '.')
496 {
497 extension = filename + i;
498 break;
499 }
500
501 if (!extension)
502 return NULL;
503
504 for (i = 0; compress_suffixes[i].suffix; i++)
505 if (FILENAME_CMP (extension, compress_suffixes[i].suffix) == 0)
506 return compress_suffixes[i].decompressor;
507
508 #if defined (__MSDOS__)
509 /* If no other suffix matched, allow any extension which ends
510 with `z' to be decompressed by gunzip. Due to limited 8+3 DOS
511 file namespace, we can expect many such cases, and supporting
512 every weird suffix thus produced would be a pain. */
513 if (extension[strlen (extension) - 1] == 'z' ||
514 extension[strlen (extension) - 1] == 'Z')
515 return "gunzip";
516 #endif
517
518 return NULL;
519 }
520
521 /* The number of the most recent file system error. */
522 int filesys_error_number = 0;
523
524 /* A function which returns a pointer to a static buffer containing
525 an error message for FILENAME and ERROR_NUM. */
526 static char *errmsg_buf = NULL;
527 static int errmsg_buf_size = 0;
528
529 /* Return string for ERROR_NUM when opening file. Return value should not
530 be freed by caller. */
531 char *
532 filesys_error_string (char *filename, int error_num)
533 {
534 int len;
535 const char *result;
536
537 if (error_num == 0)
538 return NULL;
539
540 result = strerror (error_num);
541
542 len = 4 + strlen (filename) + strlen (result);
543 if (len >= errmsg_buf_size)
544 errmsg_buf = xrealloc (errmsg_buf, (errmsg_buf_size = 2 + len));
545
546 sprintf (errmsg_buf, "%s: %s", filename, result);
547 return errmsg_buf;
548 }
549
550
551 /* Check for "dir" with all the possible info and compression suffixes,
552 in combination. */
553
554 int
555 is_dir_name (char *filename)
556 {
557 unsigned i;
558
559 for (i = 0; info_suffixes[i]; i++)
560 {
561 unsigned c;
562 char trydir[50];
563 strcpy (trydir, "dir");
564 strcat (trydir, info_suffixes[i]);
565
566 if (mbscasecmp (filename, trydir) == 0)
567 return 1;
568
569 for (c = 0; compress_suffixes[c].suffix; c++)
570 {
571 char dir_compressed[50]; /* can be short */
572 strcpy (dir_compressed, trydir);
573 strcat (dir_compressed, compress_suffixes[c].suffix);
574 if (mbscasecmp (filename, dir_compressed) == 0)
575 return 1;
576 }
577 }
578
579 return 0;
580 }