/* install-info -- merge Info directory entries from an Info file. Copyright 1996-2023 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 . */ #include "system.h" #include #include #include #define TAB_WIDTH 8 static char *progname = "install-info"; static char *default_section = NULL; struct spec_entry; struct spec_section; struct line_data *findlines (char *data, int size, int *nlinesp); void insert_entry_here (struct spec_entry *entry, int line_number, struct line_data *dir_lines, int n_entries); int compare_section_names (const void *s1, const void *s2); int compare_entries_text (const void *e1, const void *e2); /* Data structures. */ /* Record info about a single line from a file as read into core. */ struct line_data { /* The start of the line. */ char *start; /* The number of characters in the line, excluding the terminating newline. */ int size; /* Vector containing pointers to the entries to add before this line. The vector is null-terminated. */ struct spec_entry **add_entries_before; /* Vector containing pointers to the sections to add before this line. The vector is not null-terminated. */ struct spec_section **add_sections_before; /* The vector ADD_SECTIONS_BEFORE_HERE contains exactly this many pointers to sections. */ int num_sections_to_add; /* 1 means don't output this line. */ int delete; }; /* This is used for a list of the specified menu section names in which entries should be added. */ struct spec_section { struct spec_section *next; char *name; /* 1 means we have not yet found an existing section with this name in the dir file--so we will need to add a new section. */ int missing; }; /* This is used for a list of the entries specified to be added. */ struct spec_entry { struct spec_entry *next; char *text; size_t text_len; /* A pointer to the list of sections to which this entry should be added. */ struct spec_section *entry_sections; /* A pointer to a section that is beyond the end of the chain whose head is pointed to by entry_sections. */ struct spec_section *entry_sections_tail; /* Non-zero means that the entry doesn't have a name specified. This can only happen if a --description preceeds a --name option. */ int missing_name; /* Non-zero means that the entry doesn't have a description. This happens when a --name option is given prior to a --description option. */ int missing_description; /* Non-zero means that the entry doesn't have an Info file specified. This means that the entry was taken from the command-line but it only contains the name, and not the info file's basename, which we get later on. This only happens on entries that originate from --name options. */ int missing_basename; }; /* This is used for a list of nodes found by parsing the dir file. */ struct node { struct node *next; /* The node name. */ char *name; /* The line number of the line where the node starts. This is the line that contains control-underscore. */ int start_line; /* The line number of the line where the node ends, which is the end of the file or where the next line starts. */ int end_line; /* Start of first line in this node's menu (the line after the * Menu: line). */ char *menu_start; /* The start of the chain of sections in this node's menu. */ struct menu_section *sections; /* The last menu section in the chain. */ struct menu_section *last_section; }; /* This is used for a list of sections found in a node's menu. Each struct node has such a list in the sections field. */ struct menu_section { struct menu_section *next; char *name; /* Line number of start of section. */ int start_line; /* Line number of end of section. */ int end_line; }; /* This table defines all the long-named options, says whether they use an argument, and maps them into equivalent single-letter options. */ struct option longopts[] = { { "add-once", no_argument, NULL, '1'}, { "align", required_argument, NULL, 'A'}, { "append-new-sections", no_argument, NULL, 'a'}, { "calign", required_argument, NULL, 'C'}, { "debug", no_argument, NULL, 'g' }, { "delete", no_argument, NULL, 'r' }, { "defentry", required_argument, NULL, 'E' }, { "defsection", required_argument, NULL, 'S' }, { "dir-file", required_argument, NULL, 'd' }, { "entry", required_argument, NULL, 'e' }, { "name", required_argument, NULL, 't' }, { "menuentry", required_argument, NULL, 't' }, { "description", required_argument, NULL, 'c' }, { "help", no_argument, NULL, 'h' }, { "no-indent", no_argument, NULL, 'I' }, { "infodir", required_argument, NULL, 'D' }, { "info-dir", required_argument, NULL, 'D' }, { "info-file", required_argument, NULL, 'i' }, { "item", required_argument, NULL, 'e' }, { "keep-old", no_argument, NULL, 'k' }, { "maxwidth", required_argument, NULL, 'W'}, { "max-width", required_argument, NULL, 'W'}, { "quiet", no_argument, NULL, 'q' }, { "remove", no_argument, NULL, 'r' }, { "remove-exactly", no_argument, NULL, 'x' }, { "section", required_argument, NULL, 's' }, { "regex", required_argument, NULL, 'R' }, { "silent", no_argument, NULL, 'q' }, { "test", no_argument, NULL, 'n' }, { "dry-run", no_argument, NULL, 'n' }, { "version", no_argument, NULL, 'V' }, { 0 } }; regex_t *psecreg = NULL; /* Nonzero means that the name specified for the Info file will be used (without removing .gz, .info extension or leading path) to match the entries that must be removed. */ int remove_exactly = 0; /* Nonzero means that sections that don't have entries in them will be deleted. */ int remove_empty_sections = 1; /* Nonzero means that new Info entries into the DIR file go into all sections that match with --section-regex or --section. Zero means that new entries go into only the first section that matches. */ int add_entries_into_all_matching_sections = 1; /* Nonzero means we do not replace same-named info entries. */ int keep_old_flag = 0; /* Nonzero means --test was specified, to inhibit updating the dir file. */ int chicken_flag = 0; /* Zero means that entries will not be formatted when they are either added or replaced. */ int indent_flag = 1; /* Zero means that new sections will be added at the end of the DIR file. */ int order_new_sections_alphabetically_flag = 1; /* Error message functions. */ void vdiag (const char *fmt, const char *diagtype, va_list ap) { fprintf (stderr, "%s: ", progname); if (diagtype) fprintf (stderr, "%s: ", diagtype); vfprintf (stderr, fmt, ap); putc ('\n', stderr); } /* declare as static to avoid clash with glibc error function, called from gnulib. */ static void error (const char *fmt, ...) { va_list ap; va_start (ap, fmt); vdiag (fmt, NULL, ap); va_end (ap); } /* VARARGS1 */ void warning (const char *fmt, ...) { va_list ap; va_start (ap, fmt); vdiag (fmt, "warning", ap); va_end (ap); } /* Print error message and exit. */ void fatal (const char *fmt, ...) { va_list ap; va_start (ap, fmt); vdiag (fmt, NULL, ap); va_end (ap); exit (EXIT_FAILURE); } /* Return a newly-allocated string whose contents concatenate those of S1, S2, S3. */ char * concat (const char *s1, const char *s2, const char *s3) { int len1 = strlen (s1), len2 = strlen (s2), len3 = strlen (s3); char *result = (char *) xmalloc (len1 + len2 + len3 + 1); strcpy (result, s1); strcpy (result + len1, s2); strcpy (result + len1 + len2, s3); *(result + len1 + len2 + len3) = 0; return result; } /* Return a string containing SIZE characters copied from starting at STRING. */ char * copy_string (const char *string, int size) { int i; char *copy = (char *) xmalloc (size + 1); for (i = 0; i < size; i++) copy[i] = string[i]; copy[size] = 0; return copy; } /* Print fatal error message based on errno, with file name NAME. */ void pfatal_with_name (const char *name) { /* Empty files don't set errno, so we get something like "install-info: No error for foo", which is confusing. */ if (errno == 0) fatal (_("%s: empty file"), name); fatal (_("%s for %s"), strerror (errno), name); } /* Compare the menu item names in LINE1 (line length LEN1) and LINE2 (line length LEN2). Return 1 if the item name in LINE1 is less, 0 otherwise. */ static int menu_line_lessp (char *line1, int len1, char *line2, int len2) { int minlen = (len1 < len2 ? len1 : len2); int i; for (i = 0; i < minlen; i++) { /* If one item name is a prefix of the other, the former one is less. */ if (line1[i] == ':' && line2[i] != ':') return 1; if (line2[i] == ':' && line1[i] != ':') return 0; /* If they both continue and differ, one is less. */ if (line1[i] < line2[i]) return 1; if (line1[i] > line2[i]) return 0; } /* With a properly formatted dir file, we can only get here if the item names are equal. */ return 0; } /* Compare the menu item names in LINE1 (line length LEN1) and LINE2 (line length LEN2). Return 1 if the item names are equal, 0 otherwise. */ static int menu_line_equal (char *line1, int len1, char *line2, int len2) { int minlen = (len1 < len2 ? len1 : len2); int i; for (i = 0; i < minlen; i++) { /* If both item names end here, they are equal. */ if (line1[i] == ':' && line2[i] == ':') return 1; /* If they both continue and differ, one is less. */ if (line1[i] != line2[i]) return 0; } /* With a properly formatted dir file, we can only get here if the item names are equal. */ return 1; } /* Given the full text of a menu entry, null terminated, return just the menu item name (copied). */ char * extract_menu_item_name (char *item_text) { char *p; if (*item_text == '*') item_text++; while (*item_text == ' ') item_text++; p = item_text; while (*p && *p != ':') p++; return copy_string (item_text, p - item_text); } /* Return FNAME with any [.info][.gz] suffix removed. */ static char * strip_info_suffix (char *fname) { char *ret = xstrdup (fname); unsigned len = strlen (ret); if (len > 3 && FILENAME_CMP (ret + len - 3, ".gz") == 0) { len -= 3; ret[len] = 0; } else if (len > 3 && FILENAME_CMP (ret + len - 3, ".xz") == 0) { len -= 3; ret[len] = 0; } else if (len > 4 && FILENAME_CMP (ret + len - 4, ".bz2") == 0) { len -= 4; ret[len] = 0; } else if (len > 4 && FILENAME_CMP (ret + len - 4, ".zst") == 0) { len -= 4; ret[len] = 0; } else if (len > 3 && FILENAME_CMP (ret + len - 3, ".lz") == 0) { len -= 3; ret[len] = 0; } else if (len > 5 && FILENAME_CMP (ret + len - 5, ".lzma") == 0) { len -= 5; ret[len] =0; } if (len > 5 && FILENAME_CMP (ret + len - 5, ".info") == 0) { len -= 5; ret[len] = 0; } else if (len > 4 && FILENAME_CMP (ret + len - 4, ".inf") == 0) { len -= 4; ret[len] = 0; } #ifdef __MSDOS__ else if (len > 4 && (FILENAME_CMP (ret + len - 4, ".inz") == 0 || FILENAME_CMP (ret + len - 4, ".igz") == 0)) { len -= 4; ret[len] = 0; } #endif /* __MSDOS__ */ return ret; } /* Return true if ITEM matches NAME and is followed by TERM_CHAR. ITEM can also be followed by `.gz', `.info.gz', or `.info' (and then TERM_CHAR) and still match. */ static int menu_item_equal (const char *item, char term_char, const char *name) { int ret; const char *item_basename = item; unsigned name_len = strlen (name); /* We must compare the basename in ITEM, since we are passed the basename of the original info file. Otherwise, a new entry like "lilypond/lilypond" won't match "lilypond". Actually, it seems to me that we should really compare the whole name, and not just the basename. Couldn't there be dir1/foo.info and dir2/foo.info? Also, it seems like we should be using the filename from the new dir entries, not the filename on the command line. Not worrying about those things right now, though. --karl, 26mar04. */ if (!remove_exactly) { while (*item_basename && !IS_SLASH (*item_basename) && *item_basename != term_char) item_basename++; if (! *item_basename || *item_basename == term_char) item_basename = item; /* no /, use original */ else item_basename++; /* have /, move past it */ } /* First, ITEM must actually match NAME (usually it won't). */ ret = mbsncasecmp (item_basename, name, name_len) == 0; if (ret) { /* Then, `foobar' doesn't match `foo', so be sure we've got all of ITEM. The various suffixes should never actually appear in the dir file, but sometimes people put them in. */ static char *suffixes[] = { "", ".info.gz", ".info", ".inf", ".gz", #ifdef __MSDOS__ ".inz", ".igz", #endif NULL }; unsigned i; ret = 0; for (i = 0; !ret && suffixes[i]; i++) { char *suffix = suffixes[i]; unsigned suffix_len = strlen (suffix); ret = mbsncasecmp (item_basename + name_len, suffix, suffix_len) == 0 && item_basename[name_len + suffix_len] == term_char; } } return ret; } void suggest_asking_for_help (void) { fprintf (stderr, _("\tTry `%s --help' for a complete list of options.\n"), progname); exit (EXIT_FAILURE); } void print_help (void) { printf (_("Usage: %s [OPTION]... [INFO-FILE [DIR-FILE]]\n"), progname); puts (""); puts (_("Add or remove entries in INFO-FILE from the Info directory DIR-FILE.")); puts (_("INFO-FILE and DIR-FILE are required unless the --info-file\n\ or --dir-file (or --info-dir) options are given, respectively.")); puts (""); puts (_("\ Options:\n\ --add-once add only to first matching section, not all.\n\ --align=COL start description of new entries at column COL.\n\ --calign=COL format second and subsequent description lines to\n\ start at column COL.")); puts (_("\ --debug report what is being done.\n\ --delete delete existing entries for INFO-FILE from DIR-FILE;\n\ don't insert any new entries.\n\ --defsection=TEXT like --section, but only use TEXT if no sections\n\ are present in INFO-FILE (replacing \"Miscellaneous\").\n\ --description=TEXT the description of the entry is TEXT; used with\n\ the --name option to become synonymous with the\n\ --entry option.\n\ --dir-file=NAME specify file name of Info directory file;\n\ equivalent to using the DIR-FILE argument.\n\ --dry-run same as --test.")); puts (_("\ --entry=TEXT insert TEXT as an Info directory entry,\n\ overriding any corresponding entry from DIR-FILE.\n\ TEXT is written as an Info menu item line followed\n\ by zero or more extra lines starting with whitespace.\n\ If you specify more than one entry, all are added.\n\ If you don't specify any entries, they are determined\n\ from information in the Info file itself.")); puts (_("\ --help display this help and exit.\n\ --info-dir=DIR same as --dir-file=DIR/dir.\n\ --info-file=FILE specify Info file to install in the directory;\n\ equivalent to using the INFO-FILE argument.\n\ --item=TEXT same as --entry=TEXT.\n\ --keep-old do not replace entries, or remove empty sections.")); puts (_("\ --maxwidth, --max-width=COL wrap description at column COL.\n\ --menuentry=TEXT same as --name=TEXT.\n\ --name=TEXT the name of the entry is TEXT; used with --description\n\ to become synonymous with the --entry option.\n\ --no-indent do not format new entries in the DIR file.\n\ --quiet suppress warnings.")); puts (_("\ --regex=R put this file's entries in all sections that match the\n\ regular expression R (ignoring case).\n\ --remove same as --delete.\n\ --remove-exactly only remove if the info file name matches exactly;\n\ suffixes such as .info and .gz are not ignored.\n\ --section=SEC put entries in section SEC of the directory.\n\ If you specify more than one section, all the entries\n\ are added in each of the sections.\n\ If you don't specify any sections, they are determined\n\ from information in the Info file itself;\n\ if nothing is available there, the --defsection\n\ value is used; if that is not specified, the\n\ final default is \"Miscellaneous\".\n\ --section R SEC equivalent to --regex=R --section=SEC --add-once.")); puts (_("\ --silent suppress warnings.\n\ --test suppress updating of DIR-FILE.\n\ --version display version information and exit.")); puts (""); puts (_("\ Email bug reports to bug-texinfo@gnu.org,\n\ general questions and discussion to help-texinfo@gnu.org.\n\ Texinfo home page: http://www.gnu.org/software/texinfo/")); } /* If DIRFILE does not exist, and we are not in test mode, create a minimal one (or abort). If it already exists, do nothing. */ static void ensure_dirfile_exists (char *dirfile) { int desc; if (chicken_flag) return; desc = open (dirfile, O_RDONLY); if (desc < 0 && errno == ENOENT) { FILE *f; const char *readerr = strerror (errno); f = fopen (dirfile, "w"); if (f) { fprintf (f, _("This is the file .../info/dir, which contains the\n\ topmost node of the Info hierarchy, called (dir)Top.\n\ The first time you invoke Info you start off looking at this node.\n\ %c\n\ %s\tThis is the top of the INFO tree\n\ \n\ This (the Directory node) gives a menu of major topics.\n\ Typing \"q\" exits, \"H\" lists all Info commands, \"d\" returns here,\n\ \"h\" gives a primer for first-timers,\n\ \"mEmacs\" visits the Emacs manual, etc.\n\ \n\ In Emacs, you can click mouse button 2 on a menu item or cross reference\n\ to select it.\n\ \n\ %s\n\ "), /* These keywords must not be translated: */ '\x1f', "File: dir,\tNode: Top", "* Menu:" ); if (fclose (f) < 0) pfatal_with_name (dirfile); } else { /* Didn't exist, but couldn't open for writing. */ fatal (_("%s: could not read (%s) and could not create (%s)"), dirfile, readerr, strerror (errno)); } } else close (desc); /* It already existed, so fine. */ } /* Open FILENAME and return the resulting stream pointer. If it doesn't exist, look for a compressed version and return a pipe reading the output of the compression program. If still not found, call CREATE_CALLBACK (with FILENAME as arg) to create it, if that is non-NULL. If still no luck, return a null pointer. Return the actual name of the file we tried to open in OPENED_FILENAME and the compress program to (de)compress it in COMPRESSION_PROGRAM. */ FILE * open_possibly_compressed_file (char *filename, void (*create_callback) (char *), char **opened_filename, char **compression_program) { char *local_opened_filename, *local_compression_program; int nread; char data[13]; FILE *f; /* We let them pass NULL if they don't want this info, but it's easier to always determine it. */ if (!opened_filename) opened_filename = &local_opened_filename; *opened_filename = filename; f = fopen (*opened_filename, FOPEN_RBIN); if (!f) { *opened_filename = concat (filename, ".gz", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".xz", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".bz2", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".zst", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".lz", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".lzma", ""); f = fopen (*opened_filename, FOPEN_RBIN); } #ifdef __MSDOS__ if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".igz", ""); f = fopen (*opened_filename, FOPEN_RBIN); } if (!f) { free (*opened_filename); *opened_filename = concat (filename, ".inz", ""); f = fopen (*opened_filename, FOPEN_RBIN); } #endif /* __MSDOS__ */ if (!f) { /* The file was not found with any extention added. Try the original file again. */ free (*opened_filename); *opened_filename = filename; if (create_callback) { /* Create the file if we can. */ (*create_callback) (filename); /* And try opening it again. */ f = fopen (*opened_filename, FOPEN_RBIN); if (!f) return 0; } else return 0; } /* Read first few bytes of file rather than relying on the filename. If the file is shorter than this it can't be usable anyway. */ nread = fread (data, sizeof (data), 1, f); if (nread != 1) { if (nread == 0) { /* Try to create the file if its empty. */ if (feof (f) && create_callback) { if (fclose (f) != 0) return 0; /* unknown error closing file */ if (remove (filename) != 0) return 0; /* unknown error deleting file */ (*create_callback) (filename); f = fopen (*opened_filename, FOPEN_RBIN); if (!f) return 0; nread = fread (data, sizeof (data), 1, f); if (nread == 0) return 0; goto determine_file_type; /* success */ } } errno = 0; return 0; /* unknown error */ } determine_file_type: if (!compression_program) compression_program = &local_compression_program; if (data[0] == '\x1f' && data[1] == '\x8b') #if STRIP_DOT_EXE /* An explicit .exe yields a better diagnostics from popen below if they don't have gzip installed. */ *compression_program = "gzip.exe"; #else *compression_program = "gzip"; #endif else if (data[0] == '\xFD' && data[1] == '7' && data[2] == 'z' && data[3] == 'X' && data[4] == 'Z' && data[5] == 0) #ifndef STRIP_DOT_EXE *compression_program = "xz.exe"; #else *compression_program = "xz"; #endif else if (data[0] == 'B' && data[1] == 'Z' && data[2] == 'h') #ifndef STRIP_DOT_EXE *compression_program = "bzip2.exe"; #else *compression_program = "bzip2"; #endif else if (data[0] == 'B' && data[1] == 'Z' && data[2] == '0') #ifndef STRIP_DOT_EXE *compression_program = "bzip.exe"; #else *compression_program = "bzip"; #endif else if (data[0] == 0x4C && data[1] == 0x5A && data[2] == 0x49 && data[3] == 0x50 && data[4] == 1) /* "LZIP" */ #ifndef STRIP_DOT_EXE *compression_program = "lzip.exe"; #else *compression_program = "lzip"; #endif /* We (try to) match against old lzma format (which lacks proper header, two first matches), as well as the new format (last match). */ else if ((data[9] == 0x00 && data[10] == 0x00 && data[11] == 0x00 && data[12] == 0x00) || (data[5] == '\xFF' && data[6] == '\xFF' && data[7] == '\xFF' && data[8] == '\xFF' && data[9] == '\xFF' && data[10] == '\xFF' && data[11] == '\xFF' && data[12] == '\xFF') || (data[0] == '\xFF' && data[1] == 'L' && data[2] == 'Z' && data[3] == 'M' && data[4] == 'A' && data[5] == 0x00)) #ifndef STRIP_DOT_EXE *compression_program = "lzma.exe"; #else *compression_program = "lzma"; #endif else *compression_program = NULL; if (*compression_program) { /* Redirect stdin to the file and fork the decompression process reading from stdin. This allows shell metacharacters in filenames. */ char *command = concat (*compression_program, " -d", ""); FILE *f2; if (fclose (f) < 0) return 0; f2 = freopen (*opened_filename, FOPEN_RBIN, stdin); if (!f) return 0; f = popen (command, "r"); fclose (f2); if (!f) { /* Used for error message in calling code. */ *opened_filename = command; return 0; } free (command); } else { #if O_BINARY /* Since this is a text file, and we opened it in binary mode, switch back to text mode. */ f = freopen (*opened_filename, "r", f); if (! f) return 0; #else /* Seek back over the magic bytes. */ if (fseek (f, 0, 0) < 0) return 0; #endif } return f; } /* Read all of file FILENAME into memory and return the address of the data. Store the size of the data into SIZEP. If need be, uncompress (i.e., try FILENAME.gz et al. if FILENAME does not exist) and store the actual file name that was opened into OPENED_FILENAME (if it is non-NULL), and the companion compression program (if any, else NULL) into COMPRESSION_PROGRAM (if that is non-NULL). If trouble, return a null pointer. */ char * readfile (char *filename, int *sizep, void (*create_callback) (char *), char **opened_filename, char **compression_program) { FILE *f; int filled = 0; int data_size = 8192; char *data = xmalloc (data_size + 1); /* If they passed the space for the file name to return, use it. */ f = open_possibly_compressed_file (filename, create_callback, opened_filename, compression_program); if (!f) return 0; for (;;) { int nread = fread (data + filled, 1, data_size - filled, f); if (nread < 0) return 0; if (nread == 0) break; filled += nread; if (filled == data_size) { data_size += 65536; data = xrealloc (data, data_size + 1); } } /* We need to close the stream, since on some systems the pipe created by popen is simulated by a temporary file which only gets removed inside pclose. */ if (compression_program && *compression_program) pclose (f); else fclose (f); *sizep = filled; data[filled] = '\0'; return data; } /* Output the old dir file, interpolating the new sections and/or new entries where appropriate. If COMPRESSION_PROGRAM is not null, pipe to it to create DIRFILE. Thus if we read dir.gz on input, we'll write dir.gz on output. */ static void output_dirfile (char *dirfile, int dir_nlines, struct line_data *dir_lines, int n_entries_to_add, struct spec_entry *entries_to_add, struct spec_section *input_sections, char *compression_program) { int n_entries_added = 0; int i; FILE *output; int tempfile; static char *tempname; int dirfile_len; mode_t um; /* Create temporary file in the same directory as dirfile. This ensures it is on the same disk volume and can be renamed to dirfile when finished. */ free (tempname); #define suffix "-XXXXXX" dirfile_len = strlen (dirfile); tempname = xmalloc (dirfile_len + strlen (suffix) + 1); memcpy (tempname, dirfile, dirfile_len); memcpy (tempname + dirfile_len, suffix, strlen (suffix) + 1); #undef suffix tempfile = mkstemp (tempname); /* Reset the mode that the file is set to. */ um = umask (0022); umask (um); if (chmod (tempname, 0666 & ~um) < 0) { remove (tempname); pfatal_with_name (tempname); } if (compression_program) { char *command; close (tempfile); command = concat (compression_program, ">", tempname); output = popen (command, "w"); free (command); } else output = fdopen (tempfile, "w"); if (!output) pfatal_with_name (dirfile); for (i = 0; i <= dir_nlines; i++) { int j; /* If we decided to output some new entries before this line, output them now. */ if (dir_lines[i].add_entries_before) for (j = 0; j < n_entries_to_add; j++) { struct spec_entry *this = dir_lines[i].add_entries_before[j]; if (this == 0) break; if (n_entries_added >= 1 && !add_entries_into_all_matching_sections) break; fputs (this->text, output); n_entries_added++; } /* If we decided to add some sections here because there are no such sections in the file, output them now. FIXME: we add all sections here, but they should be interspersed throughout the DIR file in alphabetic order. */ if (dir_lines[i].add_sections_before) { struct spec_section *spec; struct spec_entry *entry; struct spec_entry **entries; int n_entries = 0; /* If we specified --add-once, and we've added an entry, then it's time to bail. */ if (n_entries_added >= 1 && !add_entries_into_all_matching_sections) break; qsort (dir_lines[i].add_sections_before, dir_lines[i].num_sections_to_add, sizeof (struct spec_section *), compare_section_names); /* Count the entries and allocate a vector for all of them. */ for (entry = entries_to_add; entry; entry = entry->next) n_entries++; entries = ((struct spec_entry **) xmalloc (n_entries * sizeof (struct spec_entry *))); /* Fill the vector ENTRIES with pointers to all the sections, and sort them. */ j = 0; for (entry = entries_to_add; entry; entry = entry->next) entries[j++] = entry; qsort (entries, n_entries, sizeof (struct spec_entry *), compare_entries_text); /* Generate the new sections in alphabetical order. In each new section, output all of the entries that belong to that section, in alphabetical order. */ for (j = 0; j < dir_lines[i].num_sections_to_add; j++) { spec = dir_lines[i].add_sections_before[j]; if (spec->missing) { int k; /* If the preceding line is not blank, add a blank line */ if (i >= 1 && dir_lines[i - 1].size > 0) putc ('\n', output); fputs (spec->name, output); putc ('\n', output); spec->missing = 0; for (k = 0; k < n_entries; k++) { struct spec_section *spec1; /* Did they at all want this entry to be put into this section? */ entry = entries[k]; for (spec1 = entry->entry_sections; spec1 && spec1 != entry->entry_sections_tail; spec1 = spec1->next) { if (!strcmp (spec1->name, spec->name)) break; } if (spec1 && spec1 != entry->entry_sections_tail) fputs (entry->text, output); } } } n_entries_added++; free (entries); } /* Output the original dir lines unless marked for deletion. */ if (i < dir_nlines && !dir_lines[i].delete) { fwrite (dir_lines[i].start, 1, dir_lines[i].size, output); putc ('\n', output); } } /* Some systems, such as MS-DOS, simulate pipes with temporary files. On those systems, the compressor actually gets run inside pclose, so we must call pclose. */ if (compression_program) pclose (output); else fclose (output); /* Update dir file atomically. This stops the dir file being corrupted if install-info is interrupted. */ if (rename (tempname, dirfile) == -1) { /* Try to delete target file and try again. On some platforms you may not rename to an existing file. */ if (remove (dirfile) == -1 || rename (tempname, dirfile) == -1) { remove (tempname); pfatal_with_name (dirfile); } } } /* Read through the input LINES, to find the section names and the entry names it specifies. Each INFO-DIR-SECTION entry is added to the SECTIONS linked list. Each START-INFO-DIR-ENTRY block is added to the ENTRIES linked list, and the last group of INFO-DIR-SECTION entries is recorded in next->entry_sections and next->entry_sections_tail, where next is the new entry. Return the number of entries to add from this file. */ int parse_input (const struct line_data *lines, int nlines, struct spec_section **sections, struct spec_entry **entries, int delete_flag) { int n_entries = 0; int prefix_length = strlen ("INFO-DIR-SECTION "); struct spec_section *head = *sections, *tail = NULL; int reset_tail = 0; char *start_of_this_entry = 0; int ignore_sections = *sections != 0; int ignore_entries = delete_flag ? 0: *entries != 0; int i; if (ignore_sections && ignore_entries) return 0; for (i = 0; i < nlines; i++) { if (!ignore_sections && !strncmp ("INFO-DIR-SECTION ", lines[i].start, prefix_length)) { struct spec_section *next = (struct spec_section *) xmalloc (sizeof (struct spec_section)); next->name = copy_string (lines[i].start + prefix_length, lines[i].size - prefix_length); next->next = *sections; next->missing = 1; if (reset_tail) { tail = *sections; reset_tail = 0; } *sections = next; head = *sections; } /* If entries were specified explicitly with command options, ignore the entries in the input file. */ else if (!ignore_entries) { if (!strncmp ("START-INFO-DIR-ENTRY", lines[i].start, lines[i].size) && sizeof ("START-INFO-DIR-ENTRY") - 1 == lines[i].size) { if (!*sections) { /* We found an entry, but didn't yet see any sections specified. Default to section "Miscellaneous". */ *sections = (struct spec_section *) xmalloc (sizeof (struct spec_section)); (*sections)->name = "Miscellaneous"; (*sections)->next = 0; (*sections)->missing = 1; head = *sections; } /* Next time we see INFO-DIR-SECTION, we will reset the tail pointer. */ reset_tail = 1; /* Save start of the entry. If this is non-zero, we're already inside an entry, so fail. */ if (start_of_this_entry != 0) fatal (_("START-INFO-DIR-ENTRY without matching END-INFO-DIR-ENTRY")); start_of_this_entry = lines[i + 1].start; } else if (start_of_this_entry) { if ((!strncmp ("* ", lines[i].start, 2) && lines[i].start > start_of_this_entry) || (!strncmp ("END-INFO-DIR-ENTRY", lines[i].start, lines[i].size) && sizeof ("END-INFO-DIR-ENTRY") - 1 == lines[i].size)) { /* We found the end of this entry. Save its contents in a new entry in the linked list. */ struct spec_entry *next = (struct spec_entry *) xmalloc (sizeof (struct spec_entry)); next->text = copy_string (start_of_this_entry, lines[i].start - start_of_this_entry); next->text_len = lines[i].start - start_of_this_entry; next->entry_sections = head; next->entry_sections_tail = tail; next->missing_basename = 0; next->missing_name = 0; next->missing_description = 0; next->next = *entries; *entries = next; n_entries++; if (!strncmp ("END-INFO-DIR-ENTRY", lines[i].start, lines[i].size) && sizeof ("END-INFO-DIR-ENTRY") - 1 == lines[i].size) start_of_this_entry = 0; else start_of_this_entry = lines[i].start; } else if (!strncmp ("END-INFO-DIR-ENTRY", lines[i].start, lines[i].size) && sizeof ("END-INFO-DIR-ENTRY") - 1 == lines[i].size) fatal (_("END-INFO-DIR-ENTRY without matching START-INFO-DIR-ENTRY")); } } } if (start_of_this_entry != 0) fatal (_("START-INFO-DIR-ENTRY without matching END-INFO-DIR-ENTRY")); /* If we ignored the INFO-DIR-ENTRY directives, we need now go back and plug the names of all the sections we found into every element of the ENTRIES list. */ if (ignore_entries && *entries) { struct spec_entry *entry; for (entry = *entries; entry; entry = entry->next) { entry->entry_sections = head; entry->entry_sections_tail = tail; } } return n_entries; } /* Parse the dir file whose basename is BASE_NAME. Find all the nodes, and their menus, and the sections of their menus. */ static void parse_dir_file (struct line_data *lines, int nlines, struct node **nodes) { int node_header_flag = 0; int i; *nodes = 0; for (i = 0; i < nlines; i++) { /* Parse node header lines. */ if (node_header_flag) { int j, end; for (j = 0; j < lines[i].size; j++) /* Find the node name and store it in the `struct node'. */ if (!strncmp ("Node:", lines[i].start + j, 5)) { char *line = lines[i].start; /* Find the start of the node name. */ j += 5; while (line[j] == ' ' || line[j] == '\t') j++; /* Find the end of the node name. */ end = j; while (line[end] != 0 && line[end] != ',' && line[end] != '\n' && line[end] != '\t') end++; (*nodes)->name = copy_string (line + j, end - j); } node_header_flag = 0; } /* Notice the start of a node. */ if (*lines[i].start == 037) { struct node *next = (struct node *) xmalloc (sizeof (struct node)); next->next = *nodes; next->name = NULL; next->start_line = i; next->end_line = 0; next->menu_start = NULL; next->sections = NULL; next->last_section = NULL; if (*nodes != 0) (*nodes)->end_line = i; /* Fill in the end of the last menu section of the previous node. */ if (*nodes != 0 && (*nodes)->last_section != 0) (*nodes)->last_section->end_line = i; *nodes = next; /* The following line is the header of this node; parse it. */ node_header_flag = 1; } /* Notice the lines that start menus. */ if (*nodes != 0 && !strncmp ("* Menu:", lines[i].start, 7)) (*nodes)->menu_start = lines[i + 1].start; /* Notice sections in menus. */ if (*nodes != 0 && (*nodes)->menu_start != 0 && *lines[i].start != '\n' && *lines[i].start != '*' && *lines[i].start != ' ' && *lines[i].start != '\t') { /* Add this menu section to the node's list. This list grows in forward order. */ struct menu_section *next = (struct menu_section *) xmalloc (sizeof (struct menu_section)); next->start_line = i + 1; next->next = 0; next->end_line = 0; next->name = copy_string (lines[i].start, lines[i].size); if ((*nodes)->sections) { (*nodes)->last_section->next = next; (*nodes)->last_section->end_line = i; } else (*nodes)->sections = next; (*nodes)->last_section = next; } } /* Finish the info about the end of the last node. */ if (*nodes != 0) { (*nodes)->end_line = nlines; if ((*nodes)->last_section != 0) (*nodes)->last_section->end_line = nlines; } } /* Iterate through NLINES LINES looking for an entry that has a name that matches NAME. If such an entry is found, flag the entry for deletion later on. */ int mark_entry_for_deletion (struct line_data *lines, int nlines, char *name) { int something_deleted = 0; int i; for (i = 0; i < nlines; i++) { /* Check for an existing entry that should be deleted. Delete all entries which specify this file name. */ if (*lines[i].start == '*') { char *q; char *p = lines[i].start; p++; /* skip * */ while (*p == ' ') p++; /* ignore following spaces */ q = p; /* remember this, it's the beginning of the menu item. */ /* Read menu item. */ while (*p != 0 && *p != ':') p++; if (!*p) continue; p++; /* skip : */ if (!*p) continue; else if (*p == ':') { /* XEmacs-style entry, as in * Mew::Messaging. */ if (menu_item_equal (q, ':', name)) { lines[i].delete = 1; something_deleted = 1; } } else { /* Emacs-style entry, as in * Emacs: (emacs). */ while (*p == ' ') p++; /* skip spaces after : */ if (*p == '(') /* if at parenthesized (FILENAME) */ { p++; if (*p && menu_item_equal (p, ')', name)) { lines[i].delete = 1; something_deleted = 1; } } } } /* Treat lines that start with whitespace as continuations; if we are deleting an entry, delete all its continuations as well. */ else if (i > 0 && (*lines[i].start == ' ' || *lines[i].start == '\t')) { lines[i].delete = lines[i - 1].delete; } } return something_deleted; } /* Assuming the current column is COLUMN, return the column that printing C will move the cursor to. The first column is 0. This function is used to assist in indenting of entries. */ static size_t adjust_column (size_t column, char c) { if (c == '\b') { if (column > 0) column--; } else if (c == '\r') column = 0; else if (c == '\t') column += TAB_WIDTH - column % TAB_WIDTH; else /* if (isprint (c)) */ column++; return column; } /* Format the Info entry's NAME and DESCRIPTION. NAME is of the form "* TEXT (TEXT)[:TEXT].". The description on the first line is indented at the CALIGN-th column, and all subsequent lines are indented at the ALIGN-th column. Lines are wrapped at the WIDTH column. The resulting Info entry is put into OUTSTR. */ static int format_entry (char *name, size_t name_len, char *desc, size_t desc_len, int calign, int align, size_t width, char **outstr_out, size_t *outstr_len) { int i, j; char c; size_t column = 0; /* Screen column where next char will go */ /* Used to collect a line at a time, before transferring to outstr. */ static char *line_out = NULL; size_t offset_out = 0; /* Index in `line_out' for next char. */ static size_t allocated_out = 0; /* Space allocated in `line_out'. */ char *outstr; if (!desc || !name) return 1; outstr = xmalloc (width + (desc_len + width) / (width - align) * width * 2 * sizeof (char)); outstr[0] = '\0'; strncat (outstr, name, name_len); column = name_len; if (name_len > calign - 2) { /* Name is too long to have description on the same line. */ if (desc_len > 1) { strcat (outstr, "\n"); column = 0; for (j = 0; j < calign - 1; j++) { column = adjust_column (column, ' '); strcat (outstr, " "); } } } else for (j = 0; j < calign - name_len - 1; j++) { if (desc_len <= 2) break; column = adjust_column (column, ' '); strcat (outstr, " "); } for (i = 0; i < desc_len; i++) { if (desc_len <= 2) break; c = desc[i]; if (offset_out + 1 >= allocated_out) { allocated_out = offset_out + 1; line_out = (char *) xrealloc ((void *)line_out, allocated_out + 1); /* The + 1 here shouldn't be necessary, but a crash was reported for a following strncat call. */ } if (c == '\n') { line_out[offset_out++] = c; strncat (outstr, line_out, offset_out); column = offset_out = 0; continue; } /* Come here from inside "column > width" block below. */ rescan: column = adjust_column (column, c); if (column > width) { /* This character would make the line too long. Print the line plus a newline, and make this character start the next line. */ int found_blank = 0; size_t logical_end = offset_out; /* Look for the last blank. */ while (logical_end) { --logical_end; if (line_out[logical_end] == ' ' || line_out[logical_end] == '\t') { found_blank = 1; break; } } if (found_blank) { size_t i; /* Found a blank. Don't output the part after it. */ logical_end++; strncat (outstr, line_out, logical_end); strcat (outstr, "\n"); for (j = 0; j < align - 1; j++) { column = adjust_column (column, ' '); strcat (outstr, " "); } /* Move the remainder to the beginning of the next line. The areas being copied here might overlap. */ memmove (line_out, line_out + logical_end, offset_out - logical_end); offset_out -= logical_end; for (column = i = 0; i < offset_out; i++) column = adjust_column (column, line_out[i]); goto rescan; } if (offset_out == 0) { line_out[offset_out++] = c; continue; } line_out[offset_out++] = '\n'; strncat (outstr, line_out, offset_out); column = offset_out = 0; goto rescan; } line_out[offset_out++] = c; } if (desc_len <= 2) strcat (outstr, "\n"); if (offset_out) strncat (outstr, line_out, offset_out); free (*outstr_out); *outstr_out = outstr; *outstr_len = strlen (outstr); return 1; } /* Extract the NAME and DESCRIPTION from ENTRY. NAME and DESCRIPTION must be free'd. */ static void split_entry (const char *entry, char **name, size_t *name_len, char **description, size_t *description_len) { char *endptr; /* on the first line, the description starts after the first ". "; that's a period and space -- our heuristic to handle item names like "config.status", and node names like "config.status Invocation". Also accept period-tab and period-newline. */ char *ptr = strchr (entry, '.'); while (ptr && ptr[1] != ' ' && ptr[1] != '\t' && ptr[1] != '\n') { ptr = strchr (ptr + 1, '.'); } /* Maybe there's no period, and no description */ if (!ptr) { size_t length = strlen (entry); if (length == 0) return; *name = strdup (entry); *name_len = length + 1; return; } /* The name is everything up to and including the period. */ *name_len = (size_t) (ptr - entry + 1); *name = xmalloc (*name_len + 1); (*name)[0] = '\0'; strncat (*name, entry, *name_len); ptr++; *description = xmalloc (strlen (entry)); (*description)[0] = '\0'; while (ptr[0] != '\0') { /* Eat up the whitespace after the name, and at the start of a line. */ while (isspace(ptr[0])) ptr++; /* Okay, we're at the start of the description. */ if (ptr[0] == '\0') continue; /* See how far the description goes... */ endptr = strchr (ptr, '\n'); /* Either the description continues up to the next newline. */ if (endptr) { size_t length = (size_t) (endptr - ptr) / sizeof (char); strncat (*description, ptr, length); ptr = endptr; /* First of all, we eat the newline here. But then what? Sometimes the newline separates 2 sentences, so we end up with the next word starting directly after the period, instead of after the customary 2 spaces in english. If the previous character was a `.', then we should add 2 spaces if there is anything on the next line. if it's a comma, then we should put one space. If it's neither, we just put a space. If it's some other whitespace, we shouldn't do anything. */ ptr++; if (length > 1 && strlen (ptr) > 0) { endptr--; /* *ENDPTR is the 2nd last character */ if (*endptr == '.') strcat (*description, " "); else if (!isspace (*endptr)) strcat (*description, " "); } } /* Or the description continues to the end of the string. */ else { /* Just show the rest when there's no newline. */ strcat (*description, ptr); ptr += strlen (ptr); } } /* Descriptions end in a new line. */ strcat (*description, "\n"); *description_len = strlen (*description); } /* Indent all ENTRIES according to some formatting options. CALIGN_CLI is the starting column for the first line of the description. ALIGN_CLI is the starting column for all subsequent lines of the description. MAXWIDTH_CLI is the number of columns in the line. When CALIGN_CLI, ALIGN_CLI, or MAXWIDTH_CLI is -1, choose a sane default. */ static void reformat_new_entries (struct spec_entry *entries, int calign_cli, int align_cli, int maxwidth_cli) { struct spec_entry *entry; for (entry = entries; entry; entry = entry->next) { int calign = -1, align = -1, maxwidth = -1; char *name = NULL, *desc = NULL; size_t name_len = 0, desc_len = 0; split_entry (entry->text, &name, &name_len, &desc, &desc_len); /* Specify sane defaults if we need to */ if (calign_cli == -1 || align_cli == -1) { struct spec_section *section; calign = calign_cli; align = align_cli; for (section = entry->entry_sections; section && section != entry->entry_sections_tail; section = section->next) { if (!strcmp (section->name, "Individual utilities")) { if (calign == -1) calign = 48 + 1; if (align == -1) align = 50 + 1; break; } } if (calign == -1) calign = 32 + 1; if (align == -1) align = 34 + 1; } else { calign = calign_cli; align = align_cli; } maxwidth = maxwidth_cli == -1 ? 79 : maxwidth_cli; format_entry (name, name_len, desc, desc_len, calign, align, maxwidth, &entry->text, &entry->text_len); free (name); free (desc); } } /* Insert NAME into every entry in ENTRIES that requires it. NAME is the basename of the Info file being installed. The idea here is that there was a --name on the command-line and we need to put the basename in the empty parentheses. */ void add_missing_basenames (struct spec_entry *entries, char *name) { struct spec_entry *entry; for (entry = entries; entry; entry = entry->next) { if (entry->missing_basename) { /* Insert NAME into the right place in ENTRY->TEXT. */ char *info, *rest, *text; size_t name_len = strlen (name); char *ptr = strstr (entry->text, ": (). "); if (!ptr) return; ptr[0] = '\0'; rest = ptr += strlen (": (). "); info = xmalloc (name_len + 7); snprintf (info, name_len + 7, ": (%s). ", name); text = concat (entry->text, info, rest); free (info); if (entry->text) free (entry->text); entry->text = text; entry->text_len = strlen (entry->text); entry->missing_name = 0; entry->missing_basename = 0; } } } /* Add NAME to the start of any entry in ENTRIES that is missing a name component. If NAME doesn't start with `*', it is formatted to look like an Info entry. */ void add_missing_names (struct spec_entry *entries, char *name) { struct spec_entry *entry; for (entry = entries; entry; entry = entry->next) { if (entry->missing_name) { char *text; /* Prepend NAME onto ENTRY->TEXT. */ int add_nl = 1; if (entry->text) if (entry->text[entry->text_len - 1] == '\n') add_nl = 0; if (name[0] == '*') text = concat (name, entry->text == NULL ? "" : entry->text, add_nl ? "\n" : ""); else { size_t full_name_len = strlen (name) * 2 + 9; char *full_name = xmalloc (full_name_len); snprintf (full_name, full_name_len, "* %s: (%s).", name, name); text = concat (full_name, entry->text == NULL ? "" : entry->text, add_nl ? "\n" : ""); free (full_name); } if (entry->text) free (entry->text); entry->text = text; entry->text_len = strlen (entry->text); entry->missing_name = 0; entry->missing_basename = 0; } } } /* Append DESC to every entry in ENTRIES that needs it. */ void add_missing_descriptions (struct spec_entry *entries, char *desc) { struct spec_entry *entry; for (entry = entries; entry; entry = entry->next) { if (entry->missing_description) { char *text; int add_nl = 1; if (strlen (desc) > 1) if (desc[strlen (desc) - 1] == '\n') add_nl = 0; /* Append DESC onto ENTRY->TEXT. */ text = concat (entry->text == NULL ? "" : entry->text, desc, add_nl ? "\n" : ""); if (entry->text) free (entry->text); entry->text = text; entry->text_len = strlen (entry->text); } } } /* Detect old-style Debian `--section REGEX TITLE' semantics in ARGV. When detected the options are munged to look like: `--regex REGEX --section TITLE --add-once' Return 1 if munging took place, return 0 if not. Otherwise return a negative number if something went wrong. NEW_ARGC, and NEW_ARGV are filled with the newly munged options when munging took place. */ static int munge_old_style_debian_options (int argc, char **argv, int *new_argc, char ***new_argv) { char *opt = NULL; int i, err; char *argz = NULL; size_t argz_len = 0; const char *regex, *title; int munge = 0; /* Flip through the options to detect the old `--section REGEX TITLE' syntax */ for (i = 0; i < argc; i++) { if (strcmp (argv[i], "--section") == 0) { FILE *fileptr; /* Go forward one arg and obtain the REGEX. */ if (i + 1 < argc) i++; else return -1; regex = argv[i]; /* Go forward another arg and obtain the TITLE. */ if (i + 1 < argc) i++; else return -1; title = argv[i]; /* When the title starts with a `-' it's probably an option, and not a title. */ if (title[0] == '-') break; /* When the title is a filename it's probably an Info file, or a dir file, and not a title. */ fileptr = fopen (title, "r"); if (fileptr) { fclose (fileptr); break; } /* Okay, it looks like we're using the old debian syntax for --section. */ munge = 1; /* Okay, we munge the options to look like this: --regex=REGEX --section=TITLE --add-once */ opt = xmalloc (strlen (regex) + sizeof ("--regex=")); if (sprintf (opt, "--regex=%s", regex) == -1) err = 1; if (!err) err = argz_add (&argz, &argz_len, opt); free (opt); opt = NULL; opt = xmalloc (strlen (title) + sizeof ("--section=")); if (sprintf (opt, "--section=%s", title) == -1) err = 1; if (!err) err = argz_add (&argz, &argz_len, opt); free (opt); opt = NULL; if (!err) err = argz_add (&argz, &argz_len, "--add-once"); } else err = argz_add (&argz, &argz_len, argv[i]); if (err) return -1; } if (munge) { *new_argc = argz_count (argz, argz_len); *new_argv = xmalloc ((*new_argc + 1) * sizeof (char *)); opt = NULL; i = 0; while ((opt = argz_next (argz, argz_len, opt))) { (*new_argv)[i] = xstrdup (opt); i++; } (*new_argv)[*new_argc] = NULL; } free (argz); return munge; } int main (int argc, char *argv[]) { char *opened_dirfilename; char *compression_program; char *infile_sans_info; char *infile = 0, *dirfile = 0; int calign = -1; int align = -1; int maxwidth = -1; /* Record the text of the Info file, as a sequence of characters and as a sequence of lines. */ char *input_data = NULL; int input_size = 0; struct line_data *input_lines = NULL; int input_nlines = 0; /* Record here the specified section names and directory entries. */ struct spec_section *input_sections = NULL; struct spec_entry *entries_to_add = NULL; struct spec_entry *entries_to_add_from_file = NULL; int n_entries_to_add = 0; struct spec_entry *default_entries_to_add = NULL; int n_default_entries_to_add = 0; /* Record the old text of the dir file, as plain characters, as lines, and as nodes. */ char *dir_data; int dir_size; int dir_nlines; struct line_data *dir_lines; struct node *dir_nodes; /* Nonzero means --delete was specified (just delete existing entries). */ int delete_flag = 0; int something_deleted = 0; /* Nonzero means -quiet/--silent was specified. */ int quiet_flag = 0; /* Nonzero means --debug was specified. */ int debug_flag = 0; int i; #ifdef HAVE_SETLOCALE /* Set locale via LC_ALL. */ setlocale (LC_ALL, ""); #endif /* Set the text message domain. */ bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); /* Make sure standard input can be freopened at will. Otherwise, when stdin starts off closed, bad things could happen if a plain fopen returns stdin before open_possibly_compressed_file freopens it. */ if (! freopen (NULL_DEVICE, "r", stdin)) pfatal_with_name (NULL_DEVICE); munge_old_style_debian_options (argc, argv, &argc, &argv); while (1) { int opt = getopt_long (argc, argv, "i:d:e:s:t:E:c:C:W:A:hHrk1Ia", longopts, 0); if (opt == EOF) break; switch (opt) { case 0: /* If getopt returns 0, then it has already processed a long-named option. We should do nothing. */ break; case 1: abort (); case '1': add_entries_into_all_matching_sections = 0; break; case 'a': order_new_sections_alphabetically_flag = 0; break; case 'A': { char *end = NULL; unsigned long int val; val = strtoul (optarg, &end, 0); if (end == NULL || end == optarg || *end != '\0') suggest_asking_for_help (); align = val; if (align <= 0) suggest_asking_for_help (); } break; case 'c': { struct spec_entry *next; size_t length = strlen (optarg); char *old_text; if (!entries_to_add) { next = (struct spec_entry *) xmalloc (sizeof (struct spec_entry)); next->text = NULL; next->text_len = 0; next->entry_sections = NULL; next->entry_sections_tail = NULL; next->missing_name = 1; next->missing_basename = 1; next->next = entries_to_add; entries_to_add = next; n_entries_to_add++; } else next = entries_to_add; next->missing_description = 0; if (next->text) { char *nl = strrchr (next->text, '\n'); if (nl) nl[0] = '\0'; } /* Concat the description onto the current entry, adding a newline if we need one. Prepend a space if we have no previous text, since eventually we will be adding the "* foo ()." and we want to end up with a ". " for parsing. */ old_text = next->text; next->text = concat (old_text ? old_text : " ", optarg, optarg[length - 1] == '\n' ? "" : "\n"); free (old_text); next->text_len = strlen (next->text); } break; case 'C': { char *end = NULL; unsigned long int val; val = strtoul (optarg, &end, 0); if (end == NULL || end == optarg || *end != '\0') suggest_asking_for_help (); calign = val; if (calign <= 0) suggest_asking_for_help (); } break; case 'd': if (dirfile) { fprintf (stderr, _("%s: already have dir file: %s\n"), progname, dirfile); suggest_asking_for_help (); } dirfile = optarg; break; case 'D': if (dirfile) { fprintf (stderr, _("%s: already have dir file: %s\n"), progname, dirfile); suggest_asking_for_help (); } dirfile = concat (optarg, "", "/dir"); break; case 't': { struct spec_entry *next = (struct spec_entry *) xmalloc (sizeof (struct spec_entry)); size_t length; if (optarg[0] != '*') { /* Make enough space for "* foo: (). ". */ length = strlen (optarg) + 9; next->text = xmalloc (length); snprintf (next->text, length, "* %s: (). ", optarg); next->missing_basename = 1; /* The basename will be inserted in between the parentheses at a later time. See add_missing_basenames. */ } else { /* Make enough space for "foo ". */ length = strlen (optarg) + 2; next->text = xmalloc (length); snprintf (next->text, length, "%s ", optarg); next->missing_basename = 0; /* FIXME: check for info entry correctness in TEXT. e.g. `* Aaa: (bbb).' */ } next->text_len = length - 1; next->entry_sections = NULL; next->entry_sections_tail = NULL; next->next = entries_to_add; next->missing_name = 0; next->missing_description = 1; entries_to_add = next; n_entries_to_add++; } break; case 'E': case 'e': { struct spec_entry *next = (struct spec_entry *) xmalloc (sizeof (struct spec_entry)); int olen = strlen (optarg); if (! (*optarg != 0 && optarg[olen - 1] == '\n')) { optarg = concat (optarg, "\n", ""); olen++; } next->text = optarg; next->text_len = olen; next->entry_sections = NULL; next->entry_sections_tail = NULL; next->missing_name = 0; next->missing_basename = 0; next->missing_description = 0; if (opt == 'e') { next->next = entries_to_add; entries_to_add = next; n_entries_to_add++; } else { /* Although this list is maintained, nothing is ever done with it. So it seems no one cares about the feature. The intended --help string was: --defentry=TEXT like --entry, but only use TEXT if an entry\n\ is not present in INFO-FILE.\n\ in case anyone ever wants to finish it. */ next->next = default_entries_to_add; default_entries_to_add = next; n_default_entries_to_add++; } } break; case 'g': debug_flag = 1; break; case 'h': case 'H': print_help (); exit (EXIT_SUCCESS); case 'i': if (infile) { fprintf (stderr, _("%s: Specify the Info file only once.\n"), progname); suggest_asking_for_help (); } infile = optarg; break; case 'I': indent_flag = 0; break; case 'k': keep_old_flag = 1; break; case 'n': chicken_flag = 1; break; case 'q': quiet_flag = 1; break; case 'r': delete_flag = 1; break; case 'R': { int error; if (psecreg) { warning (_("Extra regular expression specified, ignoring `%s'"), optarg); break; } psecreg = (regex_t *) xmalloc (sizeof (regex_t)); error = regcomp (psecreg, optarg, REG_ICASE|REG_NOSUB); if (error != 0) { int errbuf_size = regerror (error, psecreg, NULL, 0); char *errbuf = (char *) xmalloc (errbuf_size); regerror (error, psecreg, errbuf, errbuf_size); fatal (_("Error in regular expression `%s': %s"), optarg, errbuf); }; } break; case 'S': default_section = optarg; break; case 's': { struct spec_section *next = (struct spec_section *) xmalloc (sizeof (struct spec_section)); next->name = optarg; next->next = input_sections; next->missing = 1; input_sections = next; } break; case 'V': printf ("install-info (GNU %s) %s\n", PACKAGE, VERSION); puts (""); printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ License GPLv3+: GNU GPL version 3 or later \n\ This is free software: you are free to change and redistribute it.\n\ There is NO WARRANTY, to the extent permitted by law.\n"), "2023"); exit (EXIT_SUCCESS); case 'W': { char *end = NULL; unsigned long int val; val = strtoul (optarg, &end, 0); if (end == NULL || end == optarg || *end != '\0') suggest_asking_for_help (); maxwidth = val; if (maxwidth <= 0) suggest_asking_for_help (); } break; case 'x': delete_flag = 1; remove_exactly = 1; break; default: suggest_asking_for_help (); } } /* Interpret the non-option arguments as file names. */ for (; optind < argc; ++optind) { if (infile == 0) infile = argv[optind]; else if (dirfile == 0) dirfile = argv[optind]; else error (_("excess command line argument `%s'"), argv[optind]); } if (!infile) fatal (_("No input file specified; try --help for more information.")); if (!dirfile) fatal (_("No dir file specified; try --help for more information.")); /* Now read in the Info dir file. */ if (debug_flag) printf ("debug: reading dir file %s\n", dirfile); if (!delete_flag) { dir_data = readfile (dirfile, &dir_size, ensure_dirfile_exists, &opened_dirfilename, &compression_program); if (!dir_data) pfatal_with_name (opened_dirfilename); } else { /* For "--remove" operation, it is not an error for the dir file not to exist. */ dir_data = readfile (dirfile, &dir_size, NULL, &opened_dirfilename, &compression_program); if (!dir_data) { warning (_("Could not read %s."), opened_dirfilename); exit (EXIT_SUCCESS); } } dir_lines = findlines (dir_data, dir_size, &dir_nlines); parse_dir_file (dir_lines, dir_nlines, &dir_nodes); if (!delete_flag) { /* Find which sections match our regular expression. */ if (psecreg) { struct node *node; struct menu_section *section; for (node = dir_nodes; node ; node = node->next) for (section = node->sections; section ; section = section->next) if (regexec (psecreg, section->name, 0, NULL, 0) == 0) { /* we have a match! */ struct spec_section *next = (struct spec_section *) xmalloc (sizeof (struct spec_section)); next->name = section->name; next->next = input_sections; next->missing = 0; input_sections = next; } } } /* We will be comparing the entries in the dir file against the current filename, so need to strip off any directory prefix and/or [.info][.gz] suffix. */ if (!remove_exactly) { char *infile_basename = infile + strlen (infile); if (HAVE_DRIVE (infile)) infile += 2; /* get past the drive spec X: */ while (infile_basename > infile && !IS_SLASH (infile_basename[-1])) infile_basename--; infile_sans_info = strip_info_suffix (infile_basename); } else infile_sans_info = xstrdup(infile); /* Now Read the Info file and parse it into lines, unless we're removing exactly. */ if (!remove_exactly) { char *opened_infilename; if (debug_flag) printf ("debug: reading input file %s\n", infile); input_data = readfile (infile, &input_size, NULL, &opened_infilename, NULL); if (!input_data) pfatal_with_name (opened_infilename); input_lines = findlines (input_data, input_size, &input_nlines); } i = parse_input (input_lines, input_nlines, &input_sections, &entries_to_add_from_file, delete_flag); if (!delete_flag) { /* If there are no entries on the command-line at all, use the entries found in the Info file itself (if any). */ if (entries_to_add == NULL) { entries_to_add = entries_to_add_from_file; n_entries_to_add = i; } /* There are entries on the command-line, and they override the entries found in the Info file. */ else if (entries_to_add) { if (entries_to_add_from_file == NULL) { /* No entries found in the file anyway. Fill in any missing names with the info file's basename. We're out of luck for any missing descriptions. */ add_missing_names (entries_to_add, infile_sans_info); /* add_missing_descriptions (entries_to_add, "\n"); */ } else { /* Fill in any missing names or descriptions with what was found in the Info file. */ char *desc = NULL; size_t desc_len = 0; char *name = NULL; size_t name_len = 0; split_entry (entries_to_add_from_file->text, &name, &name_len, &desc, &desc_len); if (name) { /* If the name doesn't look right, bail and use the name based on the Info file. */ if (name[0] != '*') add_missing_names (entries_to_add, infile_sans_info); else add_missing_names (entries_to_add, name); free (name); } if (desc) { add_missing_descriptions (entries_to_add, desc); free (desc); } } } /* Lastly, fill in any missing basenames that might still be hanging around from --name options on the command-line. */ add_missing_basenames (entries_to_add, infile_sans_info); /* Reformat the new entries if we're doing that. */ if (indent_flag) { char *no_indent = getenv ("INSTALL_INFO_NO_INDENT"); if (!no_indent) reformat_new_entries (entries_to_add, calign, align, maxwidth); } /* If we got no sections, use the --defsection value if it was given, else "Miscellaneous". */ if (input_sections == NULL) { input_sections = (struct spec_section *) xmalloc (sizeof (struct spec_section)); input_sections->name = default_section ? default_section : "Miscellaneous"; input_sections->next = NULL; input_sections->missing = 1; } if (entries_to_add == 0) { /* No need to abort here, the original info file may not have the requisite Texinfo commands. This is not something an installer should have to correct (it's a problem for the maintainer), and there's no need to cause subsequent parts of `make install' to fail. */ if (!quiet_flag) warning (_("no info dir entry in `%s'"), infile); exit (EXIT_SUCCESS); } /* If the entries came from the command-line arguments, their entry_sections pointers are not yet set. Walk the chain of the entries and for each entry update entry_sections to point to the head of the list of sections where this entry should be put. Note that all the entries specified on the command line get put into ALL the sections we've got, either from the Info file, or (under --section) from the command line, because in the loop below every entry inherits the entire chain of sections. */ if (n_entries_to_add > 0 && entries_to_add->entry_sections == NULL) { struct spec_entry *ep; for (ep = entries_to_add; ep; ep = ep->next) ep->entry_sections = input_sections; } } if (delete_flag) { something_deleted = mark_entry_for_deletion (dir_lines, dir_nlines, infile_sans_info); if (!something_deleted && !remove_exactly) { struct spec_entry *entry; for (entry = entries_to_add; entry; entry = entry->next) { /* If the entry came from the info file... */ if (entry->entry_sections != NULL) { char *name = extract_menu_item_name (entry->text); something_deleted = mark_entry_for_deletion (dir_lines, dir_nlines, name); free (name); } } if (!something_deleted) { struct spec_entry *entry; for (entry = entries_to_add; entry; entry = entry->next) { /* If the entry came from the command-line... */ if (entry->entry_sections == NULL) something_deleted = mark_entry_for_deletion (dir_lines, dir_nlines, entry->text); } } } } /* Check for sections with zero entries and mark them for deletion. */ if (delete_flag && something_deleted && !keep_old_flag) { struct node *node; struct menu_section *section; int section_empty; for (node = dir_nodes; node ; node = node->next) for (section = node->sections; section ; section = section->next) { section_empty = 1; for (i = section->end_line; i > section->start_line; i--) { if (dir_lines[i - 1].delete == 0 && dir_lines[i - 1].size != 0) { section_empty = 0; break; } } if (section_empty) { /* This gets rid of any trailing empty lines at the end of the section, and the title too. */ for (i = section->end_line; i >= section->start_line; i--) dir_lines[i - 1].delete = 1; } } } /* Decide where to add the new entries (unless --delete was used). Find the menu sections to add them in. In each section, find the proper alphabetical place to add each of the entries. */ if (!delete_flag) { struct node *node; struct menu_section *section; struct spec_section *spec; for (node = dir_nodes; node; node = node->next) for (section = node->sections; section; section = section->next) { for (i = section->end_line; i > section->start_line; i--) if (dir_lines[i - 1].size != 0) break; section->end_line = i; for (spec = input_sections; spec; spec = spec->next) if (!strcmp (spec->name, section->name)) break; if (spec) { int add_at_line = section->end_line; struct spec_entry *entry; /* Say we have found at least one section with this name, so we need not add such a section. */ spec->missing = 0; /* For each entry, find the right place in this section to add it. */ for (entry = entries_to_add; entry; entry = entry->next) { /* Did they at all want this entry to be put into this section? */ for (spec = entry->entry_sections; spec && spec != entry->entry_sections_tail; spec = spec->next) { if (!strcmp (spec->name, section->name)) break; } if (!spec || spec == entry->entry_sections_tail) continue; /* Subtract one because dir_lines is zero-based, but the `end_line' and `start_line' members are one-based. */ for (i = section->end_line - 1; i >= section->start_line - 1; i--) { /* If an entry exists with the same name, and was not marked for deletion (which means it is for some other file), we are in trouble. */ if (dir_lines[i].start[0] == '*' && menu_line_equal (entry->text, entry->text_len, dir_lines[i].start, dir_lines[i].size) && !dir_lines[i].delete) { if (keep_old_flag) { add_at_line = -1; break; } else { int j; dir_lines[i].delete = 1; for (j = i + 1; j < section->end_line; j++) { if (dir_lines[j].start[0] == '*') break; dir_lines[j].delete = 1; } } } if (dir_lines[i].start[0] == '*' && menu_line_lessp (entry->text, entry->text_len, dir_lines[i].start, dir_lines[i].size)) add_at_line = i; } if (add_at_line < 0) continue; insert_entry_here (entry, add_at_line, dir_lines, n_entries_to_add); } } } } /* Decide where to add the new sections (unless --delete was used). Alphabetically find the menu sections to add them before. */ if (!delete_flag) { struct node *node; struct node *top = NULL; /* Find the `Top' node. */ for (node = dir_nodes; node; node = node->next) if (node->name && strcmp (node->name, "Top") == 0) top = node; if (top) { struct spec_section *spec; int found = 0; struct line_data *target_line = NULL; for (spec = input_sections; spec; spec = spec->next) { found = 0; target_line = NULL; if (!spec->missing) continue; if (order_new_sections_alphabetically_flag) { struct menu_section *section; struct menu_section *prev_section = NULL; /* Look for the first section name that exceeds SPEC->NAME. */ for (section = top->sections; section ; section = section->next) { found = (mbscasecmp (spec->name, section->name) < 0); if (found) { /* Mark the section for addition at this point. */ if (prev_section) target_line = &dir_lines[prev_section->end_line]; else target_line = &dir_lines[top->sections->start_line - 2]; break; } prev_section = section; } } /* When we can't put a section anywhere, we put it at the bottom of the file. */ if (!found) target_line = &dir_lines[top->end_line]; /* Add the section to our list of sections being added at this point of the DIR file. */ target_line->num_sections_to_add++; target_line->add_sections_before = (struct spec_section **) xrealloc (target_line->add_sections_before, (target_line->num_sections_to_add * sizeof (struct spec_section *))); i = target_line->num_sections_to_add - 1; target_line->add_sections_before[i] = spec; } } } if (delete_flag && !something_deleted && !quiet_flag) warning (_("no entries found for `%s'; nothing deleted"), infile); if (debug_flag) printf ("debug: writing dir file %s\n", opened_dirfilename); if (chicken_flag) printf ("test mode, not updating dir file %s\n", opened_dirfilename); else output_dirfile (opened_dirfilename, dir_nlines, dir_lines, n_entries_to_add, entries_to_add, input_sections, compression_program); exit (EXIT_SUCCESS); } /* Divide the text at DATA (of SIZE bytes) into lines. Return a vector of struct line_data describing the lines. Store the length of that vector into *NLINESP. */ struct line_data * findlines (char *data, int size, int *nlinesp) { int i; int lineflag = 1; int lines_allocated = 511; int filled = 0; struct line_data *lines = xmalloc ((lines_allocated + 1) * sizeof (struct line_data)); for (i = 0; i < size; i++) { if (lineflag) { if (filled == lines_allocated) { /* try to keep things somewhat page-aligned */ lines_allocated = ((lines_allocated + 1) * 2) - 1; lines = xrealloc (lines, (lines_allocated + 1) * sizeof (struct line_data)); } lines[filled].start = &data[i]; lines[filled].add_entries_before = 0; lines[filled].add_sections_before = NULL; lines[filled].num_sections_to_add = 0; lines[filled].delete = 0; if (filled > 0) lines[filled - 1].size = lines[filled].start - lines[filled - 1].start - 1; filled++; } lineflag = (data[i] == '\n'); } if (filled > 0) lines[filled - 1].size = &data[i] - lines[filled - 1].start - lineflag; /* Do not leave garbage in the last element. */ lines[filled].start = NULL; lines[filled].add_entries_before = NULL; lines[filled].add_sections_before = NULL; lines[filled].num_sections_to_add = 0; lines[filled].delete = 0; lines[filled].size = 0; *nlinesp = filled; return lines; } /* This is the comparison function for qsort for a vector of pointers to struct spec_section. (Have to use const void * as the parameter type to avoid incompatible-with-qsort warnings.) Compare the section names. */ int compare_section_names (const void *p1, const void *p2) { struct spec_section **sec1 = (struct spec_section **) p1; struct spec_section **sec2 = (struct spec_section **) p2; char *name1 = (*sec1)->name; char *name2 = (*sec2)->name; return strcmp (name1, name2); } /* This is the comparison function for qsort for a vector of pointers to struct spec_entry. Compare the entries' text. */ int compare_entries_text (const void *p1, const void *p2) { struct spec_entry **entry1 = (struct spec_entry **) p1; struct spec_entry **entry2 = (struct spec_entry **) p2; char *text1 = (*entry1)->text; char *text2 = (*entry2)->text; char *colon1 = strchr (text1, ':'); char *colon2 = strchr (text2, ':'); int len1, len2; if (!colon1) len1 = strlen (text1); else len1 = colon1 - text1; if (!colon2) len2 = strlen (text2); else len2 = colon2 - text2; return mbsncasecmp (text1, text2, len1 <= len2 ? len1 : len2); } /* Insert ENTRY into the ADD_ENTRIES_BEFORE vector for line number LINE_NUMBER of the dir file. DIR_LINES and N_ENTRIES carry information from like-named variables in main. */ void insert_entry_here (struct spec_entry *entry, int line_number, struct line_data *dir_lines, int n_entries) { int i, j; if (dir_lines[line_number].add_entries_before == 0) { dir_lines[line_number].add_entries_before = (struct spec_entry **) xmalloc (n_entries * sizeof (struct spec_entry *)); for (i = 0; i < n_entries; i++) dir_lines[line_number].add_entries_before[i] = 0; } /* Find the place where this entry belongs. If there are already several entries to add before LINE_NUMBER, make sure they are in alphabetical order. */ for (i = 0; i < n_entries; i++) if (dir_lines[line_number].add_entries_before[i] == 0 || menu_line_lessp (entry->text, strlen (entry->text), dir_lines[line_number].add_entries_before[i]->text, strlen (dir_lines[line_number].add_entries_before[i]->text))) break; if (i == n_entries) abort (); /* If we need to plug ENTRY into the middle of the ADD_ENTRIES_BEFORE array, move the entries which should be output after this one down one notch, before adding a new one. */ if (dir_lines[line_number].add_entries_before[i] != 0) for (j = n_entries - 1; j > i; j--) dir_lines[line_number].add_entries_before[j] = dir_lines[line_number].add_entries_before[j - 1]; dir_lines[line_number].add_entries_before[i] = entry; }