/* * man.c: The manual pager * * Copyright (C) 1990, 1991 John W. Eaton. * Copyright (C) 1994, 1995 Graeme W. Wilford. (Wilf.) * Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, * 2011, 2012 Colin Watson. * * This file is part of man-db. * * man-db 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 2 of the License, or * (at your option) any later version. * * man-db 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 man-db; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * John W. Eaton * jwe@che.utexas.edu * Department of Chemical Engineering * The University of Texas at Austin * Austin, Texas 78712 * * Mostly written/re-written by Wilf, some routines by Markus Armbruster. * * CJW: Various robustness, security, and internationalization fixes. * Improved HTML support (originally written by Fabrizio Polacco). * Rewrite of page location routines for improved maintainability and * accuracy. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif /* HAVE_CONFIG_H */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "argp.h" #include "attribute.h" #include "dirname.h" #include "error.h" #include "gl_array_list.h" #include "gl_hash_map.h" #include "gl_hash_set.h" #include "gl_list.h" #include "gl_xlist.h" #include "gl_xmap.h" #include "gl_xset.h" #include "minmax.h" #include "progname.h" #include "regex.h" #include "stat-time.h" #include "utimens.h" #include "xalloc.h" #include "xgetcwd.h" #include "xstdopen.h" #include "xstrndup.h" #include "xvasprintf.h" #include "gettext.h" #include #define _(String) gettext (String) #define N_(String) gettext_noop (String) #include "manconfig.h" #include "appendstr.h" #include "cleanup.h" #include "compression.h" #include "debug.h" #include "fatal.h" #include "filenames.h" #include "glcontainers.h" #include "pipeline.h" #include "pathsearch.h" #include "linelength.h" #include "decompress.h" #include "xregcomp.h" #include "security.h" #include "encodings.h" #include "orderfiles.h" #include "sandbox.h" #include "tempfile.h" #include "util.h" #include "mydbm.h" #include "db_storage.h" #include "globbing.h" #include "ult_src.h" #include "manp.h" #include "zsoelim.h" #include "manconv.h" #include "manconv_client.h" #ifdef MAN_OWNER extern uid_t ruid; extern uid_t euid; #endif /* MAN_OWNER */ /* the default preprocessor sequence */ #ifndef DEFAULT_MANROFFSEQ # define DEFAULT_MANROFFSEQ "" #endif /* placeholder for the manual page name in the less prompt string */ #define MAN_PN "$MAN_PN" /* Some systems lack these */ #ifndef STDIN_FILENO # define STDIN_FILENO 0 #endif #ifndef STDOUT_FILENO # define STDOUT_FILENO 1 #endif #ifndef STDERR_FILENO # define STDERR_FILENO 2 #endif char *lang; /* external formatter programs, one for use without -t, and one with -t */ #define NFMT_PROG "mandb_nfmt" #define TFMT_PROG "mandb_tfmt" #undef ALT_EXT_FORMAT /* allow external formatters located in cat hierarchy */ static bool global_manpath; /* global or user manual page hierarchy? */ static bool skip; /* page exists but has been skipped */ #if defined _AIX || defined __sgi char **global_argv; #endif struct candidate { const char *req_name; char from_db; char cat; const char *path; char *ult; struct mandata *source; int add_index; /* for sort stabilisation */ struct candidate *next; }; #define CANDIDATE_FILESYSTEM 0 #define CANDIDATE_DATABASE 1 static void gripe_system (pipeline *p, int status) { error (CHILD_FAIL, 0, _("command exited with status %d: %s"), status, pipeline_tostring (p)); } enum opts { OPT_WARNINGS = 256, OPT_REGEX, OPT_WILDCARD, OPT_NAMES, OPT_NO_HYPHENATION, OPT_NO_JUSTIFICATION, OPT_NO_SUBPAGES, OPT_MAX }; static gl_list_t manpathlist; /* globals */ int quiet = 1; extern const char *extension; /* for globbing.c */ extern char *user_config_file; /* defined in manp.c */ extern bool disable_cache; extern int min_cat_width, max_cat_width, cat_width; man_sandbox *sandbox; /* locals */ static const char *alt_system_name; static gl_list_t section_list; static const char *section; static char *colon_sep_section_list; static const char *preprocessors; static const char *pager; static const char *locale; static char *internal_locale, *multiple_locale; static const char *prompt_string; static char *less; static const char *std_sections[] = STD_SECTIONS; static char *manp; static const char *external; static gl_map_t db_map = NULL; static bool troff; static const char *roff_device = NULL; static const char *want_encoding = NULL; #ifdef NROFF_WARNINGS static const char default_roff_warnings[] = "mac"; static gl_list_t roff_warnings; #endif /* NROFF_WARNINGS */ static bool global_apropos; static bool print_where, print_where_cat; static bool catman; static bool local_man_file; static bool findall; static bool update; static bool match_case; static bool regex_opt; static bool wildcard; static bool names_only; static int ult_flags = SO_LINK | SOFT_LINK | HARD_LINK; static const char *recode = NULL; static bool no_hyphenation; static bool no_justification; static bool subpages = true; static bool ascii; /* insert tr in the output pipe */ static bool save_cat; /* security breach? Can we save the cat? */ static int first_arg; #ifdef MAN_CATS static char *tmp_cat_file; /* for open_cat_stream(), close_cat_stream() */ static bool created_tmp_cat; /* dto. */ #endif static int tmp_cat_fd; static struct timespec man_modtime; /* modtime of man page, for * commit_tmp_cat() */ # ifdef TROFF_IS_GROFF static bool ditroff; static const char *gxditview; static bool htmlout; static const char *html_pager; # endif /* TROFF_IS_GROFF */ const char *argp_program_version = "man " PACKAGE_VERSION; const char *argp_program_bug_address = PACKAGE_BUGREPORT; error_t argp_err_exit_status = FAIL; static const char args_doc[] = N_("[SECTION] PAGE..."); # ifdef NROFF_WARNINGS # define ONLY_NROFF_WARNINGS 0 # else # define ONLY_NROFF_WARNINGS OPTION_HIDDEN # endif # ifdef HAS_TROFF # ifdef TROFF_IS_GROFF # define ONLY_TROFF_IS_GROFF 0 # else # define ONLY_TROFF_IS_GROFF OPTION_HIDDEN # endif # endif /* HAS_TROFF */ /* Please keep these options in the same order as in parse_opt below. */ static struct argp_option options[] = { OPT ("config-file", 'C', N_("FILE"), N_("use this user configuration file")), OPT ("debug", 'd', 0, N_("emit debugging messages")), OPT ("default", 'D', 0, N_("reset all options to their default values")), OPT_FULL ("warnings", OPT_WARNINGS, N_("WARNINGS"), ONLY_NROFF_WARNINGS | OPTION_ARG_OPTIONAL, N_("enable warnings from groff")), OPT_GROUP_HEADER (N_("Main modes of operation:"), 10), OPT ("whatis", 'f', 0, N_("equivalent to whatis")), OPT ("apropos", 'k', 0, N_("equivalent to apropos")), OPT ("global-apropos", 'K', 0, N_("search for text in all pages")), OPT ("where", 'w', 0, N_("print physical location of man page(s)")), OPT_ALIAS ("path", 0), OPT_ALIAS ("location", 0), OPT ("where-cat", 'W', 0, N_("print physical location of cat file(s)")), OPT_ALIAS ("location-cat", 0), OPT ("local-file", 'l', 0, N_("interpret PAGE argument(s) as local filename(s)")), OPT ("catman", 'c', 0, N_("used by catman to reformat out of date cat pages"), 11), OPT ("recode", 'R', N_("ENCODING"), N_("output source page encoded in ENCODING")), OPT_GROUP_HEADER (N_("Finding manual pages:"), 20), OPT ("locale", 'L', N_("LOCALE"), N_("define the locale for this particular man search")), OPT ("systems", 'm', N_("SYSTEM"), N_("use manual pages from other systems")), OPT ("manpath", 'M', N_("PATH"), N_("set search path for manual pages to PATH")), OPT ("sections", 'S', N_("LIST"), N_("use colon separated section list"), 21), OPT_ALIAS (0, 's'), OPT ("extension", 'e', N_("EXTENSION"), N_("limit search to extension type EXTENSION"), 22), OPT ("ignore-case", 'i', 0, N_("look for pages case-insensitively (default)"), 23), OPT ("match-case", 'I', 0, N_("look for pages case-sensitively")), OPT ("regex", OPT_REGEX, 0, N_("show all pages matching regex"), 24), OPT ("wildcard", OPT_WILDCARD, 0, N_("show all pages matching wildcard")), OPT ("names-only", OPT_NAMES, 0, N_("make --regex and --wildcard match page names only, not " "descriptions"), 25), OPT ("all", 'a', 0, N_("find all matching manual pages"), 26), OPT ("update", 'u', 0, N_("force a cache consistency check")), OPT ("no-subpages", OPT_NO_SUBPAGES, 0, N_("don't try subpages, e.g. 'man foo bar' => 'man foo-bar'"), 27), OPT_GROUP_HEADER (N_("Controlling formatted output:"), 30), OPT ("pager", 'P', N_("PAGER"), N_("use program PAGER to display output")), OPT ("prompt", 'r', N_("STRING"), N_("provide the `less' pager with a prompt")), OPT ("ascii", '7', 0, N_("display ASCII translation of certain latin1 chars"), 31), OPT ("encoding", 'E', N_("ENCODING"), N_("use selected output encoding")), OPT ("no-hyphenation", OPT_NO_HYPHENATION, 0, N_("turn off hyphenation")), OPT_ALIAS ("nh", 0), OPT ("no-justification", OPT_NO_JUSTIFICATION, 0, N_("turn off justification")), OPT_ALIAS ("nj", 0), OPT ("preprocessor", 'p', N_("STRING"), N_("STRING indicates which preprocessors to run:\n" "e - [n]eqn, p - pic, t - tbl,\n" "g - grap, r - refer, v - vgrind")), #ifdef HAS_TROFF OPT ("troff", 't', 0, N_("use %s to format pages"), 32), OPT_FULL ("troff-device", 'T', N_("DEVICE"), OPTION_ARG_OPTIONAL, N_("use %s with selected device")), OPT_FULL ("html", 'H', N_("BROWSER"), ONLY_TROFF_IS_GROFF | OPTION_ARG_OPTIONAL, N_("use %s or BROWSER to display HTML output"), 33), OPT_FULL ("gxditview", 'X', N_("RESOLUTION"), ONLY_TROFF_IS_GROFF | OPTION_ARG_OPTIONAL, N_("use groff and display through gxditview (X11):\n" "-X = -TX75, -X100 = -TX100, -X100-12 = -TX100-12")), OPT_FULL ("ditroff", 'Z', 0, ONLY_TROFF_IS_GROFF, N_("use groff and force it to produce ditroff")), #endif /* HAS_TROFF */ OPT_HELP_COMPAT, { 0 } }; #ifdef TROFF_IS_GROFF static void init_html_pager (void) { html_pager = getenv ("BROWSER"); if (!html_pager) html_pager = PROG_BROWSER; } #endif /* TROFF_IS_GROFF */ static error_t parse_opt (int key, char *arg, struct argp_state *state) { static bool apropos, whatis; /* retain values between calls */ /* Please keep these keys in the same order as in options above. */ switch (key) { case 'C': user_config_file = arg; return 0; case 'd': debug_level = true; return 0; case 'D': /* discard all preset options */ local_man_file = findall = update = catman = debug_level = troff = global_apropos = print_where = print_where_cat = ascii = match_case = regex_opt = wildcard = names_only = no_hyphenation = no_justification = false; #ifdef TROFF_IS_GROFF ditroff = false; gxditview = NULL; htmlout = false; init_html_pager (); #endif roff_device = want_encoding = extension = pager = locale = alt_system_name = external = preprocessors = NULL; colon_sep_section_list = manp = NULL; return 0; case OPT_WARNINGS: #ifdef NROFF_WARNINGS { char *s = xstrdup (arg ? arg : default_roff_warnings); const char *warning; for (warning = strtok (s, ","); warning; warning = strtok (NULL, ",")) gl_list_add_last (roff_warnings, xstrdup (warning)); free (s); } #endif /* NROFF_WARNINGS */ return 0; case 'f': external = WHATIS; whatis = true; return 0; case 'k': external = APROPOS; apropos = true; return 0; case 'K': global_apropos = true; return 0; case 'w': print_where = true; return 0; case 'W': print_where_cat = true; return 0; case 'l': local_man_file = true; return 0; case 'c': catman = true; return 0; case 'R': recode = arg; ult_flags &= ~SO_LINK; return 0; case 'L': locale = arg; return 0; case 'm': alt_system_name = arg; return 0; case 'M': manp = arg; return 0; case 'S': case 's': if (*arg) colon_sep_section_list = arg; return 0; case 'e': extension = arg; return 0; case 'i': match_case = false; return 0; case 'I': match_case = true; return 0; case OPT_REGEX: regex_opt = true; findall = true; return 0; case OPT_WILDCARD: wildcard = true; findall = true; return 0; case OPT_NAMES: names_only = true; return 0; case 'a': findall = true; return 0; case 'u': update = true; return 0; case OPT_NO_SUBPAGES: subpages = false; return 0; case 'P': pager = arg; return 0; case 'r': prompt_string = arg; return 0; case '7': ascii = true; return 0; case 'E': want_encoding = arg; if (is_roff_device (want_encoding)) roff_device = want_encoding; return 0; case OPT_NO_HYPHENATION: no_hyphenation = true; return 0; case OPT_NO_JUSTIFICATION: no_justification = true; return 0; case 'p': preprocessors = arg; return 0; #ifdef HAS_TROFF case 't': troff = true; return 0; case 'T': /* Traditional nroff knows -T; troff does not (gets * ignored). All incarnations of groff know it. Why * does -T imply -t? */ roff_device = (arg ? arg : "ps"); troff = true; return 0; case 'H': # ifdef TROFF_IS_GROFF if (arg) html_pager = arg; htmlout = true; troff = true; roff_device = "html"; # endif /* TROFF_IS_GROFF */ return 0; case 'X': # ifdef TROFF_IS_GROFF troff = true; gxditview = (arg ? arg : "75"); # endif /* TROFF_IS_GROFF */ return 0; case 'Z': # ifdef TROFF_IS_GROFF ditroff = true; troff = true; # endif /* TROFF_IS_GROFF */ return 0; #endif /* HAS_TROFF */ case 'h': argp_state_help (state, state->out_stream, ARGP_HELP_STD_HELP); break; case ARGP_KEY_SUCCESS: /* check for incompatible options */ if ((int) troff + (int) whatis + (int) apropos + (int) catman + (int) (print_where || print_where_cat) > 1) { char *badopts = xasprintf ("%s%s%s%s%s%s", troff ? "-[tTZH] " : "", whatis ? "-f " : "", apropos ? "-k " : "", catman ? "-c " : "", print_where ? "-w " : "", print_where_cat ? "-W " : ""); argp_error (state, _("%s: incompatible options"), badopts); } if ((int) regex_opt + (int) wildcard > 1) { char *badopts = xasprintf ("%s%s", regex_opt ? "--regex " : "", wildcard ? "--wildcard " : ""); argp_error (state, _("%s: incompatible options"), badopts); } return 0; } return ARGP_ERR_UNKNOWN; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" static char *help_filter (int key, const char *text, void *input MAYBE_UNUSED) { #ifdef HAS_TROFF # ifdef TROFF_IS_GROFF static const char formatter[] = "groff"; const char *browser; # else static const char formatter[] = "troff"; # endif /* TROFF_IS_GROFF */ #endif /* HAS_TROFF */ switch (key) { #ifdef HAS_TROFF case 't': case 'T': return xasprintf (text, formatter); # ifdef TROFF_IS_GROFF case 'H': browser = html_pager; assert (browser); if (STRNEQ (browser, "exec ", 5)) browser += 5; return xasprintf (text, browser); # endif /* TROFF_IS_GROFF */ #endif /* HAS_TROFF */ default: return (char *) text; } } #pragma GCC diagnostic pop static struct argp argp = { options, parse_opt, args_doc, 0, 0, help_filter }; /* * changed these messages from stdout to stderr, * (Fabrizio Polacco) Fri, 14 Feb 1997 01:30:07 +0200 */ static void gripe_no_name (const char *sect) { if (sect) { fprintf (stderr, _("No manual entry for %s\n"), sect); fprintf (stderr, _("(Alternatively, what manual page do you want from " "section %s?)\n"), sect); } else fputs (_("What manual page do you want?\n"), stderr); fputs (_("For example, try 'man man'.\n"), stderr); exit (FAIL); } #ifdef HAVE_TERMIOS_H static struct termios tms; static int tms_set = 0; static pid_t tms_pid = 0; static void set_term (void) { if (tms_set && getpid () == tms_pid) tcsetattr (STDIN_FILENO, TCSANOW, &tms); } static void get_term (void) { if (isatty (STDOUT_FILENO)) { debug ("is a tty\n"); tcgetattr (STDIN_FILENO, &tms); if (!tms_set++) { /* Work around pipecmd_exec calling exit(3) rather * than _exit(2), which means our atexit-registered * functions are called at the end of each child * process created using pipecmd_new_function and * friends. It would probably be good to fix this * in libpipeline at some point, but it would * require care to avoid breaking compatibility. */ tms_pid = getpid (); atexit (set_term); } } } #else /* !HAVE_TERMIOS_H */ static void get_term (void) { } #endif /* HAVE_TERMIOS_H */ #if defined(TROFF_IS_GROFF) || defined(HEIRLOOM_NROFF) static int get_roff_line_length (void) { int line_length = cat_width ? cat_width : get_line_length (); if (!troff || ditroff) { int length = line_length * 39 / 40; if (length > line_length - 2) return line_length - 2; else return length; } else return 0; } #ifdef HEIRLOOM_NROFF static void heirloom_line_length (void *data) { printf (".ll %sn\n", (const char *) data); /* TODO: This fails to do anything useful. Why? */ printf (".lt %sn\n", (const char *) data); printf (".lf 1\n"); } #endif /* HEIRLOOM_NROFF */ static pipecmd *add_roff_line_length (pipecmd *cmd, bool *save_cat_p) { int length; pipecmd *ret = NULL; if (!catman && cat_width) debug ("Cat pages forced to terminal width %d\n", cat_width); else if (!catman) { int line_length = get_line_length (); debug ("Terminal width %d\n", line_length); if (line_length >= min_cat_width && line_length <= max_cat_width) debug ("Terminal width %d within cat page range " "[%d, %d]\n", line_length, min_cat_width, max_cat_width); else { debug ("Terminal width %d not within cat page range " "[%d, %d]\n", line_length, min_cat_width, max_cat_width); *save_cat_p = false; } } length = get_roff_line_length (); if (length) { #ifdef HEIRLOOM_NROFF char *name; char *lldata; pipecmd *llcmd; #endif /* HEIRLOOM_NROFF */ debug ("Using %d-character lines\n", length); #if defined(TROFF_IS_GROFF) pipecmd_argf (cmd, "-rLL=%dn", length); pipecmd_argf (cmd, "-rLT=%dn", length); #elif defined(HEIRLOOM_NROFF) name = xasprintf ("echo .ll %dn && echo .lt %dn && echo .lf 1", length, length); lldata = xasprintf ("%d", length); llcmd = pipecmd_new_function (name, heirloom_line_length, free, lldata); ret = pipecmd_new_sequence ("line-length", llcmd, pipecmd_new_passthrough (), NULL); free (name); #endif /* HEIRLOOM_NROFF */ } return ret; } #endif /* TROFF_IS_GROFF || HEIRLOOM_NROFF */ static void gripe_no_man (const char *name, const char *sec) { /* On AIX and IRIX, fall back to the vendor supplied browser. */ #if defined _AIX || defined __sgi if (!troff) { pipecmd *vendor_man; int i; vendor_man = pipecmd_new ("/usr/bin/man"); for (i = 1; i < argc; ++i) pipecmd_arg (vendor_man, global_argv[i]); pipecmd_unsetenv (vendor_man, "MANPATH"); pipecmd_exec (vendor_man); } #endif if (sec) fprintf (stderr, _("No manual entry for %s in section %s\n"), name, sec); else fprintf (stderr, _("No manual entry for %s\n"), name); #ifdef UNDOC_COMMAND if (getenv ("MAN_TEST_DISABLE_UNDOCUMENTED") == NULL && pathsearch_executable (name)) fprintf (stderr, _("See '%s' for help when manual pages are not " "available.\n"), UNDOC_COMMAND); #endif } /* fire up the appropriate external program */ static void do_extern (int argc, char *argv[]) { pipeline *p; pipecmd *cmd; cmd = pipecmd_new (external); /* Please keep these in the same order as they are in whatis.c. */ if (debug_level) pipecmd_arg (cmd, "-d"); if (regex_opt) pipecmd_arg (cmd, "-r"); if (wildcard) pipecmd_arg (cmd, "-w"); if (local_man_file) /* actually apropos/whatis --long */ pipecmd_arg (cmd, "-l"); if (colon_sep_section_list) pipecmd_args (cmd, "-s", colon_sep_section_list, (void *) 0); if (alt_system_name) pipecmd_args (cmd, "-m", alt_system_name, (void *) 0); if (manp) pipecmd_args (cmd, "-M", manp, (void *) 0); if (locale) pipecmd_args (cmd, "-L", locale, (void *) 0); if (user_config_file) pipecmd_args (cmd, "-C", user_config_file, (void *) 0); while (first_arg < argc) pipecmd_arg (cmd, argv[first_arg++]); p = pipeline_new_commands (cmd, (void *) 0); /* privs are already dropped */ exit (pipeline_run (p)); } /* lookup $MANOPT and if available, put in *argv[] format for argp */ static char **manopt_to_env (int *argc) { char *manopt, *manopt_copy, *opt_start, **argv; manopt = getenv ("MANOPT"); if (manopt == NULL || *manopt == '\0') return NULL; opt_start = manopt = manopt_copy = xstrdup (manopt); /* allocate space for the program name */ *argc = 0; argv = XNMALLOC (*argc + 3, char *); argv[(*argc)++] = base_name (program_name); /* for each [ \t]+ delimited string, allocate an array space and fill it in. An escaped space is treated specially */ while (*manopt) { switch (*manopt) { case ' ': case '\t': if (manopt != opt_start) { *manopt = '\0'; argv = xnrealloc (argv, *argc + 3, sizeof (char *)); argv[(*argc)++] = xstrdup (opt_start); } while (CTYPE (isspace, *(manopt + 1))) *++manopt = '\0'; opt_start = manopt + 1; break; case '\\': if (*(manopt + 1) == ' ') manopt++; break; default: break; } manopt++; } if (*opt_start) argv[(*argc)++] = xstrdup (opt_start); argv[*argc] = NULL; free (manopt_copy); return argv; } /* Return char array with 'less' special chars escaped. Uses static storage. */ static const char *escape_less (const char *string) { static char *escaped_string; char *ptr; /* 2*strlen will always be long enough to hold the escaped string */ ptr = escaped_string = xrealloc (escaped_string, 2 * strlen (string) + 1); while (*string) { char c = *string++; if (c == '$') /* Dollar signs are difficult to handle properly, and * not really worth the trouble, so just replace them * with question marks. See * https://bugs.debian.org/1021951. */ c = '?'; if (strchr ("?:.%\\", c)) *ptr++ = '\\'; *ptr++ = c; } *ptr = *string; return escaped_string; } #if defined(MAN_DB_CREATES) || defined(MAN_DB_UPDATES) /* Run mandb to ensure databases are up to date. Only used with -u. * Returns the exit status of mandb. * * If filename is non-NULL, uses mandb's -f option to update a single file. */ static int run_mandb (bool create, const char *manpath, const char *filename) { pipeline *mandb_pl = pipeline_new (); pipecmd *mandb_cmd = pipecmd_new ("mandb"); if (debug_level) pipecmd_arg (mandb_cmd, "-d"); else pipecmd_arg (mandb_cmd, "-q"); if (user_config_file) pipecmd_args (mandb_cmd, "-C", user_config_file, (void *) 0); if (filename) pipecmd_args (mandb_cmd, "-f", filename, (void *) 0); else if (create) { pipecmd_arg (mandb_cmd, "-c"); pipecmd_setenv (mandb_cmd, "MAN_MUST_CREATE", "1"); } else pipecmd_arg (mandb_cmd, "-p"); if (manpath) pipecmd_arg (mandb_cmd, manpath); pipeline_command (mandb_pl, mandb_cmd); if (debug_level) { debug ("running mandb: "); pipeline_dump (mandb_pl, stderr); } return pipeline_run (mandb_pl); } #endif /* MAN_DB_CREATES || MAN_DB_UPDATES */ static char *locale_manpath (const char *manpath) { char *all_locales; char *new_manpath; if (multiple_locale && *multiple_locale) { if (internal_locale && *internal_locale) all_locales = xasprintf ("%s:%s", multiple_locale, internal_locale); else all_locales = xstrdup (multiple_locale); } else { if (internal_locale && *internal_locale) all_locales = xstrdup (internal_locale); else all_locales = NULL; } new_manpath = add_nls_manpaths (manpath, all_locales); free (all_locales); return new_manpath; } /* * Check to see if the argument is a valid section number. * If the name matches one of * the sections listed in section_list, we'll assume that it's a section. * The list of sections in config.h simply allows us to specify oddly * named directories like .../man3f. Yuk. */ static const char * ATTRIBUTE_PURE is_section (const char *name) { const char *vs; GL_LIST_FOREACH (section_list, vs) { if (STREQ (vs, name)) return name; /* allow e.g. 3perl but disallow 8139too and libfoo */ if (strlen (vs) == 1 && CTYPE (isdigit, *vs) && strlen (name) > 1 && !CTYPE (isdigit, name[1]) && STRNEQ (vs, name, 1)) return name; } return NULL; } /* Snarf pre-processors from file, return string or NULL on failure */ static char *get_preprocessors_from_file (decompress *decomp, int prefixes) { const size_t block = 4096; int i; char *line = NULL; size_t previous_len = 0; if (!decomp) return NULL; /* Prefixes are inserted into the stream by man itself, and we must * skip over them to find any preprocessors line that exists. Each * one ends with an .lf macro. */ for (i = 0; ; ++i) { size_t len = block * (i + 1); const char *buffer, *scan, *end; int j; scan = buffer = decompress_peek (decomp, &len); if (!buffer || len == 0) return NULL; for (j = 0; j < prefixes; ++j) { scan = memmem (scan, len - (scan - buffer), "\n.lf ", strlen ("\n.lf ")); if (!scan) break; ++scan; scan = memchr (scan, '\n', len - (scan - buffer)); if (!scan) break; ++scan; } if (!scan) continue; end = memchr (scan, '\n', len - (scan - buffer)); if (!end && len == previous_len) /* end of file, no newline found */ end = buffer + len - 1; if (end) { line = xstrndup (scan, end - scan + 1); break; } previous_len = len; } if (!line) return NULL; if (!strncmp (line, PP_COOKIE, 4)) { const char *newline = strchr (line, '\n'); if (newline) return xstrndup (line + 4, newline - (line + 4)); else return xstrdup (line + 4); } return NULL; } /* Determine pre-processors, set save_cat and return string */ static char *get_preprocessors (decompress *decomp, const char *dbfilters, int prefixes) { char *pp_string; const char *pp_source; const char *env; /* try in order: database, command line, file, environment, default */ /* command line overrides the database, but database empty overrides default */ if (dbfilters && (dbfilters[0] != '-') && !preprocessors) { pp_string = xstrdup (dbfilters); pp_source = "database"; save_cat = true; } else if (preprocessors) { pp_string = xstrdup (preprocessors); pp_source = "command line"; save_cat = false; } else if ((pp_string = get_preprocessors_from_file (decomp, prefixes))) { pp_source = "file"; save_cat = true; } else if ((env = getenv ("MANROFFSEQ"))) { pp_string = xstrdup (env); pp_source = "environment"; save_cat = false; } else if (!dbfilters) { pp_string = xstrdup (DEFAULT_MANROFFSEQ); pp_source = "default"; save_cat = true; } else { pp_string = xstrdup (""); pp_source = "no filters"; save_cat = true; } debug ("pre-processors `%s' from %s\n", pp_string, pp_source); return pp_string; } static const char *my_locale_charset (void) { if (want_encoding && !is_roff_device (want_encoding)) return want_encoding; else return get_locale_charset (); } static void add_col (pipeline *p, const char *locale_charset, ...) { pipecmd *cmd; va_list argv; char *col_locale = NULL; cmd = pipecmd_new (PROG_COL); va_start (argv, locale_charset); pipecmd_argv (cmd, argv); va_end (argv); pipecmd_pre_exec (cmd, sandbox_load, sandbox_free, sandbox); if (locale_charset) col_locale = find_charset_locale (locale_charset); if (col_locale) { pipecmd_setenv (cmd, "LC_CTYPE", col_locale); free (col_locale); } pipeline_command (p, cmd); } static void add_filter (pipeline *p, pipecmd *cmd, bool wants_dev, bool wants_post) { if (wants_dev) { if (roff_device) pipecmd_argf (cmd, "-T%s", roff_device); #ifdef TROFF_IS_GROFF else if (gxditview) { pipecmd_argf (cmd, "-TX%s", gxditview); if (strstr (gxditview, "-12")) pipecmd_argf (cmd, "-rS12"); } #endif /* TROFF_IS_GROFF */ } if (wants_post) { #ifdef TROFF_IS_GROFF if (gxditview) /* -X arranges for the correct options to be passed * to troff. Normally it would run gxditview as * well, but we suppress that with -Z so that we can * do it ourselves; this lets us set a better window * title, and means that we don't have to worry * about sandboxing text processing and an X program * in the same way. */ pipecmd_args (cmd, "-X", "-Z", (void *) 0); #endif /* TROFF_IS_GROFF */ if (roff_device && STREQ (roff_device, "ps")) /* Tell grops to guess the page size. */ pipecmd_arg (cmd, "-P-g"); } pipecmd_pre_exec (cmd, sandbox_load_permissive, sandbox_free, sandbox); pipeline_command (p, cmd); } /* Return pipeline to format file to stdout. */ static pipeline *make_roff_command (const char *dir, const char *file, decompress *decomp, const char *pp_string, char **result_encoding) { const char *roff_opt; char *fmt_prog = NULL; pipeline *p = pipeline_new (); pipecmd *cmd; char *page_encoding = NULL; const char *output_encoding = NULL; const char *locale_charset = NULL; *result_encoding = xstrdup ("UTF-8"); /* optimistic default */ roff_opt = getenv ("MANROFFOPT"); if (!roff_opt) roff_opt = ""; if (dir && !recode) { #ifdef ALT_EXT_FORMAT char *catpath = get_catpath (dir, global_manpath ? SYSTEM_CAT : USER_CAT); /* If we have an alternate catpath, look for an external * formatter there. */ if (catpath) { fmt_prog = appendstr (catpath, "/", troff ? TFMT_PROG : NFMT_PROG, (void *) 0); if (!CAN_ACCESS (fmt_prog, X_OK)) { free (fmt_prog); fmt_prog = NULL; } } #endif /* ALT_EXT_FORMAT */ /* If the page is in a proper manual page hierarchy (as * opposed to being read using --local-file or similar), * look for an external formatter there. */ if (!fmt_prog) { fmt_prog = appendstr (NULL, dir, "/", troff ? TFMT_PROG : NFMT_PROG, (void *) 0); if (!CAN_ACCESS (fmt_prog, X_OK)) { free (fmt_prog); fmt_prog = NULL; } } } if (fmt_prog) debug ("External formatter %s\n", fmt_prog); if (!fmt_prog) { /* we don't have an external formatter script */ const char *source_encoding, *roff_encoding; const char *groff_preconv; if (!recode) { struct zsoelim_stdin_data *zsoelim_data; zsoelim_data = zsoelim_stdin_data_new (dir, manpathlist); cmd = pipecmd_new_function (ZSOELIM, &zsoelim_stdin, zsoelim_stdin_data_free, zsoelim_data); pipecmd_pre_exec (cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (p, cmd); } page_encoding = check_preprocessor_encoding (decomp, NULL, NULL); if (!page_encoding) page_encoding = get_page_encoding (lang); if (page_encoding && !STREQ (page_encoding, "UTF-8")) source_encoding = page_encoding; else source_encoding = get_source_encoding (lang); debug ("page_encoding = %s\n", page_encoding); debug ("source_encoding = %s\n", source_encoding); /* Load the roff_device value dependent on the language dir * in the path. */ if (!troff) { #define STRC(s, otherwise) ((s) ? (s) : (otherwise)) locale_charset = my_locale_charset (); debug ("locale_charset = %s\n", STRC (locale_charset, "NULL")); /* Pick the default device for this locale if there * wasn't one selected explicitly. */ if (!roff_device) { roff_device = get_default_device (locale_charset, source_encoding); #ifdef HEIRLOOM_NROFF /* In Heirloom, if LC_CTYPE is a UTF-8 * locale, then -Tlocale will be equivalent * to -Tutf8 except that it will do a * slightly better job of rendering some * special characters. */ if (STREQ (roff_device, "utf8")) { const char *real_locale_charset = get_locale_charset (); if (real_locale_charset && STREQ (real_locale_charset, "UTF-8")) roff_device = "locale"; } #endif /* HEIRLOOM_NROFF */ debug ("roff_device (locale) = %s\n", STRC (roff_device, "NULL")); } } roff_encoding = get_roff_encoding (roff_device, source_encoding); debug ("roff_encoding = %s\n", roff_encoding); /* We may need to recode: * from page_encoding to roff_encoding on input; * from output_encoding to locale_charset on output * (if not troff). * If we have preconv, then use it to recode the * input to a safe escaped form. * The --recode option overrides everything else. */ groff_preconv = get_groff_preconv (); if (recode) add_manconv (p, page_encoding, recode); else if (groff_preconv) { pipecmd *preconv_cmd; add_manconv (p, page_encoding, "UTF-8"); preconv_cmd = pipecmd_new_args (groff_preconv, "-e", "UTF-8", (void *) 0); pipecmd_pre_exec (preconv_cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (p, preconv_cmd); } else if (roff_encoding) add_manconv (p, page_encoding, roff_encoding); else add_manconv (p, page_encoding, page_encoding); if (!troff && !recode) { output_encoding = get_output_encoding (roff_device); if (!output_encoding) output_encoding = source_encoding; debug ("output_encoding = %s\n", output_encoding); free (*result_encoding); *result_encoding = xstrdup (output_encoding); if (!getenv ("LESSCHARSET")) { const char *less_charset = get_less_charset (locale_charset); debug ("less_charset = %s\n", less_charset); setenv ("LESSCHARSET", less_charset, 1); } if (!getenv ("JLESSCHARSET")) { const char *jless_charset = get_jless_charset (locale_charset); if (jless_charset) { debug ("jless_charset = %s\n", jless_charset); setenv ("JLESSCHARSET", jless_charset, 1); } } } } if (recode) ; else if (!fmt_prog) { char *pp_string_initial; const char *pp; #ifndef GNU_NROFF bool using_tbl = false; #endif /* GNU_NROFF */ #ifdef NROFF_WARNINGS const char *warning; #endif /* NROFF_WARNINGS */ /* Add preprocessors. Per groff(1), grap, chem, and ideal must * come before pic, and tbl must come before eqn. */ pp_string_initial = xstrndup (pp_string, strcspn (pp_string, " -")); if (strchr (pp_string_initial, 'r')) { cmd = pipecmd_new_argstr (get_def ("refer", PROG_REFER)); add_filter (p, cmd, false, false); } if (strchr (pp_string_initial, 'g')) { cmd = pipecmd_new_argstr (get_def ("grap", PROG_GRAP)); add_filter (p, cmd, false, false); } if (strchr (pp_string_initial, 'p')) { cmd = pipecmd_new_argstr (get_def ("pic", PROG_PIC)); add_filter (p, cmd, false, false); } if (strchr (pp_string_initial, 't')) { cmd = pipecmd_new_argstr (get_def ("tbl", PROG_TBL)); add_filter (p, cmd, false, false); #ifndef GNU_NROFF using_tbl = true; #endif /* GNU_NROFF */ } if (strchr (pp_string_initial, 'e')) { const char *eqn; if (troff) eqn = get_def ("eqn", PROG_EQN); else eqn = get_def ("neqn", PROG_NEQN); cmd = pipecmd_new_argstr (eqn); /* eqn wants device options. */ add_filter (p, cmd, true, false); } if (strchr (pp_string_initial, 'v')) { cmd = pipecmd_new_argstr (get_def ("vgrind", PROG_VGRIND)); add_filter (p, cmd, false, false); } for (pp = pp_string_initial; *pp; ++pp) { if (!strchr ("rgptev", *pp)) error (0, 0, _("ignoring unknown preprocessor `%c'"), *pp); } free (pp_string_initial); /* Add *roff itself. */ if (troff) { cmd = pipecmd_new_argstr (get_def ("troff", PROG_TROFF)); save_cat = false; } else cmd = pipecmd_new_argstr (get_def ("nroff", PROG_NROFF)); #ifdef TROFF_IS_GROFF if (troff && ditroff) pipecmd_arg (cmd, "-Z"); #endif /* TROFF_IS_GROFF */ #if defined(TROFF_IS_GROFF) || defined(HEIRLOOM_NROFF) { pipecmd *seq = add_roff_line_length (cmd, &save_cat); if (seq) pipeline_command (p, seq); } #endif /* TROFF_IS_GROFF || HEIRLOOM_NROFF */ #ifdef TROFF_IS_GROFF /* See also display () for the more complex non-groff case. */ if (!recode && no_hyphenation) pipecmd_argf (cmd, "-rHY=0"); #endif /* !TROFF_IS_GROFF */ #ifdef NROFF_WARNINGS GL_LIST_FOREACH (roff_warnings, warning) { if (warning[0] == '!') pipecmd_argf (cmd, "-W%s", warning + 1); else pipecmd_argf (cmd, "-w%s", warning); } #endif /* NROFF_WARNINGS */ #ifdef HEIRLOOM_NROFF if (running_setuid ()) pipecmd_unsetenv (cmd, "TROFFMACS"); #endif /* HEIRLOOM_NROFF */ pipecmd_argstr (cmd, roff_opt); /* *roff wants both device and postprocessor arguments. */ add_filter (p, cmd, true, true); if (!troff && *PROG_COL != '\0') { const char *man_keep_formatting = getenv ("MAN_KEEP_FORMATTING"); if ((!man_keep_formatting || !*man_keep_formatting) && !isatty (STDOUT_FILENO)) /* we'll run col later, but prepare for it */ setenv ("GROFF_NO_SGR", "1", 1); #ifndef GNU_NROFF /* tbl needs col */ else if (using_tbl && !troff && *PROG_COL != '\0') add_col (p, locale_charset, (void *) 0); #endif /* GNU_NROFF */ } } else { /* use external formatter script, it takes arguments input file, preprocessor string, and (optional) output device */ cmd = pipecmd_new_args (fmt_prog, file, pp_string, (void *) 0); if (roff_device) pipecmd_arg (cmd, roff_device); pipeline_command (p, cmd); } free (fmt_prog); free (page_encoding); return p; } #ifdef TROFF_IS_GROFF /* Return pipeline to run a browser on a given file, observing * http://www.tuxedo.org/~esr/BROWSER/. * * (Actually, I really implement * https://www.dwheeler.com/browse/secure_browser.html, but it's * backward-compatible.) * * TODO: Is there any way to use the pipeline library better here? */ static pipeline *make_browser (const char *pattern, const char *file) { pipeline *p; pipecmd *cmd; char *browser = xmalloc (1); bool found_percent_s = false; char *percent; char *esc_file; *browser = '\0'; percent = strchr (pattern, '%'); while (percent) { size_t len = strlen (browser); browser = xrealloc (browser, len + 1 + (percent - pattern)); strncat (browser, pattern, percent - pattern); switch (*(percent + 1)) { case '\0': case '%': browser = appendstr (browser, "%", (void *) 0); break; case 'c': browser = appendstr (browser, ":", (void *) 0); break; case 's': esc_file = escape_shell (file); browser = appendstr (browser, esc_file, (void *) 0); free (esc_file); found_percent_s = true; break; default: len = strlen (browser); /* cannot be NULL */ browser = xrealloc (browser, len + 3); strncat (browser, percent, 2); break; } if (*(percent + 1)) pattern = percent + 2; else pattern = percent + 1; percent = strchr (pattern, '%'); } browser = appendstr (browser, pattern, (void *) 0); if (!found_percent_s) { esc_file = escape_shell (file); browser = appendstr (browser, " ", esc_file, (void *) 0); free (esc_file); } cmd = pipecmd_new_args ("/bin/sh", "-c", browser, (void *) 0); pipecmd_pre_exec (cmd, drop_privs, NULL, NULL); p = pipeline_new_commands (cmd, (void *) 0); pipeline_ignore_signals (p, 1); free (browser); return p; } #endif /* TROFF_IS_GROFF */ static void setenv_less (pipecmd *cmd, const char *title) { const char *esc_title; char *less_opts, *man_pn; esc_title = escape_less (title); less_opts = xasprintf (LESS_OPTS, prompt_string, prompt_string); less_opts = appendstr (less_opts, less, (void *) 0); man_pn = strstr (less_opts, MAN_PN); while (man_pn) { char *subst_opts = xmalloc (strlen (less_opts) - strlen (MAN_PN) + strlen (esc_title) + 1); strncpy (subst_opts, less_opts, man_pn - less_opts); subst_opts[man_pn - less_opts] = '\0'; strcat (subst_opts, esc_title); strcat (subst_opts, man_pn + strlen (MAN_PN)); free (less_opts); less_opts = subst_opts; man_pn = strstr (less_opts, MAN_PN); } debug ("Setting LESS to %s\n", less_opts); pipecmd_setenv (cmd, "LESS", less_opts); debug ("Setting MAN_PN to %s\n", esc_title); pipecmd_setenv (cmd, "MAN_PN", esc_title); free (less_opts); } static void add_output_iconv (pipeline *p, const char *source, const char *target) { debug ("add_output_iconv: source %s, target %s\n", source, target); if (source && target && !STREQ (source, target)) { char *target_translit = xasprintf ("%s//TRANSLIT", target); pipecmd *iconv_cmd; iconv_cmd = pipecmd_new_args ("iconv", "-c", "-f", source, "-t", target_translit, (void *) 0); pipecmd_pre_exec (iconv_cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (p, iconv_cmd); free (target_translit); } } /* Pipeline command to squeeze multiple blank lines into one. * */ static void squeeze_blank_lines (void *data MAYBE_UNUSED) { char *line = NULL; size_t len = 0; while (getline (&line, &len, stdin) != -1) { bool in_blank_line = true; bool got_blank_line = false; while (in_blank_line) { char *p; for (p = line; *p; ++p) { if (!CTYPE (isspace, *p)) { in_blank_line = false; break; } } if (in_blank_line) { got_blank_line = true; free (line); line = NULL; len = 0; if (getline (&line, &len, stdin) == -1) break; } } if (got_blank_line && putchar ('\n') < 0) break; if (!in_blank_line && fputs (line, stdout) < 0) break; free (line); line = NULL; len = 0; } free (line); return; } /* Return pipeline to display file provided on stdin. * * TODO: htmlout case is pretty weird now. I'd like the intelligence to be * somewhere other than format_display. */ static pipeline *make_display_command (const char *encoding, const char *title) { pipeline *p = pipeline_new (); const char *locale_charset = NULL; pipecmd *pager_cmd = NULL; locale_charset = my_locale_charset (); if (!troff && (!want_encoding || !is_roff_device (want_encoding))) add_output_iconv (p, encoding, locale_charset); if (!troff && *PROG_COL != '\0') { /* get rid of special characters if not writing to a * terminal */ const char *man_keep_formatting = getenv ("MAN_KEEP_FORMATTING"); if ((!man_keep_formatting || !*man_keep_formatting) && !isatty (STDOUT_FILENO)) add_col (p, locale_charset, "-b", "-p", "-x", (void *) 0); } #ifdef TROFF_IS_GROFF if (gxditview) { char *x_resource = xasprintf ("*iconName:%s", title); pipeline_command_args (p, "gxditview", "-title", title, "-xrm", x_resource, "-", (void *) 0); free (x_resource); return p; } #endif /* TROFF_IS_GROFF */ /* emulate pager -s, the sed code is just for information */ { pipecmd *cmd; const char *name = "sed -e '/^[[:space:]]*$/{ N; /^[[:space:]]*\\n[[:space:]]*$/D; }'"; cmd = pipecmd_new_function (name, &squeeze_blank_lines, NULL, NULL); pipeline_command (p, cmd); } if (isatty (STDOUT_FILENO)) { if (ascii) { pipecmd *tr_cmd; tr_cmd = pipecmd_new_argstr (get_def_user ("tr", PROG_TR TR_SET1 TR_SET2)); pipecmd_pre_exec (tr_cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (p, tr_cmd); pager_cmd = pipecmd_new_argstr (pager); } else #ifdef TROFF_IS_GROFF if (!htmlout) /* format_display deals with html_pager */ #endif pager_cmd = pipecmd_new_argstr (pager); } if (pager_cmd) { setenv_less (pager_cmd, title); pipeline_command (p, pager_cmd); } pipeline_ignore_signals (p, 1); if (!pipeline_get_ncommands (p)) /* Always return at least a dummy pipeline. */ pipeline_command (p, pipecmd_new_passthrough ()); return p; } /* return a (malloced) temporary name in cat_file's directory */ static char *tmp_cat_filename (const char *cat_file) { char *name; if (debug_level) { name = xstrdup ("/dev/null"); tmp_cat_fd = open (name, O_WRONLY); } else { char *slash; name = xstrdup (cat_file); slash = strrchr (name, '/'); if (slash) *(slash + 1) = '\0'; else *name = '\0'; name = appendstr (name, "catXXXXXX", (void *) 0); tmp_cat_fd = mkstemp (name); } if (tmp_cat_fd == -1) { free (name); return NULL; } else return name; } /* If delete unlink tmp_cat, else commit tmp_cat to cat_file. Return non-zero on error. */ static int commit_tmp_cat (const char *cat_file, const char *tmp_cat, int delete) { int status = 0; #ifdef MAN_OWNER if (!delete && global_manpath && euid == 0) { if (debug_level) { debug ("fixing temporary cat's ownership\n"); status = 0; } else { struct passwd *man_owner = get_man_owner (); status = chown (tmp_cat, man_owner->pw_uid, man_owner->pw_gid); if (status) error (0, errno, _("can't chown %s"), tmp_cat); } } #endif /* MAN_OWNER */ if (!delete && !status) { if (debug_level) { debug ("fixing temporary cat's mode\n"); status = 0; } else { status = chmod (tmp_cat, CATMODE); if (status) error (0, errno, _("can't chmod %s"), tmp_cat); } } if (!delete && !status) { if (debug_level) { debug ("renaming temporary cat to %s\n", cat_file); status = 0; } else { status = rename (tmp_cat, cat_file); if (status) error (0, errno, _("can't rename %s to %s"), tmp_cat, cat_file); } } if (!delete && !status) { if (debug_level) { debug ("setting modtime on cat file %s\n", cat_file); status = 0; } else { struct timespec times[2]; times[0].tv_sec = 0; times[0].tv_nsec = UTIME_NOW; times[1] = man_modtime; status = utimens (cat_file, times); if (status) error (0, errno, _("can't set times on %s"), cat_file); } } if (delete || status) { if (debug_level) debug ("unlinking temporary cat\n"); else if (unlink (tmp_cat)) error (0, errno, _("can't unlink %s"), tmp_cat); } return status; } /* TODO: This should all be refactored after work on the decompression * library is complete. */ static void discard_stderr (pipeline *p) { int i; for (i = 0; i < pipeline_get_ncommands (p); ++i) pipecmd_discard_err (pipeline_get_command (p, i), 1); } static void maybe_discard_stderr (pipeline *p) { const char *man_keep_stderr = getenv ("MAN_KEEP_STDERR"); if ((!man_keep_stderr || !*man_keep_stderr) && isatty (STDOUT_FILENO)) discard_stderr (p); } static void chdir_commands (pipeline *p, const char *dir) { int i; for (i = 0; i < pipeline_get_ncommands (p); ++i) pipecmd_chdir (pipeline_get_command (p, i), dir); } static void cleanup_unlink (void *arg) { const char *path = arg; if (unlink (path)) error (0, errno, _("can't unlink %s"), path); } #ifdef MAN_CATS /* Return pipeline to write formatted manual page to for saving as cat file. */ static pipeline *open_cat_stream (const char *cat_file, const char *encoding) { pipeline *cat_p; # ifdef COMP_CAT pipecmd *comp_cmd; # endif created_tmp_cat = false; debug ("creating temporary cat for %s\n", cat_file); tmp_cat_file = tmp_cat_filename (cat_file); if (tmp_cat_file) created_tmp_cat = true; else { if (!debug_level && (errno == EACCES || errno == EROFS)) { /* No permission to write to the cat file. Oh well, * return NULL and let the caller sort it out. */ debug ("can't write to temporary cat for %s\n", cat_file); return NULL; } else fatal (errno, _("can't create temporary cat for %s"), cat_file); } if (!debug_level) push_cleanup (cleanup_unlink, tmp_cat_file, 1); cat_p = pipeline_new (); add_output_iconv (cat_p, encoding, "UTF-8"); # ifdef COMP_CAT /* fork the compressor */ comp_cmd = pipecmd_new_argstr (get_def ("compressor", PROG_COMPRESSOR)); pipecmd_nice (comp_cmd, 10); pipecmd_pre_exec (comp_cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (cat_p, comp_cmd); # endif /* pipeline_start will close tmp_cat_fd */ pipeline_want_out (cat_p, tmp_cat_fd); return cat_p; } /* Close the cat page stream, return non-zero on error. If delete don't update the cat file. */ static int close_cat_stream (pipeline *cat_p, const char *cat_file, int delete) { int status; status = pipeline_wait (cat_p); debug ("cat-saver exited with status %d\n", status); pipeline_free (cat_p); if (created_tmp_cat) { status |= commit_tmp_cat (cat_file, tmp_cat_file, delete || status); if (!debug_level) pop_cleanup (cleanup_unlink, tmp_cat_file); } free (tmp_cat_file); return status; } /* * format a manual page with format_cmd, display it with disp_cmd, and * save it to cat_file */ static int format_display_and_save (decompress *d, pipeline *format_cmd, pipeline *disp_cmd, const char *cat_file, const char *encoding) { pipeline *decomp = decompress_get_pipeline (d); pipeline *sav_p = open_cat_stream (cat_file, encoding); int instat; if (global_manpath) drop_effective_privs (); maybe_discard_stderr (format_cmd); pipeline_connect (decomp, format_cmd, (void *) 0); if (sav_p) { pipeline_connect (format_cmd, disp_cmd, sav_p, (void *) 0); pipeline_pump (decomp, format_cmd, disp_cmd, sav_p, (void *) 0); } else { pipeline_connect (format_cmd, disp_cmd, (void *) 0); pipeline_pump (decomp, format_cmd, disp_cmd, (void *) 0); } if (global_manpath) regain_effective_privs (); pipeline_wait (decomp); instat = pipeline_wait (format_cmd); if (sav_p) close_cat_stream (sav_p, cat_file, instat); pipeline_wait (disp_cmd); return instat; } #endif /* MAN_CATS */ #ifdef TROFF_IS_GROFF # define MAN_FILE_UNUSED #else /* !TROFF_IS_GROFF */ # define MAN_FILE_UNUSED MAYBE_UNUSED #endif /* TROFF_IS_GROFF */ /* Format a manual page with format_cmd and display it with disp_cmd. * Handle temporary file creation if necessary. * TODO: merge with format_display_and_save */ static void format_display (decompress *d, pipeline *format_cmd, pipeline *disp_cmd, const char *man_file MAN_FILE_UNUSED) { pipeline *decomp = decompress_get_pipeline (d); int format_status = 0, disp_status = 0; #ifdef TROFF_IS_GROFF char *htmldir = NULL, *htmlfile = NULL; #endif /* TROFF_IS_GROFF */ if (format_cmd) maybe_discard_stderr (format_cmd); drop_effective_privs (); #ifdef TROFF_IS_GROFF if (format_cmd && htmlout) { char *man_base, *man_ext; int htmlfd; htmldir = create_tempdir ("hman"); if (!htmldir) fatal (errno, _("can't create temporary directory")); chdir_commands (format_cmd, htmldir); chdir_commands (disp_cmd, htmldir); man_base = base_name (man_file); man_ext = strchr (man_base, '.'); if (man_ext) *man_ext = '\0'; htmlfile = xasprintf ("%s/%s.html", htmldir, man_base); free (man_base); htmlfd = open (htmlfile, O_CREAT | O_EXCL | O_WRONLY, 0644); if (htmlfd == -1) fatal (errno, _("can't open temporary file %s"), htmlfile); pipeline_want_out (format_cmd, htmlfd); pipeline_connect (decomp, format_cmd, (void *) 0); pipeline_pump (decomp, format_cmd, (void *) 0); pipeline_wait (decomp); format_status = pipeline_wait (format_cmd); } else #endif /* TROFF_IS_GROFF */ if (format_cmd) { pipeline_connect (decomp, format_cmd, (void *) 0); pipeline_connect (format_cmd, disp_cmd, (void *) 0); pipeline_pump (decomp, format_cmd, disp_cmd, (void *) 0); pipeline_wait (decomp); format_status = pipeline_wait (format_cmd); disp_status = pipeline_wait (disp_cmd); } else { pipeline_connect (decomp, disp_cmd, (void *) 0); pipeline_pump (decomp, disp_cmd, (void *) 0); pipeline_wait (decomp); disp_status = pipeline_wait (disp_cmd); } #ifdef TROFF_IS_GROFF if (format_cmd && htmlout) { char *browser_list, *candidate; if (format_status) { if (remove_directory (htmldir, false) == -1) error (0, errno, _("can't remove directory %s"), htmldir); free (htmlfile); free (htmldir); gripe_system (format_cmd, format_status); } browser_list = xstrdup (html_pager); for (candidate = strtok (browser_list, ":"); candidate; candidate = strtok (NULL, ":")) { pipeline *browser; debug ("Trying browser: %s\n", candidate); browser = make_browser (candidate, htmlfile); disp_status = pipeline_run (browser); if (!disp_status) break; } if (!candidate) { if (html_pager && *html_pager) error (CHILD_FAIL, 0, "couldn't execute any browser from %s", html_pager); else error (CHILD_FAIL, 0, "no browser configured, so cannot show " "HTML output"); } else if (!disp_status) sleep (5); /* firefox runs into background too fast */ free (browser_list); if (remove_directory (htmldir, false) == -1) error (0, errno, _("can't remove directory %s"), htmldir); free (htmlfile); free (htmldir); } else #endif /* TROFF_IS_GROFF */ { if (format_status && format_status != (SIGPIPE + 0x80) * 256) gripe_system (format_cmd, format_status); if (disp_status && disp_status != (SIGPIPE + 0x80) * 256) gripe_system (disp_cmd, disp_status); } regain_effective_privs (); } /* "Display" a page in catman mode, which amounts to saving it. */ /* TODO: merge with format_display_and_save? */ static void display_catman (const char *cat_file, decompress *d, pipeline *format_cmd, const char *encoding) { char *tmpcat = tmp_cat_filename (cat_file); pipeline *decomp = decompress_get_pipeline (d); #ifdef COMP_CAT pipecmd *comp_cmd; #endif /* COMP_CAT */ int status; add_output_iconv (format_cmd, encoding, "UTF-8"); #ifdef COMP_CAT comp_cmd = pipecmd_new_argstr (get_def ("compressor", PROG_COMPRESSOR)); pipecmd_pre_exec (comp_cmd, sandbox_load, sandbox_free, sandbox); pipeline_command (format_cmd, comp_cmd); #endif /* COMP_CAT */ maybe_discard_stderr (format_cmd); pipeline_want_out (format_cmd, tmp_cat_fd); push_cleanup (cleanup_unlink, tmpcat, 1); /* save the cat as real user * (1) required for user man hierarchy * (2) else depending on ruid's privs is ok, effectively disables * catman for non-root. */ drop_effective_privs (); pipeline_connect (decomp, format_cmd, (void *) 0); pipeline_pump (decomp, format_cmd, (void *) 0); pipeline_wait (decomp); status = pipeline_wait (format_cmd); regain_effective_privs (); if (status) gripe_system (format_cmd, status); close (tmp_cat_fd); commit_tmp_cat (cat_file, tmpcat, status); pop_cleanup (cleanup_unlink, tmpcat); free (tmpcat); } #ifndef TROFF_IS_GROFF static void disable_hyphenation (void *data MAYBE_UNUSED) { fputs (".nh\n" ".de hy\n" "..\n" ".lf 1\n", stdout); } #endif /* TROFF_IS_GROFF */ static void disable_justification (void *data MAYBE_UNUSED) { fputs (".ie (\\n(.g&((\\n(.x>1):((\\n(.x==1)&(\\n(.y>=23))))" " .ds AD l\n" ".el \\{\\\n" ". na\n" ". de ad\n" ". .\n" ".\\}\n" ".lf 1\n", stdout); } #ifdef TROFF_IS_GROFF static void locale_macros (void *data) { const char *macro_lang = data; const char *hyphen_lang = STREQ (lang, "en") ? "us" : macro_lang; debug ("Macro language %s; hyphenation language %s\n", macro_lang, hyphen_lang); printf ( /* If we're using groff >= 1.20.2 (for the 'file' warning * category): */ ".if \\n[.g] \\{\\\n" ". ds Ystring \\n[.Y]\n" ". while (\\B'\\*[Ystring]' = 0) .chop Ystring\n" ". if ((\\n[.x] > 1) :" " ((\\n[.x] == 1) & (\\n[.y] > 20)) :" " ((\\n[.x] == 1) & (\\n[.y] == 20) & (\\*[Ystring] >= 2))) " "\\{\\\n" /* disable warnings of category 'file' */ ". warn (\\n[.warn] -" " (\\n[.warn] / 1048576 %% 2 * 1048576))\n" /* and load the appropriate per-locale macros */ ". mso %s.tmac\n" ". \\}\n" ". rm Ystring\n" ".\\}\n" /* set the hyphenation language anyway, to make sure groff * only hyphenates languages it knows about */ ".hla %s\n" ".lf 1\n", macro_lang, hyphen_lang); } #endif /* TROFF_IS_GROFF */ /* allow user to skip a page or quit after viewing desired page return 1 to skip return 0 to view */ static int do_prompt (const char *name) { int ch; FILE *tty = NULL; skip = false; if (!isatty (STDOUT_FILENO) || !isatty (STDIN_FILENO)) return 0; /* noninteractive */ tty = fopen ("/dev/tty", "r+"); if (!tty) return 0; fprintf (tty, _( "--Man-- next: %s " "[ view (return) | skip (Ctrl-D) | quit (Ctrl-C) ]\n"), name); fflush (tty); do { ch = getc (tty); switch (ch) { case '\n': fclose (tty); return 0; case EOF: skip = true; fclose (tty); return 1; default: break; } } while (1); fclose (tty); return 0; } /* * optionally chdir to dir, if necessary update cat_file from man_file * and display it. if man_file is NULL cat_file is a stray cat. If * !save_cat or cat_file is NULL we must not save the formatted cat. * If man_file is "" this is a special case -- we expect the man page * on standard input. */ static int display (const char *dir, const char *man_file, const char *cat_file, const char *title, const char *dbfilters) { int found; static int prompt; int prefixes = 0; pipeline *format_cmd; /* command to format man_file to stdout */ char *formatted_encoding = NULL; bool display_to_stdout; decompress *decomp = NULL; int decomp_errno = 0; /* define format_cmd */ if (man_file) { pipecmd *seq = pipecmd_new_sequence ("decompressor", (void *) 0); if (*man_file) decomp = decompress_open (man_file, 0); else decomp = decompress_fdopen (dup (STDIN_FILENO)); #ifndef TROFF_IS_GROFF /* See also make_roff_command () for the simpler groff case. */ if (!recode && no_hyphenation) { pipecmd *hcmd = pipecmd_new_function ( "echo .nh && echo .de hy && echo .. && " "echo .lf 1", disable_hyphenation, NULL, NULL); pipecmd_sequence_command (seq, hcmd); ++prefixes; } #endif /* TROFF_IS_GROFF */ if (!recode && no_justification) { pipecmd *jcmd = pipecmd_new_function ( #ifdef TROFF_IS_GROFF /* Technically only for groff >= 1.23.0. */ "echo .ds AD l && echo .lf 1", #else /* !TROFF_IS_GROFF */ "echo .na && echo .de ad && echo .. && " "echo .lf 1", #endif /* TROFF_IS_GROFF */ disable_justification, NULL, NULL); pipecmd_sequence_command (seq, jcmd); ++prefixes; } #ifdef TROFF_IS_GROFF /* This only works with preconv, since the per-locale macros * may change the assumed input encoding. */ if (!recode && *man_file && get_groff_preconv ()) { char *page_lang = lang_dir (man_file); if (page_lang && *page_lang && !STREQ (page_lang, "C")) { struct locale_bits bits; char *name; pipecmd *lcmd; unpack_locale_bits (page_lang, &bits); name = xasprintf ("echo .mso %s.tmac && " "echo .lf 1", bits.language); lcmd = pipecmd_new_function ( name, locale_macros, free, xstrdup (bits.language)); pipecmd_sequence_command (seq, lcmd); ++prefixes; free (name); free_locale_bits (&bits); } free (page_lang); } #endif /* TROFF_IS_GROFF */ if (prefixes) { pipeline *decomp_p = decompress_get_pipeline (decomp); assert (pipeline_get_ncommands (decomp_p) <= 1); if (pipeline_get_ncommands (decomp_p)) { pipecmd_sequence_command (seq, pipeline_get_command (decomp_p, 0)); pipeline_set_command (decomp_p, 0, seq); } else { pipecmd_sequence_command (seq, pipecmd_new_passthrough ()); pipeline_command (decomp_p, seq); } } else pipecmd_free (seq); } if (decomp) { char *pp_string; decompress_start (decomp); pp_string = get_preprocessors (decomp, dbfilters, prefixes); format_cmd = make_roff_command (dir, man_file, decomp, pp_string, &formatted_encoding); if (dir) chdir_commands (format_cmd, dir); debug ("formatted_encoding = %s\n", formatted_encoding); free (pp_string); } else { format_cmd = NULL; decomp_errno = errno; } /* Get modification time, for commit_tmp_cat(). */ if (man_file && *man_file) { struct stat stb; if (stat (man_file, &stb)) { man_modtime.tv_sec = 0; man_modtime.tv_nsec = 0; } else man_modtime = get_stat_mtime (&stb); } display_to_stdout = troff; #ifdef TROFF_IS_GROFF if (htmlout || gxditview) display_to_stdout = false; #endif if (recode) display_to_stdout = true; if (display_to_stdout) { /* If we're reading stdin via '-l -', man_file is "". See * below. */ assert (man_file); if (!decomp) { assert (!format_cmd); /* no need to free it */ error (0, decomp_errno, _("can't open %s"), man_file); return 0; } if (*man_file == '\0') found = 1; else found = CAN_ACCESS (man_file, R_OK); if (found) { pipeline *decomp_p = decompress_get_pipeline (decomp); int status; if (prompt && do_prompt (title)) { pipeline_free (format_cmd); decompress_free (decomp); free (formatted_encoding); return 0; } drop_effective_privs (); pipeline_connect (decomp_p, format_cmd, (void *) 0); pipeline_pump (decomp_p, format_cmd, (void *) 0); pipeline_wait (decomp_p); status = pipeline_wait (format_cmd); regain_effective_privs (); if (status != 0) gripe_system (format_cmd, status); } } else { bool format = true; int status; /* The caller should already have checked for any * FSSTND-style (same hierarchy) cat page that may be * present, and we don't expect to have to update the cat * page in that case. If by some chance we do have to update * it, then there's no harm trying; open_cat_stream() will * refuse gracefully if the file isn't writeable. */ /* In theory we might be able to get away with saving cats * for want_encoding, but it does change the roff device so * perhaps that's best avoided. */ if (want_encoding #ifdef TROFF_IS_GROFF || htmlout || gxditview #endif || local_man_file || recode || disable_cache || no_hyphenation || no_justification) save_cat = false; if (!man_file) { /* Stray cat. */ assert (cat_file); format = false; } else if (!cat_file) { assert (man_file); save_cat = false; format = true; } else if (format && save_cat) { char *cat_dir; char *tmp; status = is_changed (man_file, cat_file); format = (status == -2) || ((status & 1) == 1); /* don't save if we haven't a cat directory */ cat_dir = xstrdup (cat_file); tmp = strrchr (cat_dir, '/'); if (tmp) *tmp = 0; save_cat = is_directory (cat_dir) == 1; if (!save_cat) debug ("cat dir %s does not exist\n", cat_dir); free (cat_dir); } if (format && (!format_cmd || !decomp)) { assert (man_file); /* format_cmd is NULL iff decomp is NULL; no need to * free either of them. */ assert (!format_cmd); assert (!decomp); error (0, decomp_errno, _("can't open %s"), man_file); return 0; } /* if we're trying to read stdin via '-l -' then man_file * will be "" which access() obviously barfs on, but all is * well because the format_cmd will have been created to * expect input via stdin. So we special-case this to avoid * the bogus access() check. */ if (format && *man_file == '\0') found = 1; else found = CAN_ACCESS (format ? man_file : cat_file, R_OK); debug ("format: %d, save_cat: %d, found: %d\n", (int) format, (int) save_cat, found); if (!found) { pipeline_free (format_cmd); decompress_free (decomp); return found; } if (print_where || print_where_cat) { bool printed = false; if (print_where && man_file) { printf ("%s", man_file); printed = true; } if (print_where_cat && cat_file && !format) { if (printed) putchar (' '); printf ("%s", cat_file); printed = true; } if (printed) putchar ('\n'); } else if (catman) { if (format) { if (!save_cat) error (0, 0, _("\ncannot write to " "%s in catman mode"), cat_file); else display_catman (cat_file, decomp, format_cmd, formatted_encoding); } } else if (format) { /* no cat or out of date */ pipeline *disp_cmd; if (prompt && do_prompt (title)) { pipeline_free (format_cmd); decompress_free (decomp); free (formatted_encoding); if (local_man_file) return 1; else return 0; } disp_cmd = make_display_command (formatted_encoding, title); #ifdef MAN_CATS if (save_cat) { /* save cat */ assert (disp_cmd); /* not htmlout for now */ format_display_and_save (decomp, format_cmd, disp_cmd, cat_file, formatted_encoding); } else #endif /* MAN_CATS */ /* don't save cat */ format_display (decomp, format_cmd, disp_cmd, man_file); pipeline_free (disp_cmd); } else { /* display preformatted cat */ pipeline *disp_cmd; decompress *decomp_cat; if (prompt && do_prompt (title)) { pipeline_free (format_cmd); decompress_free (decomp); return 0; } decomp_cat = decompress_open (cat_file, 0); if (!decomp_cat) { error (0, errno, _("can't open %s"), cat_file); pipeline_free (format_cmd); decompress_free (decomp); return 0; } disp_cmd = make_display_command ("UTF-8", title); format_display (decomp_cat, NULL, disp_cmd, man_file); pipeline_free (disp_cmd); decompress_free (decomp_cat); } } free (formatted_encoding); pipeline_free (format_cmd); decompress_free (decomp); if (!prompt) prompt = found; return found; } static _Noreturn void gripe_converting_name (const char *name) { fatal (0, _("Can't convert %s to cat name"), name); } /* Convert the trailing part of 'name' to be a cat page path by altering its * extension appropriately. If fsstnd is set, also try converting the * containing directory name from "man1" to "cat1" etc., returning NULL if * that doesn't work. * * fsstnd should only be set if name is the original path of a man page * found in a man hierarchy, not something like a symlink target or a file * named with 'man -l'. Otherwise, a symlink to "/home/manuel/foo.1.gz" * would be converted to "/home/catuel/foo.1.gz", which would be bad. */ static char *convert_name (const char *name, bool fsstnd) { char *to_name, *t1 = NULL; char *t2 = NULL; struct compression *comp; char *namestem; comp = comp_info (name, true); if (comp) namestem = comp->stem; else namestem = xstrdup (name); #ifdef COMP_CAT /* TODO: BSD layout requires .0. */ to_name = xasprintf ("%s.%s", namestem, COMPRESS_EXT); #else /* !COMP_CAT */ to_name = xstrdup (namestem); #endif /* COMP_CAT */ free (namestem); if (fsstnd) { t1 = strrchr (to_name, '/'); if (!t1) gripe_converting_name (name); *t1 = '\0'; t2 = strrchr (to_name, '/'); if (!t2) gripe_converting_name (name); ++t2; *t1 = '/'; if (STRNEQ (t2, "man", 3)) { /* If the second-last component starts with "man", * replace "man" with "cat". */ *t2 = 'c'; *(t2 + 2) = 't'; } else { free (to_name); debug ("couldn't convert %s to FSSTND cat file\n", name); return NULL; } } debug ("converted %s to %s\n", name, to_name); return to_name; } static char *find_cat_file (const char *path, const char *original, const char *man_file) { size_t path_len = strlen (path); char *cat_file, *cat_path; int status; /* Try the FSSTND way first, namely a cat page in the same hierarchy * as the original path to the man page. We don't create these * unless no alternate cat hierarchy is available, but will use them * if they happen to exist already and have the same timestamp as * the corresponding man page. (In practice I'm betting that this * means we'll hardly ever use them at all except for user * hierarchies; but compatibility, eh?) */ cat_file = convert_name (original, true); if (cat_file) { status = is_changed (original, cat_file); if (status != -2 && (!(status & 1)) == 1) { debug ("found valid FSSTND cat file %s\n", cat_file); return cat_file; } free (cat_file); } /* Otherwise, find the cat page we actually want to use or create, * taking any alternate cat hierarchy into account. If the original * path and man_file differ (i.e. original was a symlink or .so * link), try the link target and then the source. */ if (!STREQ (man_file, original)) { global_manpath = is_global_mandir (man_file); cat_path = get_catpath (man_file, global_manpath ? SYSTEM_CAT : USER_CAT); if (cat_path) { cat_file = convert_name (cat_path, false); free (cat_path); } else if (STRNEQ (man_file, path, path_len) && man_file[path_len] == '/') cat_file = convert_name (man_file, true); else cat_file = NULL; if (cat_file) { char *cat_dir = xstrdup (cat_file); char *tmp = strrchr (cat_dir, '/'); if (tmp) *tmp = 0; if (is_directory (cat_dir)) { debug ("will try cat file %s\n", cat_file); free (cat_dir); return cat_file; } else debug ("cat dir %s does not exist\n", cat_dir); free (cat_dir); } else debug ("no cat path for %s\n", man_file); } global_manpath = is_global_mandir (original); cat_path = get_catpath (original, global_manpath ? SYSTEM_CAT : USER_CAT); if (cat_path) { cat_file = convert_name (cat_path, false); free (cat_path); } else cat_file = convert_name (original, true); if (cat_file) debug ("will try cat file %s\n", cat_file); else debug ("no cat path for %s\n", original); return cat_file; } static int get_ult_flags (char from_db, char id) { if (!from_db) return ult_flags; else if (id == ULT_MAN) /* Checking .so links is expensive, as we have to open the * file. Therefore, if the database lists it as ULT_MAN, * that's good enough for us and we won't recheck that. This * does mean that if a page changes from ULT_MAN to SO_MAN * then you might get duplicates until mandb is next run, * but that isn't a big deal. */ return ult_flags & ~SO_LINK; else return ult_flags; } /* Is this candidate substantially a duplicate of a previous one? * Returns true if so, otherwise false. */ static bool duplicate_candidates (struct candidate *left, struct candidate *right) { const char *slash1, *slash2; struct locale_bits bits1, bits2; bool ret; if (left->ult && right->ult && STREQ (left->ult, right->ult)) return true; /* same ultimate source file */ if (!STREQ (left->source->name, right->source->name) || !STREQ (left->source->sec, right->source->sec) || !STREQ (left->source->ext, right->source->ext)) return false; /* different name/section/extension */ if (STREQ (left->path, right->path)) return true; /* same path */ /* Figure out if we've had a sufficiently similar candidate for this * language already. */ slash1 = strrchr (left->path, '/'); slash2 = strrchr (right->path, '/'); if (!slash1 || !slash2 || !STRNEQ (left->path, right->path, MAX (slash1 - left->path, slash2 - right->path))) return false; /* different path base */ unpack_locale_bits (++slash1, &bits1); unpack_locale_bits (++slash2, &bits2); if (!STREQ (bits1.language, bits2.language) || !STREQ (bits1.territory, bits2.territory) || !STREQ (bits1.modifier, bits2.modifier)) ret = false; /* different language/territory/modifier */ else /* Everything seems to be the same; we can find nothing to * choose between them. */ ret = true; free_locale_bits (&bits1); free_locale_bits (&bits2); return ret; } /* Return zero if the candidates are in different base paths or if both * candidates or neither are in OVERRIDE_DIR relative to their base path; * negative if left is in OVERRIDE_DIR and right is not; or positive if * right is in OVERRIDE_DIR and left is not. */ static int compare_override_dir (const struct candidate *left, const struct candidate *right) { size_t left_len, right_len; if (!*OVERRIDE_DIR) return 0; left_len = strlen (left->path); right_len = strlen (right->path); if (left_len == right_len || !STRNEQ (left->path, right->path, MIN (left_len, right_len))) return 0; if (left_len > right_len) { if (left->path[right_len] == '/' && STREQ (left->path + right_len + 1, OVERRIDE_DIR)) return -1; } else { if (right->path[left_len] == '/' && STREQ (right->path + left_len + 1, OVERRIDE_DIR)) return 1; } return 0; } static int compare_candidates (const struct candidate *left, const struct candidate *right) { const struct mandata *lsource = left->source, *rsource = right->source; int cmp; const char *slash1, *slash2; /* If one candidate matches the requested name exactly, sort it * first. This makes --ignore-case behave more sensibly. */ /* name is never NULL here, see add_candidate() */ if (STREQ (lsource->name, left->req_name)) { if (!STREQ (rsource->name, right->req_name)) return -1; } else { if (STREQ (rsource->name, right->req_name)) return 1; } /* ULT_MAN comes first, etc. Consider SO_MAN equivalent to ULT_MAN. * This has the effect of sorting mere whatis references below real * pages. */ cmp = compare_ids (lsource->id, rsource->id, true); if (cmp) return cmp; /* Compare pure sections first, then extensions. * * Any extension spelt out in full in section_list effectively * becomes a pure section; this allows extensions to be selectively * moved out of order with respect to their parent sections. */ if (strcmp (lsource->ext, rsource->ext)) { size_t index_left, index_right; /* If the user asked for an explicit section, sort exact * matches first. */ if (section) { if (STREQ (lsource->ext, section)) { if (!STREQ (rsource->ext, section)) return -1; } else { if (STREQ (rsource->ext, section)) return 1; } } /* Find out whether lsource->ext is ahead of rsource->ext in * section_list. Sections missing from section_list are * sorted to the end. */ index_left = gl_list_indexof (section_list, lsource->ext); if (index_left == (size_t) -1 && strlen (lsource->ext) > 1) { char *sec_left = xstrndup (lsource->ext, 1); index_left = gl_list_indexof (section_list, sec_left); free (sec_left); if (index_left == (size_t) -1) index_left = gl_list_size (section_list); } index_right = gl_list_indexof (section_list, rsource->ext); if (index_right == (size_t) -1 && strlen (rsource->ext) > 1) { char *sec_right = xstrndup (rsource->ext, 1); index_right = gl_list_indexof (section_list, sec_right); free (sec_right); if (index_right == (size_t) -1) index_right = gl_list_size (section_list); } if (index_left < index_right) return -1; else if (index_left > index_right) return 1; cmp = strcmp (lsource->sec, rsource->sec); if (cmp) return cmp; } /* The order in section_list has already been compared above. For * everything not mentioned explicitly there, we just compare * lexically. */ cmp = strcmp (lsource->ext, rsource->ext); if (cmp) return cmp; /* If one candidate is in OVERRIDE_DIR within the same base path as * the other candidate, then the candidate in OVERRIDE_DIR comes * first. */ cmp = compare_override_dir (left, right); if (cmp) return cmp; /* Try comparing based on language. We used to prefer to display a * page in the user's preferred language than a page from a better * section, but that attracted objections, so now we prefer to get * the section right. See Debian bug #519547. */ slash1 = strrchr (left->path, '/'); slash2 = strrchr (right->path, '/'); if (slash1 && slash2) { char *locale_copy, *p; struct locale_bits bits1, bits2, lbits; const char *codeset1, *codeset2; unpack_locale_bits (++slash1, &bits1); unpack_locale_bits (++slash2, &bits2); /* We need the current locale as well. */ locale_copy = xstrdup (internal_locale); p = strchr (locale_copy, ':'); if (p) *p = '\0'; unpack_locale_bits (locale_copy, &lbits); free (locale_copy); #define COMPARE_LOCALE_ELEMENTS(elt) do { \ /* For different elements, prefer one that matches the locale if * possible. */ \ if (*lbits.elt) { \ if (STREQ (lbits.elt, bits1.elt)) { \ if (!STREQ (lbits.elt, bits2.elt)) { \ cmp = -1; \ goto out; \ } \ } else { \ if (STREQ (lbits.elt, bits2.elt)) { \ cmp = 1; \ goto out; \ } \ } \ } \ cmp = strcmp (bits1.elt, bits2.elt); \ if (cmp) \ /* No help from locale; might as well sort lexically. */ \ goto out; \ } while (0) COMPARE_LOCALE_ELEMENTS (language); COMPARE_LOCALE_ELEMENTS (territory); COMPARE_LOCALE_ELEMENTS (modifier); #undef COMPARE_LOCALE_ELEMENTS /* Prefer UTF-8 if available. Otherwise, consider them * equal. */ codeset1 = get_canonical_charset_name (bits1.codeset); codeset2 = get_canonical_charset_name (bits2.codeset); if (STREQ (codeset1, "UTF-8")) { if (!STREQ (codeset2, "UTF-8")) { cmp = -1; goto out; } } else { if (STREQ (codeset2, "UTF-8")) { cmp = 1; goto out; } } out: free_locale_bits (&lbits); free_locale_bits (&bits1); free_locale_bits (&bits2); if (cmp) return cmp; } /* Explicitly stabilise the sort as a last resort, so that manpath * ordering (e.g. language-specific hierarchies) works. */ if (left->add_index < right->add_index) return -1; else if (left->add_index > right->add_index) return 1; else return 0; return 0; } static int compare_candidates_qsort (const void *l, const void *r) { const struct candidate *left = *(const struct candidate **)l; const struct candidate *right = *(const struct candidate **)r; return compare_candidates (left, right); } static void free_candidate (struct candidate *candidate) { if (candidate) free (candidate->ult); free (candidate); } /* Add an entry to the list of candidates. */ static int add_candidate (struct candidate **head, char from_db, char cat, const char *req_name, const char *path, const char *ult, struct mandata *source) { struct candidate *search, *prev, *insert, *candp; static int add_index = 0; if (!ult) { const char *name; char *filename; const struct ult_value *ult_value; if (*source->pointer != '-') name = source->pointer; else if (source->name) name = source->name; else name = req_name; filename = make_filename (path, name, source, cat ? "cat" : "man"); if (!filename) return 0; ult_value = ult_src (filename, path, NULL, get_ult_flags (from_db, source->id)); if (ult_value) ult = ult_value->path; free (filename); } debug ("candidate: %d %d %s %s %s %c %s %s %s\n", from_db, cat, req_name, path, ult, source->id, source->name ? source->name : "-", source->sec, source->ext); if (!source->name) source->name = xstrdup (req_name); candp = XMALLOC (struct candidate); candp->req_name = req_name; candp->from_db = from_db; candp->cat = cat; candp->path = path; candp->ult = ult ? xstrdup (ult) : NULL; candp->source = source; candp->add_index = add_index++; candp->next = NULL; /* insert will be NULL (insert at start) or a pointer to the element * after which this element should be inserted. */ insert = NULL; search = *head; prev = NULL; /* This search produces quadratic-time behaviour, although in * practice it doesn't seem to be too bad at the moment since the * run-time is dominated by calls to ult_src. In future it might be * worth optimising this; the reason I haven't done this yet is that * it involves quite a bit of tedious bookkeeping. A practical * approach would be to keep two hashes, one that's just a set to * keep track of whether candp->ult has been seen already, and one * that keeps a list of candidates for each candp->name that could * then be quickly checked by brute force. */ while (search) { bool dupcand = duplicate_candidates (candp, search); debug ("search: %d %d %s %s %s %c %s %s %s " "(dup: %d)\n", search->from_db, search->cat, search->req_name, search->path, search->ult, search->source->id, search->source->name ? search->source->name : "-", search->source->sec, search->source->ext, (int) dupcand); /* Check for duplicates. */ if (dupcand) { int cmp = compare_candidates (candp, search); if (cmp >= 0) { debug ("other duplicate is at least as " "good\n"); free_candidate (candp); return 0; } else { debug ("this duplicate is better; removing " "old one\n"); if (prev) { prev->next = search->next; free_candidate (search); search = prev->next; } else { *head = search->next; free_candidate (search); search = *head; } continue; } } prev = search; if (search->next) search = search->next; else break; } /* Insert the new candidate at the end of the list (having had to go * through them all looking for duplicates anyway); we'll sort it * into place later. */ insert = prev; candp->next = insert ? insert->next : *head; if (insert) insert->next = candp; else *head = candp; return 1; } /* Sort the entire list of candidates. */ static void sort_candidates (struct candidate **candidates) { struct candidate *cand, **allcands; size_t count = 0, i; for (cand = *candidates; cand; cand = cand->next) ++count; if (count == 0) return; allcands = XNMALLOC (count, struct candidate *); i = 0; for (cand = *candidates; cand; cand = cand->next) { assert (i < count); allcands[i++] = cand; } assert (i == count); qsort (allcands, count, sizeof *allcands, compare_candidates_qsort); *candidates = cand = allcands[0]; for (i = 1; i < count; ++i) { cand->next = allcands[i]; cand = cand->next; } cand->next = NULL; free (allcands); } /* * See if the preformatted man page or the source exists in the given * section. */ static int try_section (const char *path, const char *sec, const char *name, struct candidate **cand_head) { int found = 0; gl_list_t names = NULL; const char *found_name; char cat = 0; int lff_opts = (match_case ? LFF_MATCHCASE : 0) | (regex_opt ? LFF_REGEX : 0) | (wildcard ? LFF_WILDCARD : 0); debug ("trying section %s with globbing\n", sec); #ifndef NROFF_MISSING /* #ifdef PROG_NROFF */ /* * Look for man page source files. */ names = look_for_file (path, sec, name, false, lff_opts); if (!gl_list_size (names)) /* * No files match. * See if there's a preformatted page around that * we can display. */ #endif /* NROFF_MISSING */ { if (catman) return 1; if (!troff && !want_encoding && !recode) { if (names) gl_list_free (names); names = look_for_file (path, sec, name, true, lff_opts); cat = 1; } } if (!names) return 0; order_files (path, &names); GL_LIST_FOREACH (names, found_name) { struct mandata *info = filename_info (found_name, quiet < 2); const struct ult_value *ult; int f; if (!info) continue; /* What kind of page is this? Since it's a real file, it * must be either ULT_MAN or SO_MAN. ult_src() can tell us * which. */ ult = ult_src (found_name, path, NULL, ult_flags); if (!ult) { /* already warned */ debug ("try_section(): bad link %s\n", found_name); free_mandata_struct (info); continue; } if (STREQ (ult->path, found_name)) info->id = ULT_MAN; else info->id = SO_MAN; f = add_candidate (cand_head, CANDIDATE_FILESYSTEM, cat, name, path, ult->path, info); found += f; /* Free info if it wasn't added to the candidates. */ if (f == 0) free_mandata_struct (info); /* Don't free info here. */ } gl_list_free (names); return found; } static int display_filesystem (struct candidate *candp) { char *filename = make_filename (candp->path, NULL, candp->source, candp->cat ? "cat" : "man"); char *title; int found = 0; if (!filename) return 0; /* source->name is never NULL thanks to add_candidate() */ title = xasprintf ("%s(%s)", candp->source->name, candp->source->ext); if (candp->cat) { if (troff || want_encoding || recode) goto out; found = display (candp->path, NULL, filename, title, NULL); } else { const struct ult_value *man_ult; char *cat_file; man_ult = ult_src (filename, candp->path, NULL, ult_flags); if (!man_ult) goto out; debug ("found ultimate source file %s\n", man_ult->path); lang = lang_dir (man_ult->path); cat_file = find_cat_file (candp->path, filename, man_ult->path); found = display (candp->path, man_ult->path, cat_file, title, NULL); free (cat_file); free (lang); lang = NULL; } out: free (title); free (filename); return found; } #ifdef MAN_DB_UPDATES /* wrapper to dbdelete which deals with opening/closing the db */ static void dbdelete_wrapper (const char *page, struct mandata *info, const char *manpath) { if (!catman) { char *catpath, *database; MYDBM_FILE dbf; catpath = get_catpath (manpath, global_manpath ? SYSTEM_CAT : USER_CAT); database = mkdbname (catpath ? catpath : manpath); dbf = MYDBM_NEW (database); if (MYDBM_RWOPEN (dbf)) { if (dbdelete (dbf, page, info) == 1) debug ("%s(%s) not in db!\n", page, info->ext); } MYDBM_FREE (dbf); free (database); free (catpath); } } #endif /* MAN_DB_UPDATES */ /* This started out life as try_section, but a lot of that routine is redundant wrt the db cache. */ static int display_database (struct candidate *candp) { int found = 0; char *file; const char *name; char *title; struct mandata *in = candp->source; debug ("trying a db located file.\n"); dbprintf (in); /* if the pointer holds some data, this is a reference to the real page, use that instead. */ if (*in->pointer != '-') name = in->pointer; else if (in->name) name = in->name; else name = candp->req_name; if (in->id == WHATIS_MAN || in->id == WHATIS_CAT) debug (_("%s: relying on whatis refs is deprecated\n"), name); title = xasprintf ("%s(%s)", in->name ? in->name : candp->req_name, in->ext); #ifndef NROFF_MISSING /* #ifdef PROG_NROFF */ /* * Look for man page source files. */ if (in->id < STRAY_CAT) { /* There should be a src page */ file = make_filename (candp->path, name, in, "man"); if (file) { const struct ult_value *man_ult; char *cat_file; man_ult = ult_src (file, candp->path, NULL, get_ult_flags (1, in->id)); if (!man_ult) { free (title); return found; /* zero */ } debug ("found ultimate source file %s\n", man_ult->path); lang = lang_dir (man_ult->path); cat_file = find_cat_file (candp->path, file, man_ult->path); found += display (candp->path, man_ult->path, cat_file, title, in->filter); free (cat_file); free (lang); lang = NULL; free (file); } /* else {drop through to the bottom and return 0 anyway} */ } else #endif /* NROFF_MISSING */ if (in->id <= WHATIS_CAT) { /* The db says we have a stray cat or whatis ref */ if (catman) { free (title); return ++found; } /* If explicitly asked for troff or a different encoding, * don't show a stray cat. */ if (troff || want_encoding || recode) { free (title); return found; } file = make_filename (candp->path, name, in, "cat"); if (!file) { char *catpath; catpath = get_catpath (candp->path, global_manpath ? SYSTEM_CAT : USER_CAT); if (catpath && strcmp (catpath, candp->path) != 0) { file = make_filename (catpath, name, in, "cat"); free (catpath); if (!file) { /* don't delete here, return==0 will do that */ free (title); return found; /* zero */ } } else { free (catpath); free (title); return found; /* zero */ } } found += display (candp->path, NULL, file, title, in->filter); free (file); } free (title); return found; } /* test for existence, if fail: call dbdelete_wrapper, else return amount */ static int display_database_check (struct candidate *candp) { int exists = display_database (candp); #ifdef MAN_DB_UPDATES if (!exists && !skip) { debug ("dbdelete_wrapper (%s, %p, %s)\n", candp->req_name, candp->source, candp->path); dbdelete_wrapper (candp->req_name, candp->source, candp->path); } #endif /* MAN_DB_UPDATES */ return exists; } #ifdef MAN_DB_UPDATES static int maybe_update_file (const char *manpath, const char *name, struct mandata *info) { const char *real_name; char *file; struct stat buf; struct timespec file_mtime; int status; if (!update) return 0; /* If the pointer holds some data, then we need to look at that * name in the filesystem instead. */ if (!STRNEQ (info->pointer, "-", 1)) real_name = info->pointer; else if (info->name) real_name = info->name; else real_name = name; file = make_filename (manpath, real_name, info, "man"); if (!file) return 0; if (lstat (file, &buf) != 0) return 0; file_mtime = get_stat_mtime (&buf); if (timespec_cmp (file_mtime, info->mtime) == 0) return 0; debug ("%s needs to be recached: %ld.%09ld %ld.%09ld\n", file, (long) info->mtime.tv_sec, (long) info->mtime.tv_nsec, (long) file_mtime.tv_sec, (long) file_mtime.tv_nsec); status = run_mandb (false, manpath, file); if (status) error (0, 0, _("mandb command failed with exit status %d"), status); free (file); return 1; } #endif /* MAN_DB_UPDATES */ /* Special return values from try_db(). */ #define TRY_DATABASE_OPEN_FAILED -1 #ifdef MAN_DB_CREATES #define TRY_DATABASE_CREATED -2 #endif /* MAN_DB_CREATES */ #ifdef MAN_DB_UPDATES #define TRY_DATABASE_UPDATED -3 #endif /* MAN_DB_UPDATES */ static void db_map_value_free (const void *value) { /* The value may be NULL to indicate that opening the database at * this location already failed. */ if (value) gl_list_free ((gl_list_t) value); } /* Look for a page in the database. If db not accessible, return -1, otherwise return number of pages found. */ static int try_db (const char *manpath, const char *sec, const char *name, struct candidate **cand_head) { gl_list_t matches; struct mandata *loc; char *catpath, *database; MYDBM_FILE dbf = NULL; int found = 0; #ifdef MAN_DB_UPDATES bool found_stale = false; #endif /* MAN_DB_UPDATES */ /* find out where our db for this manpath should be */ catpath = get_catpath (manpath, global_manpath ? SYSTEM_CAT : USER_CAT); database = mkdbname (catpath ? catpath : manpath); if (!db_map) db_map = new_string_map (GL_HASH_MAP, db_map_value_free); /* If we haven't looked here already, do so now. */ if (!gl_map_search (db_map, manpath, (const void **) &matches)) { dbf = MYDBM_NEW (database); if (MYDBM_RDOPEN (dbf) && !dbver_rd (dbf)) { debug ("Succeeded in opening %s O_RDONLY\n", database); /* if section is set, only return those that match, otherwise NULL retrieves all available */ if (regex_opt || wildcard) matches = dblookup_pattern (dbf, name, section, match_case, regex_opt, !names_only); else matches = dblookup_all (dbf, name, section, match_case); gl_map_put (db_map, xstrdup (manpath), matches); #ifdef MAN_DB_CREATES } else if (!global_manpath) { /* create one */ debug ("Failed to open %s O_RDONLY\n", database); if (run_mandb (true, manpath, NULL)) { gl_map_put (db_map, xstrdup (manpath), NULL); found = TRY_DATABASE_OPEN_FAILED; goto out; } found = TRY_DATABASE_CREATED; goto out; #endif /* MAN_DB_CREATES */ } else { debug ("Failed to open %s O_RDONLY\n", database); gl_map_put (db_map, xstrdup (manpath), NULL); found = TRY_DATABASE_OPEN_FAILED; goto out; } assert (matches != NULL); } /* We already tried (and failed) to open this db before. */ if (!matches) { found = TRY_DATABASE_OPEN_FAILED; goto out; } #ifdef MAN_DB_UPDATES /* Check that all the entries found are up to date. If not, the * caller should try again. */ GL_LIST_FOREACH (matches, loc) if (STREQ (sec, loc->sec) && (!extension || STREQ (extension, loc->ext) || STREQ (extension, loc->ext + strlen (sec)))) if (maybe_update_file (manpath, name, loc)) found_stale = true; if (found_stale) { gl_map_remove (db_map, manpath); found = TRY_DATABASE_UPDATED; goto out; } #endif /* MAN_DB_UPDATES */ /* cycle through the mandata structures (there's usually only 1 or 2) and see what we have w.r.t. the current section */ GL_LIST_FOREACH (matches, loc) if (STREQ (sec, loc->sec) && (!extension || STREQ (extension, loc->ext) || STREQ (extension, loc->ext + strlen (sec)))) found += add_candidate (cand_head, CANDIDATE_DATABASE, 0, name, manpath, NULL, loc); out: MYDBM_FREE (dbf); free (database); free (catpath); return found; } /* Try to locate the page under the specified manpath, in the desired section, * with the supplied name. Glob if necessary. Initially search the filesystem; * if that fails, try finding it via a db cache access. */ static int locate_page (const char *manpath, const char *sec, const char *name, struct candidate **candidates) { int found, db_ok; /* sort out whether we want to treat this hierarchy as global or user. Differences: global: if setuid, use privs; don't create db. user : if setuid, drop privs; allow db creation. */ global_manpath = is_global_mandir (manpath); if (!global_manpath) drop_effective_privs (); debug ("searching in %s, section %s\n", manpath, sec); found = try_section (manpath, sec, name, candidates); if ((!found || findall) && !global_apropos) { db_ok = try_db (manpath, sec, name, candidates); #ifdef MAN_DB_CREATES if (db_ok == TRY_DATABASE_CREATED) /* we created a db in the last call */ db_ok = try_db (manpath, sec, name, candidates); #endif /* MAN_DB_CREATES */ #ifdef MAN_DB_UPDATES if (db_ok == TRY_DATABASE_UPDATED) /* We found some outdated entries and rebuilt the * database in the last call. If this keeps * happening, though, give up and punt to the * filesystem. */ db_ok = try_db (manpath, sec, name, candidates); #endif /* MAN_DB_UPDATES */ if (db_ok > 0) /* we found/opened a db and found something */ found += db_ok; } if (!global_manpath) regain_effective_privs (); return found; } static int display_pages (struct candidate *candidates) { struct candidate *candp; int found = 0; for (candp = candidates; candp; candp = candp->next) { global_manpath = is_global_mandir (candp->path); if (!global_manpath) drop_effective_privs (); switch (candp->from_db) { case CANDIDATE_FILESYSTEM: found += display_filesystem (candp); break; case CANDIDATE_DATABASE: found += display_database_check (candp); break; default: error (0, 0, _("internal error: candidate type %d " "out of range"), candp->from_db); } if (!global_manpath) regain_effective_privs (); if (found && !findall) return found; } return found; } /* * Search for text in all manual pages. * * This is not a real full-text search, but a brute-force on-demand search. * The idea, name, and approach originate in the 'man' package, added (I * believe) by Andries Brouwer, although the implementation is new for * man-db and much faster due to running in-process. * * Conceptually, this really belongs in whatis.c, as part of apropos. * However, the implementation in 'man' offers pages for immediate display * on request rather than simply listing them, which is currently awkward to * do in apropos. If we ever add support to apropos/whatis for either * calling back to man or displaying pages directly, we should revisit this. */ static int grep (const char *file, const char *string, const regex_t *search) { struct stat st; decompress *decomp; const char *line; int ret = 0; /* pipeline_start makes file open failures unconditionally fatal. * Here, we'd rather just ignore any such files. */ if (stat (file, &st) < 0) return 0; decomp = decompress_open (file, DECOMPRESS_ALLOW_INPROCESS); if (!decomp) return 0; decompress_start (decomp); while ((line = decompress_readline (decomp)) != NULL) { if (regex_opt) { if (regexec (search, line, 0, (regmatch_t *) 0, 0) == 0) { ret = 1; break; } } else { if (match_case ? strstr (line, string) : strcasestr (line, string)) { ret = 1; break; } } } decompress_free (decomp); return ret; } static int do_global_apropos_section (const char *path, const char *sec, const char *name, gl_set_t seen) { int found = 0; gl_list_t names; const char *found_name; regex_t search; global_manpath = is_global_mandir (path); if (!global_manpath) drop_effective_privs (); debug ("searching in %s, section %s\n", path, sec); names = look_for_file (path, sec, "*", false, LFF_WILDCARD); if (regex_opt) xregcomp (&search, name, REG_EXTENDED | REG_NOSUB | (match_case ? 0 : REG_ICASE)); else memset (&search, 0, sizeof search); order_files (path, &names); GL_LIST_FOREACH (names, found_name) { struct mandata *info; char *title = NULL; const struct ult_value *man_ult; char *cat_file = NULL; if (!grep (found_name, name, &search)) continue; info = filename_info (found_name, quiet < 2); if (!info) goto next; title = xasprintf ("%s(%s)", info->name, info->ext); man_ult = ult_src (found_name, path, NULL, ult_flags); if (!man_ult) goto next; if (gl_set_search (seen, man_ult->path)) goto next; gl_set_add (seen, xstrdup (man_ult->path)); lang = lang_dir (man_ult->path); cat_file = find_cat_file (path, found_name, man_ult->path); if (display (path, man_ult->path, cat_file, title, NULL)) found = 1; free (lang); lang = NULL; next: free (cat_file); free (title); free_mandata_struct (info); } gl_list_free (names); if (regex_opt) regfree (&search); if (!global_manpath) regain_effective_privs (); return found; } static int do_global_apropos (const char *name, int *found) { gl_list_t my_section_list; const char *sec; gl_set_t seen; if (section) { my_section_list = gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL, NULL, false); gl_list_add_last (my_section_list, section); } else my_section_list = section_list; seen = new_string_set (GL_HASH_SET); GL_LIST_FOREACH (my_section_list, sec) { char *mp; GL_LIST_FOREACH (manpathlist, mp) *found += do_global_apropos_section (mp, sec, name, seen); } gl_set_free (seen); if (section) gl_list_free (my_section_list); return *found ? OK : NOT_FOUND; } static int man (const char *name, int *found); /* man issued with `-l' option */ static int local_man_loop (const char *argv) { int exit_status = OK; bool local_mf = local_man_file; drop_effective_privs (); local_man_file = true; if (strcmp (argv, "-") == 0) display (NULL, "", NULL, "(stdin)", NULL); else { struct stat st; /* Check that the file exists and isn't e.g. a directory */ if (stat (argv, &st)) { error (0, errno, "%s", argv); return NOT_FOUND; } if (S_ISDIR (st.st_mode)) { error (0, EISDIR, "%s", argv); return NOT_FOUND; } if (S_ISCHR (st.st_mode) || S_ISBLK (st.st_mode)) { /* EINVAL is about the best I can do. */ error (0, EINVAL, "%s", argv); return NOT_FOUND; } if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) { /* Perhaps an executable. If its directory is on * $PATH, then we want to look up the corresponding * manual page in the appropriate hierarchy rather * than displaying the executable. */ char *argv_dir = dir_name (argv); int found = 0; if (directory_on_path (argv_dir)) { char *argv_base = base_name (argv); char *new_manp, *nm; gl_list_t old_manpathlist; debug ("recalculating manpath for executable " "in %s\n", argv_dir); new_manp = get_manpath_from_path (argv_dir, false); if (!new_manp || !*new_manp) { debug ("no useful manpath for " "executable\n"); goto executable_out; } nm = locale_manpath (new_manp); free (new_manp); new_manp = nm; old_manpathlist = manpathlist; manpathlist = create_pathlist (new_manp); man (argv_base, &found); free_pathlist (manpathlist); manpathlist = old_manpathlist; executable_out: free (new_manp); free (argv_base); } free (argv_dir); if (found) return OK; } if (exit_status == OK) { char *argv_base = base_name (argv); char *argv_abs; if (argv[0] == '/') argv_abs = xstrdup (argv); else { argv_abs = xgetcwd (); if (argv_abs) argv_abs = appendstr (argv_abs, "/", argv, (void *) 0); else argv_abs = xstrdup (argv); } lang = lang_dir (argv_abs); free (argv_abs); if (!display (NULL, argv, NULL, argv_base, NULL)) { if (local_mf) error (0, errno, "%s", argv); exit_status = NOT_FOUND; } free (lang); lang = NULL; free (argv_base); } } local_man_file = local_mf; regain_effective_privs (); return exit_status; } /* * Splits a "name[.section]" or "name(section)" into { "name", "section" }. * Section would be NULL if not present. * The caller is responsible for freeing *ret_name and *ret_section. * */ static void split_page_name (const char *page_name, char **ret_name, char **ret_section) { char *dot, *lparen, *rparen; dot = strrchr (page_name, '.'); if (dot && is_section (dot + 1)) { *ret_name = xstrndup (page_name, dot - page_name); *ret_section = xstrdup (dot + 1); return; } lparen = strrchr (page_name, '('); rparen = strrchr (page_name, ')'); if (lparen && rparen && rparen > lparen) { char *paren_section = xstrndup (lparen + 1, rparen - lparen - 1); if (is_section (paren_section)) { *ret_name = xstrndup (page_name, lparen - page_name); *ret_section = paren_section; /* steal memory */ return; } free (paren_section); } *ret_name = xstrdup (page_name); *ret_section = NULL; } static void locate_page_in_manpath (const char *page_section, const char *page_name, struct candidate **candidates, int *found) { char *mp; GL_LIST_FOREACH (manpathlist, mp) *found += locate_page (mp, page_section, page_name, candidates); } /* * Search for manual pages. * * If preformatted manual pages are supported, look for the formatted * file first, then the man page source file. If they both exist and * the man page source file is newer, or only the source file exists, * try to reformat it and write the results in the cat directory. If * it is not possible to write the cat file, simply format and display * the man file. * * If preformatted pages are not supported, or the troff option is * being used, only look for the man page source file. * */ static int man (const char *name, int *found) { char *page_name, *page_section; struct candidate *candidates = NULL, *cand, *candnext; *found = 0; fflush (stdout); if (section) locate_page_in_manpath (section, name, &candidates, found); else { const char *sec; GL_LIST_FOREACH (section_list, sec) locate_page_in_manpath (sec, name, &candidates, found); } split_page_name (name, &page_name, &page_section); if (!*found && page_section) locate_page_in_manpath (page_section, page_name, &candidates, found); free (page_name); free (page_section); sort_candidates (&candidates); if (*found) *found = display_pages (candidates); for (cand = candidates; cand; cand = candnext) { candnext = cand->next; free_candidate (cand); } return *found ? OK : NOT_FOUND; } static int man_maybe_local (const char *name, int *found) { *found = 0; if (strchr (name, '/')) { int status = local_man_loop (name); if (status == OK) *found = 1; return status; } return man (name, found); } static gl_list_t get_section_list (void) { gl_list_t config_sections, sections; char *section_list_copy; const char *sec; /* Section list from configuration file, or STD_SECTIONS if it's * empty. */ config_sections = get_sections (); if (!gl_list_size (config_sections)) { int i; for (i = 0; std_sections[i]; ++i) gl_list_add_last (config_sections, xstrdup (std_sections[i])); } if (colon_sep_section_list == NULL) colon_sep_section_list = getenv ("MANSECT"); if (colon_sep_section_list == NULL || *colon_sep_section_list == '\0') return config_sections; /* Although this is documented as colon-separated, at least Solaris * man's -s option takes a comma-separated list, so we accept that * too for compatibility. */ sections = new_string_list (GL_ARRAY_LIST, true); section_list_copy = xstrdup (colon_sep_section_list); for (sec = strtok (section_list_copy, ":,"); sec; sec = strtok (NULL, ":,")) gl_list_add_last (sections, xstrdup (sec)); free (section_list_copy); if (gl_list_size (sections)) { gl_list_free (config_sections); return sections; } else { gl_list_free (sections); return config_sections; } } /* * Returns the first token of a libpipeline/sh-style command. See SUSv4TC2: * 2.2 Shell Command Language: Quoting. * * Free the returned value. * * Examples: * sh_lang_first_word ("echo 3") returns "echo" * sh_lang_first_word ("'e ho' 3") returns "e ho" * sh_lang_first_word ("e\\cho 3") returns "echo" * sh_lang_first_word ("e\\\ncho 3") returns "echo" * sh_lang_first_word ("\"echo t\" 3") returns "echo t" * sh_lang_first_word ("\"ech\\o t\" 3") returns "ech\\o t" * sh_lang_first_word ("\"ech\\\\o t\" 3") returns "ech\\o t" * sh_lang_first_word ("\"ech\\\no t\" 3") returns "echo t" * sh_lang_first_word ("\"ech\\$ t\" 3") returns "ech$ t" * sh_lang_first_word ("\"ech\\` t\" 3") returns "ech` t" * sh_lang_first_word ("e\"ch\"o 3") returns "echo" * sh_lang_first_word ("e'ch'o 3") returns "echo" */ static char *sh_lang_first_word (const char *cmd) { int i, o = 0; char *ret = xmalloc (strlen (cmd) + 1); for (i = 0; cmd[i] != '\0'; i++) { if (cmd[i] == '\\') { /* Escape Character (Backslash) */ i++; if (cmd[i] == '\0') break; if (cmd[i] != '\n') ret[o++] = cmd[i]; } else if (cmd[i] == '\'') { /* Single-Quotes */ i++; while (cmd[i] != '\0' && cmd[i] != '\'') ret[o++] = cmd[i++]; } else if (cmd[i] == '"') { /* Double-Quotes */ i++; while (cmd[i] != '\0' && cmd[i] != '"') { if (cmd[i] == '\\') { if (cmd[i + 1] == '$' || cmd[i + 1] == '`' || cmd[i + 1] == '"' || cmd[i + 1] == '\\') ret[o++] = cmd[++i]; else if (cmd[i + 1] == '\n') i++; else ret[o++] = cmd[i]; } else ret[o++] = cmd[i]; i++; } } else if (cmd[i] == '\t' || cmd[i] == ' ' || cmd[i] == '\n' || cmd[i] == '#') break; else ret[o++] = cmd[i]; } ret[o] = '\0'; return ret; } int main (int argc, char *argv[]) { int argc_env, exit_status = OK; char **argv_env; const char *tmp; set_program_name (argv[0]); init_debug (); pipeline_install_post_fork (pop_all_cleanups); sandbox = sandbox_init (); umask (022); init_locale (); internal_locale = setlocale (LC_MESSAGES, NULL); /* Use LANGUAGE only when LC_MESSAGES locale category is * neither "C" nor "POSIX". */ if (internal_locale && strcmp (internal_locale, "C") && strcmp (internal_locale, "POSIX")) multiple_locale = getenv ("LANGUAGE"); internal_locale = xstrdup (internal_locale ? internal_locale : "C"); xstdopen (); /* export argv, it might be needed when invoking the vendor supplied browser */ #if defined _AIX || defined __sgi global_argv = argv; #endif #ifdef TROFF_IS_GROFF /* used in --help, so initialise early */ if (!html_pager) init_html_pager (); #endif /* TROFF_IS_GROFF */ #ifdef NROFF_WARNINGS roff_warnings = new_string_list (GL_ARRAY_LIST, true); #endif /* NROFF_WARNINGS */ /* First of all, find out if $MANOPT is set. If so, put it in *argv[] format for argp to play with. */ argv_env = manopt_to_env (&argc_env); if (argv_env) if (argp_parse (&argp, argc_env, argv_env, ARGP_NO_ARGS, 0, 0)) exit (FAIL); /* parse the actual program args */ if (argp_parse (&argp, argc, argv, ARGP_NO_ARGS, &first_arg, 0)) exit (FAIL); /* record who we are and drop effective privs for later use */ init_security (); read_config_file (local_man_file || user_config_file); /* if the user wants whatis or apropos, give it to them... */ if (external) do_extern (argc, argv); get_term (); /* stores terminal settings */ /* close this locale and reinitialise if a new locale was issued as an argument or in $MANOPT */ if (locale) { free (internal_locale); internal_locale = setlocale (LC_ALL, locale); if (internal_locale) internal_locale = xstrdup (internal_locale); else internal_locale = xstrdup (locale); debug ("main(): locale = %s, internal_locale = %s\n", locale, internal_locale); if (internal_locale) { setenv ("LANGUAGE", internal_locale, 1); locale_changed (); multiple_locale = NULL; } } #ifdef TROFF_IS_GROFF if (htmlout) pager = html_pager; #endif /* TROFF_IS_GROFF */ if (pager == NULL) pager = getenv ("MANPAGER"); if (pager == NULL) pager = getenv ("PAGER"); if (pager == NULL) pager = get_def_user ("pager", NULL); if (pager == NULL) { char *pager_program = sh_lang_first_word (PROG_PAGER); if (pathsearch_executable (pager_program)) pager = PROG_PAGER; else pager = ""; free (pager_program); } if (*pager == '\0') pager = get_def_user ("cat", PROG_CAT); if (prompt_string == NULL) prompt_string = getenv ("MANLESS"); if (prompt_string == NULL) #ifdef LESS_PROMPT prompt_string = LESS_PROMPT; #else prompt_string = _( " Manual page " MAN_PN " ?ltline %lt?L/%L.:byte %bB?s/%s..?e (END):" "?pB %pB\\%.. " "(press h for help or q to quit)"); #endif /* Restore and save $LESS in $MAN_ORIG_LESS so that recursive uses * of man work as expected. */ less = getenv ("MAN_ORIG_LESS"); if (less == NULL) less = getenv ("LESS"); setenv ("MAN_ORIG_LESS", less ? less : "", 1); debug ("using %s as pager\n", pager); if (first_arg == argc) { if (print_where) { manp = get_manpath (""); printf ("%s\n", manp); exit (OK); } else { free (internal_locale); gripe_no_name (NULL); } } section_list = get_section_list (); if (manp == NULL) { char *mp = get_manpath (alt_system_name); manp = locale_manpath (mp); free (mp); } else free (get_manpath (NULL)); manpathlist = create_pathlist (manp); /* man issued with `-l' option */ if (local_man_file) { while (first_arg < argc) { exit_status = local_man_loop (argv[first_arg]); ++first_arg; } free (internal_locale); exit (exit_status); } /* finished manpath processing, regain privs */ regain_effective_privs (); #ifdef MAN_DB_UPDATES /* If `-u', do it now. */ if (update) { int status = run_mandb (false, NULL, NULL); if (status) error (0, 0, _("mandb command failed with exit status %d"), status); } #endif /* MAN_DB_UPDATES */ while (first_arg < argc) { int status = OK; int found = 0; static bool maybe_section = false; const char *nextarg = argv[first_arg++]; /* * See if this argument is a valid section name. If not, * is_section returns NULL. */ if (!catman) { tmp = is_section (nextarg); if (tmp) { section = tmp; debug ("\nsection: %s\n", section); maybe_section = true; } } if (maybe_section) { if (first_arg < argc) /* e.g. 'man 3perl Shell' */ nextarg = argv[first_arg++]; else /* e.g. 'man 9wm' */ section = NULL; /* ... but leave maybe_section set so we can * tell later that this happened. */ } /* this is where we actually start looking for the man page */ skip = false; if (global_apropos) status = do_global_apropos (nextarg, &found); else { bool found_subpage = false; if (subpages && first_arg < argc) { char *subname = xasprintf ( "%s-%s", nextarg, argv[first_arg]); assert (subname); status = man (subname, &found); free (subname); if (status == OK) { found_subpage = true; ++first_arg; } } if (!found_subpage && subpages && first_arg < argc) { char *subname = xasprintf ( "%s_%s", nextarg, argv[first_arg]); assert (subname); status = man (subname, &found); free (subname); if (status == OK) { found_subpage = true; ++first_arg; } } if (!found_subpage) status = man_maybe_local (nextarg, &found); } /* clean out the cache of database lookups for each man page */ if (db_map) { gl_map_free (db_map); db_map = NULL; } if (section && maybe_section) { if (status != OK && !catman) { /* Maybe the section wasn't a section after * all? e.g. 'man 9wm fvwm'. */ bool found_subpage = false; debug ("\nRetrying section %s as name\n", section); tmp = section; section = NULL; if (subpages) { char *subname = xasprintf ( "%s-%s", tmp, nextarg); status = man (subname, &found); free (subname); if (status == OK) { found_subpage = true; ++first_arg; } } if (!found_subpage) status = man_maybe_local (tmp, &found); if (db_map) { gl_map_free (db_map); db_map = NULL; } /* ... but don't gripe about it if it doesn't * work! */ if (status == OK) { /* It was a name after all, so arrange * to try the next page again with a * null section. */ nextarg = tmp; --first_arg; } else /* No go, it really was a section. */ section = tmp; } } if (status != OK && !catman) { if (!skip) { exit_status = status; if (exit_status == NOT_FOUND) { if (!section && maybe_section && CTYPE (isdigit, nextarg[0])) gripe_no_name (nextarg); else gripe_no_man (nextarg, section); } } } else { debug ("\nFound %d man pages\n", found); if (catman) { printf ("%s", nextarg); if (section) printf ("(%s)", section); if (first_arg != argc) fputs (", ", stdout); else fputs (".\n", stdout); } } maybe_section = false; } if (db_map) { gl_map_free (db_map); db_map = NULL; } drop_effective_privs (); gl_list_free (section_list); free_pathlist (manpathlist); free (internal_locale); sandbox_free (sandbox); exit (exit_status); }