xref: /openbsd/gnu/usr.bin/texinfo/info/filesys.c (revision 7b36286a)
1 /* filesys.c -- filesystem specific functions.
2    $Id: filesys.c,v 1.5 2006/07/17 16:12:36 espie Exp $
3 
4    Copyright (C) 1993, 1997, 1998, 2000, 2002, 2003, 2004 Free Software
5    Foundation, Inc.
6 
7    This program is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2, or (at your option)
10    any later version.
11 
12    This program is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16 
17    You should have received a copy of the GNU General Public License
18    along with this program; if not, write to the Free Software
19    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 
21    Written by Brian Fox (bfox@ai.mit.edu). */
22 
23 #include "info.h"
24 
25 #include "tilde.h"
26 #include "filesys.h"
27 
28 /* Local to this file. */
29 static char *info_file_in_path (char *filename, char *path);
30 static char *lookup_info_filename (char *filename);
31 static char *info_absolute_file (char *fname);
32 
33 static void remember_info_filename (char *filename, char *expansion);
34 static void maybe_initialize_infopath (void);
35 
36 typedef struct
37 {
38   char *suffix;
39   char *decompressor;
40 } COMPRESSION_ALIST;
41 
42 static char *info_suffixes[] = {
43   ".info",
44   "-info",
45   "/index",
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   { ".gz", "gunzip" },
58   { ".bz2", "bunzip2" },
59   { ".z", "gunzip" },
60   { ".Z", "uncompress" },
61   { ".Y", "unyabba" },
62 #ifdef __MSDOS__
63   { "gz", "gunzip" },
64   { "z", "gunzip" },
65 #endif
66   { (char *)NULL, (char *)NULL }
67 };
68 
69 /* The path on which we look for info files.  You can initialize this
70    from the environment variable INFOPATH if there is one, or you can
71    call info_add_path () to add paths to the beginning or end of it.
72    You can call zap_infopath () to make the path go away. */
73 char *infopath = (char *)NULL;
74 static int infopath_size = 0;
75 
76 /* Expand the filename in PARTIAL to make a real name for this operating
77    system.  This looks in INFO_PATHS in order to find the correct file.
78    If it can't find the file, it returns NULL. */
79 static char *local_temp_filename = (char *)NULL;
80 static int local_temp_filename_size = 0;
81 
82 char *
83 info_find_fullpath (char *partial)
84 {
85   int initial_character;
86   char *temp;
87 
88   filesys_error_number = 0;
89 
90   maybe_initialize_infopath ();
91 
92   if (partial && (initial_character = *partial))
93     {
94       char *expansion;
95 
96       expansion = lookup_info_filename (partial);
97 
98       if (expansion)
99         return (expansion);
100 
101       /* If we have the full path to this file, we still may have to add
102          various extensions to it.  I guess we have to stat this file
103          after all. */
104       if (IS_ABSOLUTE (partial))
105 	temp = info_absolute_file (partial);
106       else if (initial_character == '~')
107         {
108           expansion = tilde_expand_word (partial);
109           if (IS_ABSOLUTE (expansion))
110             {
111               temp = info_absolute_file (expansion);
112               free (expansion);
113             }
114           else
115             temp = expansion;
116         }
117       else if (initial_character == '.' &&
118                (IS_SLASH (partial[1]) ||
119 		(partial[1] == '.' && IS_SLASH (partial[2]))))
120         {
121           if (local_temp_filename_size < 1024)
122             local_temp_filename = (char *)xrealloc
123               (local_temp_filename, (local_temp_filename_size = 1024));
124 #if defined (HAVE_GETCWD)
125           if (!getcwd (local_temp_filename, local_temp_filename_size))
126 #else /*  !HAVE_GETCWD */
127           if (!getwd (local_temp_filename))
128 #endif /* !HAVE_GETCWD */
129             {
130               filesys_error_number = errno;
131               return (partial);
132             }
133 
134           strcat (local_temp_filename, "/");
135           strcat (local_temp_filename, partial);
136 	  temp = info_absolute_file (local_temp_filename); /* try extensions */
137 	  if (!temp)
138 	    partial = local_temp_filename;
139         }
140       else
141         temp = info_file_in_path (partial, infopath);
142 
143       if (temp)
144         {
145           remember_info_filename (partial, temp);
146           if (strlen (temp) > (unsigned int) local_temp_filename_size)
147             local_temp_filename = (char *) xrealloc
148               (local_temp_filename,
149                (local_temp_filename_size = (50 + strlen (temp))));
150           strcpy (local_temp_filename, temp);
151           free (temp);
152           return (local_temp_filename);
153         }
154     }
155   return (partial);
156 }
157 
158 /* Scan the list of directories in PATH looking for FILENAME.  If we find
159    one that is a regular file, return it as a new string.  Otherwise, return
160    a NULL pointer. */
161 static char *
162 info_file_in_path (char *filename, char *path)
163 {
164   struct stat finfo;
165   char *temp_dirname;
166   int statable, dirname_index;
167 
168   /* Reject ridiculous cases up front, to prevent infinite recursion
169      later on.  E.g., someone might say "info '(.)foo'"...  */
170   if (!*filename || STREQ (filename, ".") || STREQ (filename, ".."))
171     return NULL;
172 
173   dirname_index = 0;
174 
175   while ((temp_dirname = extract_colon_unit (path, &dirname_index)))
176     {
177       register int i, pre_suffix_length;
178       char *temp;
179 
180       /* Expand a leading tilde if one is present. */
181       if (*temp_dirname == '~')
182         {
183           char *expanded_dirname;
184 
185           expanded_dirname = tilde_expand_word (temp_dirname);
186           free (temp_dirname);
187           temp_dirname = expanded_dirname;
188         }
189 
190       temp = (char *)xmalloc (30 + strlen (temp_dirname) + strlen (filename));
191       strcpy (temp, temp_dirname);
192       if (!IS_SLASH (temp[(strlen (temp)) - 1]))
193         strcat (temp, "/");
194       strcat (temp, filename);
195 
196       pre_suffix_length = strlen (temp);
197 
198       free (temp_dirname);
199 
200       for (i = 0; info_suffixes[i]; i++)
201         {
202           strcpy (temp + pre_suffix_length, info_suffixes[i]);
203 
204           statable = (stat (temp, &finfo) == 0);
205 
206           /* If we have found a regular file, then use that.  Else, if we
207              have found a directory, look in that directory for this file. */
208           if (statable)
209             {
210               if (S_ISREG (finfo.st_mode))
211                 {
212                   return (temp);
213                 }
214               else if (S_ISDIR (finfo.st_mode))
215                 {
216                   char *newpath, *filename_only, *newtemp;
217 
218                   newpath = xstrdup (temp);
219                   filename_only = filename_non_directory (filename);
220                   newtemp = info_file_in_path (filename_only, newpath);
221 
222                   free (newpath);
223                   if (newtemp)
224                     {
225                       free (temp);
226                       return (newtemp);
227                     }
228                 }
229             }
230           else
231             {
232               /* Add various compression suffixes to the name to see if
233                  the file is present in compressed format. */
234               register int j, pre_compress_suffix_length;
235 
236               pre_compress_suffix_length = strlen (temp);
237 
238               for (j = 0; compress_suffixes[j].suffix; j++)
239                 {
240                   strcpy (temp + pre_compress_suffix_length,
241                           compress_suffixes[j].suffix);
242 
243                   statable = (stat (temp, &finfo) == 0);
244                   if (statable && (S_ISREG (finfo.st_mode)))
245                     return (temp);
246                 }
247             }
248         }
249       free (temp);
250     }
251   return ((char *)NULL);
252 }
253 
254 /* Assume FNAME is an absolute file name, and check whether it is
255    a regular file.  If it is, return it as a new string; otherwise
256    return a NULL pointer.  We do it by taking the file name apart
257    into its directory and basename parts, and calling info_file_in_path.*/
258 static char *
259 info_absolute_file (char *fname)
260 {
261   char *containing_dir = xstrdup (fname);
262   char *base = filename_non_directory (containing_dir);
263 
264   if (base > containing_dir)
265     base[-1] = '\0';
266 
267   return info_file_in_path (filename_non_directory (fname), containing_dir);
268 }
269 
270 
271 /* Given a string containing units of information separated by the
272    PATH_SEP character, return the next one after IDX, or NULL if there
273    are no more.  Advance IDX to the character after the colon. */
274 
275 char *
276 extract_colon_unit (char *string, int *idx)
277 {
278   unsigned int i = (unsigned int) *idx;
279   unsigned int start = i;
280 
281   if (!string || i >= strlen (string))
282     return NULL;
283 
284   if (!string[i]) /* end of string */
285     return NULL;
286 
287   /* Advance to next PATH_SEP.  */
288   while (string[i] && string[i] != PATH_SEP[0])
289     i++;
290 
291   {
292     char *value = xmalloc ((i - start) + 1);
293     strncpy (value, &string[start], (i - start));
294     value[i - start] = 0;
295 
296     i++; /* move past PATH_SEP */
297     *idx = i;
298     return value;
299   }
300 }
301 
302 /* A structure which associates a filename with its expansion. */
303 typedef struct
304 {
305   char *filename;
306   char *expansion;
307 } FILENAME_LIST;
308 
309 /* An array of remembered arguments and results. */
310 static FILENAME_LIST **names_and_files = (FILENAME_LIST **)NULL;
311 static int names_and_files_index = 0;
312 static int names_and_files_slots = 0;
313 
314 /* Find the result for having already called info_find_fullpath () with
315    FILENAME. */
316 static char *
317 lookup_info_filename (char *filename)
318 {
319   if (filename && names_and_files)
320     {
321       register int i;
322       for (i = 0; names_and_files[i]; i++)
323         {
324           if (FILENAME_CMP (names_and_files[i]->filename, filename) == 0)
325             return (names_and_files[i]->expansion);
326         }
327     }
328   return (char *)NULL;;
329 }
330 
331 /* Add a filename and its expansion to our list. */
332 static void
333 remember_info_filename (char *filename, char *expansion)
334 {
335   FILENAME_LIST *new;
336 
337   if (names_and_files_index + 2 > names_and_files_slots)
338     {
339       int alloc_size;
340       names_and_files_slots += 10;
341 
342       alloc_size = names_and_files_slots * sizeof (FILENAME_LIST *);
343 
344       names_and_files =
345         (FILENAME_LIST **) xrealloc (names_and_files, alloc_size);
346     }
347 
348   new = (FILENAME_LIST *)xmalloc (sizeof (FILENAME_LIST));
349   new->filename = xstrdup (filename);
350   new->expansion = expansion ? xstrdup (expansion) : (char *)NULL;
351 
352   names_and_files[names_and_files_index++] = new;
353   names_and_files[names_and_files_index] = (FILENAME_LIST *)NULL;
354 }
355 
356 static void
357 maybe_initialize_infopath (void)
358 {
359   if (!infopath_size)
360     {
361       infopath = (char *)
362         xmalloc (infopath_size = (1 + strlen (DEFAULT_INFOPATH)));
363 
364       strcpy (infopath, DEFAULT_INFOPATH);
365     }
366 }
367 
368 /* Add PATH to the list of paths found in INFOPATH.  2nd argument says
369    whether to put PATH at the front or end of INFOPATH. */
370 void
371 info_add_path (char *path, int where)
372 {
373   int len;
374 
375   if (!infopath)
376     {
377       infopath = (char *)xmalloc (infopath_size = 200 + strlen (path));
378       infopath[0] = '\0';
379     }
380 
381   len = strlen (path) + strlen (infopath);
382 
383   if (len + 2 >= infopath_size)
384     infopath = (char *)xrealloc (infopath, (infopath_size += (2 * len) + 2));
385 
386   if (!*infopath)
387     strcpy (infopath, path);
388   else if (where == INFOPATH_APPEND)
389     {
390       strcat (infopath, PATH_SEP);
391       strcat (infopath, path);
392     }
393   else if (where == INFOPATH_PREPEND)
394     {
395       char *temp = xstrdup (infopath);
396       strcpy (infopath, path);
397       strcat (infopath, PATH_SEP);
398       strcat (infopath, temp);
399       free (temp);
400     }
401 }
402 
403 /* Make INFOPATH have absolutely nothing in it. */
404 void
405 zap_infopath (void)
406 {
407   if (infopath)
408     free (infopath);
409 
410   infopath = (char *)NULL;
411   infopath_size = 0;
412 }
413 
414 /* Given a chunk of text and its length, convert all CRLF pairs at every
415    end-of-line into a single Newline character.  Return the length of
416    produced text.
417 
418    This is required because the rest of code is too entrenched in having
419    a single newline at each EOL; in particular, searching for various
420    Info headers and cookies can become extremely tricky if that assumption
421    breaks.
422 
423    FIXME: this could also support Mac-style text files with a single CR
424    at the EOL, but what about random CR characters in non-Mac files?  Can
425    we afford converting them into newlines as well?  Maybe implement some
426    heuristics here, like in Emacs 20.
427 
428    FIXME: is it a good idea to show the EOL type on the modeline?  */
429 long
430 convert_eols (char *text, long int textlen)
431 {
432   register char *s = text;
433   register char *d = text;
434 
435   while (textlen--)
436     {
437       if (*s == '\r' && textlen && s[1] == '\n')
438 	{
439 	  s++;
440 	  textlen--;
441 	}
442       *d++ = *s++;
443     }
444 
445   return (long)(d - text);
446 }
447 
448 /* Read the contents of PATHNAME, returning a buffer with the contents of
449    that file in it, and returning the size of that buffer in FILESIZE.
450    FINFO is a stat struct which has already been filled in by the caller.
451    If the file turns out to be compressed, set IS_COMPRESSED to non-zero.
452    If the file cannot be read, return a NULL pointer. */
453 char *
454 filesys_read_info_file (char *pathname, long int *filesize,
455     struct stat *finfo, int *is_compressed)
456 {
457   long st_size;
458 
459   *filesize = filesys_error_number = 0;
460 
461   if (compressed_filename_p (pathname))
462     {
463       *is_compressed = 1;
464       return (filesys_read_compressed (pathname, filesize));
465     }
466   else
467     {
468       int descriptor;
469       char *contents;
470 
471       *is_compressed = 0;
472       descriptor = open (pathname, O_RDONLY | O_BINARY, 0666);
473 
474       /* If the file couldn't be opened, give up. */
475       if (descriptor < 0)
476         {
477           filesys_error_number = errno;
478           return ((char *)NULL);
479         }
480 
481       /* Try to read the contents of this file. */
482       st_size = (long) finfo->st_size;
483       contents = (char *)xmalloc (1 + st_size);
484       if ((read (descriptor, contents, st_size)) != st_size)
485         {
486 	  filesys_error_number = errno;
487 	  close (descriptor);
488 	  free (contents);
489 	  return ((char *)NULL);
490         }
491 
492       close (descriptor);
493 
494       /* Convert any DOS-style CRLF EOLs into Unix-style NL.
495 	 Seems like a good idea to have even on Unix, in case the Info
496 	 files are coming from some Windows system across a network.  */
497       *filesize = convert_eols (contents, st_size);
498 
499       /* EOL conversion can shrink the text quite a bit.  We don't
500 	 want to waste storage.  */
501       if (*filesize < st_size)
502 	contents = (char *)xrealloc (contents, 1 + *filesize);
503       contents[*filesize] = '\0';
504 
505       return (contents);
506     }
507 }
508 
509 /* Typically, pipe buffers are 4k. */
510 #define BASIC_PIPE_BUFFER (4 * 1024)
511 
512 /* We use some large multiple of that. */
513 #define FILESYS_PIPE_BUFFER_SIZE (16 * BASIC_PIPE_BUFFER)
514 
515 char *
516 filesys_read_compressed (char *pathname, long int *filesize)
517 {
518   FILE *stream;
519   char *command, *decompressor;
520   char *contents = (char *)NULL;
521 
522   *filesize = filesys_error_number = 0;
523 
524   decompressor = filesys_decompressor_for_file (pathname);
525 
526   if (!decompressor)
527     return ((char *)NULL);
528 
529   command = (char *)xmalloc (15 + strlen (pathname) + strlen (decompressor));
530   /* Explicit .exe suffix makes the diagnostics of `popen'
531      better on systems where COMMAND.COM is the stock shell.  */
532   sprintf (command, "%s%s < %s",
533 	   decompressor, STRIP_DOT_EXE ? ".exe" : "", pathname);
534 
535 #if !defined (BUILDING_LIBRARY)
536   if (info_windows_initialized_p)
537     {
538       char *temp;
539 
540       temp = (char *)xmalloc (5 + strlen (command));
541       sprintf (temp, "%s...", command);
542       message_in_echo_area ("%s", temp, NULL);
543       free (temp);
544     }
545 #endif /* !BUILDING_LIBRARY */
546 
547   stream = popen (command, FOPEN_RBIN);
548   free (command);
549 
550   /* Read chunks from this file until there are none left to read. */
551   if (stream)
552     {
553       long offset, size;
554       char *chunk;
555 
556       offset = size = 0;
557       chunk = (char *)xmalloc (FILESYS_PIPE_BUFFER_SIZE);
558 
559       while (1)
560         {
561           int bytes_read;
562 
563           bytes_read = fread (chunk, 1, FILESYS_PIPE_BUFFER_SIZE, stream);
564 
565           if (bytes_read + offset >= size)
566             contents = (char *)xrealloc
567               (contents, size += (2 * FILESYS_PIPE_BUFFER_SIZE));
568 
569           memcpy (contents + offset, chunk, bytes_read);
570           offset += bytes_read;
571           if (bytes_read != FILESYS_PIPE_BUFFER_SIZE)
572             break;
573         }
574 
575       free (chunk);
576       if (pclose (stream) == -1)
577 	{
578 	  if (contents)
579 	    free (contents);
580 	  contents = (char *)NULL;
581 	  filesys_error_number = errno;
582 	}
583       else
584 	{
585 	  *filesize = convert_eols (contents, offset);
586 	  contents = (char *)xrealloc (contents, 1 + *filesize);
587 	  contents[*filesize] = '\0';
588 	}
589     }
590   else
591     {
592       filesys_error_number = errno;
593     }
594 
595 #if !defined (BUILDING_LIBARARY)
596   if (info_windows_initialized_p)
597     unmessage_in_echo_area ();
598 #endif /* !BUILDING_LIBRARY */
599   return (contents);
600 }
601 
602 /* Return non-zero if FILENAME belongs to a compressed file. */
603 int
604 compressed_filename_p (char *filename)
605 {
606   char *decompressor;
607 
608   /* Find the final extension of this filename, and see if it matches one
609      of our known ones. */
610   decompressor = filesys_decompressor_for_file (filename);
611 
612   if (decompressor)
613     return (1);
614   else
615     return (0);
616 }
617 
618 /* Return the command string that would be used to decompress FILENAME. */
619 char *
620 filesys_decompressor_for_file (char *filename)
621 {
622   register int i;
623   char *extension = (char *)NULL;
624 
625   /* Find the final extension of FILENAME, and see if it appears in our
626      list of known compression extensions. */
627   for (i = strlen (filename) - 1; i > 0; i--)
628     if (filename[i] == '.')
629       {
630         extension = filename + i;
631         break;
632       }
633 
634   if (!extension)
635     return ((char *)NULL);
636 
637   for (i = 0; compress_suffixes[i].suffix; i++)
638     if (FILENAME_CMP (extension, compress_suffixes[i].suffix) == 0)
639       return (compress_suffixes[i].decompressor);
640 
641 #if defined (__MSDOS__)
642   /* If no other suffix matched, allow any extension which ends
643      with `z' to be decompressed by gunzip.  Due to limited 8+3 DOS
644      file namespace, we can expect many such cases, and supporting
645      every weird suffix thus produced would be a pain.  */
646   if (extension[strlen (extension) - 1] == 'z' ||
647       extension[strlen (extension) - 1] == 'Z')
648     return "gunzip";
649 #endif
650 
651   return ((char *)NULL);
652 }
653 
654 /* The number of the most recent file system error. */
655 int filesys_error_number = 0;
656 
657 /* A function which returns a pointer to a static buffer containing
658    an error message for FILENAME and ERROR_NUM. */
659 static char *errmsg_buf = (char *)NULL;
660 static int errmsg_buf_size = 0;
661 
662 char *
663 filesys_error_string (char *filename, int error_num)
664 {
665   int len;
666   char *result;
667 
668   if (error_num == 0)
669     return ((char *)NULL);
670 
671   result = strerror (error_num);
672 
673   len = 4 + strlen (filename) + strlen (result);
674   if (len >= errmsg_buf_size)
675     errmsg_buf = (char *)xrealloc (errmsg_buf, (errmsg_buf_size = 2 + len));
676 
677   sprintf (errmsg_buf, "%s: %s", filename, result);
678   return (errmsg_buf);
679 }
680 
681 
682 /* Check for "dir" with all the possible info and compression suffixes,
683    in combination.  */
684 
685 int
686 is_dir_name (char *filename)
687 {
688   unsigned i;
689 
690   for (i = 0; info_suffixes[i]; i++)
691     {
692       unsigned c;
693       char trydir[50];
694       strcpy (trydir, "dir");
695       strcat (trydir, info_suffixes[i]);
696 
697       if (strcasecmp (filename, trydir) == 0)
698         return 1;
699 
700       for (c = 0; compress_suffixes[c].suffix; c++)
701         {
702           char dir_compressed[50]; /* can be short */
703           strcpy (dir_compressed, trydir);
704           strcat (dir_compressed, compress_suffixes[c].suffix);
705           if (strcasecmp (filename, dir_compressed) == 0)
706             return 1;
707         }
708     }
709 
710   return 0;
711 }
712