diff options
Diffstat (limited to '')
-rw-r--r-- | src/man.c | 4397 |
1 files changed, 4397 insertions, 0 deletions
diff --git a/src/man.c b/src/man.c new file mode 100644 index 0000000..a144596 --- /dev/null +++ b/src/man.c @@ -0,0 +1,4397 @@ +/* + * 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 <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <assert.h> +#include <errno.h> +#include <termios.h> +#include <unistd.h> + +#ifndef R_OK +# define R_OK 4 +# define X_OK 1 +#endif /* !R_OK */ + +#include <limits.h> + +#if HAVE_FCNTL_H +# include <fcntl.h> +#endif + +#include <ctype.h> +#include <signal.h> +#include <time.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include "argp.h" +#include "dirname.h" +#include "minmax.h" +#include "progname.h" +#include "regex.h" +#include "stat-time.h" +#include "utimens.h" +#include "xvasprintf.h" +#include "xgetcwd.h" + +#include "gettext.h" +#include <locale.h> +#define _(String) gettext (String) +#define N_(String) gettext_noop (String) + +#include "manconfig.h" + +#include "error.h" +#include "cleanup.h" +#include "hashtable.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 "mydbm.h" +#include "db_storage.h" + +#include "filenames.h" +#include "globbing.h" +#include "ult_src.h" +#include "manp.h" +#include "zsoelim.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 int global_manpath = -1; /* global or user manual page hierarchy? */ +static int 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 +}; + +struct string_llist; +struct string_llist { + const char *name; + struct string_llist *next; +}; + + +static char *manpathlist[MAXDIRS]; + +/* globals */ +int quiet = 1; +char *database = NULL; +extern const char *extension; /* for globbing.c */ +extern char *user_config_file; /* defined in manp.c */ +extern int disable_cache; +extern int min_cat_width, max_cat_width, cat_width; +man_sandbox *sandbox; + +/* locals */ +static const char *alt_system_name; +static const char **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 struct hashtable *db_hash = NULL; + +static int troff; +static const char *roff_device = NULL; +static const char *want_encoding = NULL; +#ifdef NROFF_WARNINGS +static const char default_roff_warnings[] = "mac"; +static struct string_llist *roff_warnings = NULL; +#endif /* NROFF_WARNINGS */ +static int global_apropos; +static int print_where, print_where_cat; +static int catman; +static int local_man_file; +static int findall; +static int update; +static int match_case; +static int regex_opt; +static int wildcard; +static int names_only; +static int ult_flags = SO_LINK | SOFT_LINK | HARD_LINK; +static const char *recode = NULL; +static int no_hyphenation; +static int no_justification; +static int subpages = 1; + +static int ascii; /* insert tr in the output pipe */ +static int save_cat; /* security breach? Can we save the cat? */ + +static int first_arg; + +static int found_a_stray; /* found a straycat */ + +#ifdef MAN_CATS +static char *tmp_cat_file; /* for open_cat_stream(), close_cat_stream() */ +static int 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 int ditroff; +static const char *gxditview; +static int 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 TROFF_IS_GROFF +# define ONLY_TROFF_IS_GROFF 0 +# else +# define ONLY_TROFF_IS_GROFF OPTION_HIDDEN +# endif + +/* Please keep these options in the same order as in parse_opt below. */ +static struct argp_option options[] = { + { "config-file", 'C', N_("FILE"), 0, N_("use this user configuration file") }, + { "debug", 'd', 0, 0, N_("emit debugging messages") }, + { "default", 'D', 0, 0, N_("reset all options to their default values") }, + { "warnings", OPT_WARNINGS, N_("WARNINGS"), ONLY_NROFF_WARNINGS | OPTION_ARG_OPTIONAL, + N_("enable warnings from groff") }, + + { 0, 0, 0, 0, N_("Main modes of operation:"), 10 }, + { "whatis", 'f', 0, 0, N_("equivalent to whatis") }, + { "apropos", 'k', 0, 0, N_("equivalent to apropos") }, + { "global-apropos", 'K', 0, 0, N_("search for text in all pages") }, + { "where", 'w', 0, 0, N_("print physical location of man page(s)") }, + { "path", 0, 0, OPTION_ALIAS }, + { "location", 0, 0, OPTION_ALIAS }, + { "where-cat", 'W', 0, 0, N_("print physical location of cat file(s)") }, + { "location-cat", 0, 0, OPTION_ALIAS }, + { "local-file", 'l', 0, 0, N_("interpret PAGE argument(s) as local filename(s)") }, + { "catman", 'c', 0, 0, N_("used by catman to reformat out of date cat pages"), 11 }, + { "recode", 'R', N_("ENCODING"), 0, N_("output source page encoded in ENCODING") }, + + { 0, 0, 0, 0, N_("Finding manual pages:"), 20 }, + { "locale", 'L', N_("LOCALE"), 0, N_("define the locale for this particular man search") }, + { "systems", 'm', N_("SYSTEM"), 0, N_("use manual pages from other systems") }, + { "manpath", 'M', N_("PATH"), 0, N_("set search path for manual pages to PATH") }, + { "sections", 'S', N_("LIST"), 0, N_("use colon separated section list"), 21 }, + { 0, 's', 0, OPTION_ALIAS }, + { "extension", 'e', N_("EXTENSION"), + 0, N_("limit search to extension type EXTENSION"), 22 }, + { "ignore-case", 'i', 0, 0, N_("look for pages case-insensitively (default)"), 23 }, + { "match-case", 'I', 0, 0, N_("look for pages case-sensitively") }, + { "regex", OPT_REGEX, 0, 0, N_("show all pages matching regex"), 24 }, + { "wildcard", OPT_WILDCARD, 0, 0, N_("show all pages matching wildcard") }, + { "names-only", OPT_NAMES, 0, 0, N_("make --regex and --wildcard match page names only, not " + "descriptions"), 25 }, + { "all", 'a', 0, 0, N_("find all matching manual pages"), 26 }, + { "update", 'u', 0, 0, N_("force a cache consistency check") }, + { "no-subpages", + OPT_NO_SUBPAGES, 0, 0, N_("don't try subpages, e.g. 'man foo bar' => 'man foo-bar'"), 27 }, + + { 0, 0, 0, 0, N_("Controlling formatted output:"), 30 }, + { "pager", 'P', N_("PAGER"), 0, N_("use program PAGER to display output") }, + { "prompt", 'r', N_("STRING"), 0, N_("provide the `less' pager with a prompt") }, + { "ascii", '7', 0, 0, N_("display ASCII translation of certain latin1 chars"), 31 }, + { "encoding", 'E', N_("ENCODING"), 0, N_("use selected output encoding") }, + { "no-hyphenation", + OPT_NO_HYPHENATION, 0, 0, N_("turn off hyphenation") }, + { "nh", 0, 0, OPTION_ALIAS }, + { "no-justification", + OPT_NO_JUSTIFICATION, 0, 0, N_("turn off justification") }, + { "nj", 0, 0, OPTION_ALIAS }, + { "preprocessor", 'p', N_("STRING"), 0, 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 + { "troff", 't', 0, 0, N_("use %s to format pages"), 32 }, + { "troff-device", 'T', N_("DEVICE"), OPTION_ARG_OPTIONAL, + N_("use %s with selected device") }, + { "html", 'H', N_("BROWSER"), ONLY_TROFF_IS_GROFF | OPTION_ARG_OPTIONAL, + N_("use %s or BROWSER to display HTML output"), 33 }, + { "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") }, + { "ditroff", 'Z', 0, ONLY_TROFF_IS_GROFF, N_("use groff and force it to produce ditroff") }, +#endif /* HAS_TROFF */ + + { 0, 'h', 0, OPTION_HIDDEN, 0 }, /* compatibility for --help */ + { 0 } +}; + +static void init_html_pager (void) +{ + html_pager = getenv ("BROWSER"); + if (!html_pager) + html_pager = WEB_BROWSER; +} + +static error_t parse_opt (int key, char *arg, struct argp_state *state) +{ + static int 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 = 1; + 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 = 0; +#ifdef TROFF_IS_GROFF + ditroff = 0; + gxditview = NULL; + htmlout = 0; + 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, ",")) { + struct string_llist *new; + new = xmalloc (sizeof *new); + new->name = xstrdup (warning); + new->next = roff_warnings; + roff_warnings = new; + } + + free (s); + } +#endif /* NROFF_WARNINGS */ + return 0; + + case 'f': + external = WHATIS; + whatis = 1; + return 0; + case 'k': + external = APROPOS; + apropos = 1; + return 0; + case 'K': + global_apropos = 1; + return 0; + case 'w': + print_where = 1; + return 0; + case 'W': + print_where_cat = 1; + return 0; + case 'l': + local_man_file = 1; + return 0; + case 'c': + catman = 1; + 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 = 0; + return 0; + case 'I': + match_case = 1; + return 0; + case OPT_REGEX: + regex_opt = 1; + findall = 1; + return 0; + case OPT_WILDCARD: + wildcard = 1; + findall = 1; + return 0; + case OPT_NAMES: + names_only = 1; + return 0; + case 'a': + findall = 1; + return 0; + case 'u': + update = 1; + return 0; + case OPT_NO_SUBPAGES: + subpages = 0; + return 0; + + case 'P': + pager = arg; + return 0; + case 'r': + prompt_string = arg; + return 0; + case '7': + ascii = 1; + 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 = 1; + return 0; + case OPT_NO_JUSTIFICATION: + no_justification = 1; + return 0; + case 'p': + preprocessors = arg; + return 0; +#ifdef HAS_TROFF + case 't': + troff = 1; + 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 = 1; + return 0; + case 'H': +# ifdef TROFF_IS_GROFF + if (arg) + html_pager = arg; + htmlout = 1; + troff = 1; + roff_device = "html"; +# endif /* TROFF_IS_GROFF */ + return 0; + case 'X': +# ifdef TROFF_IS_GROFF + troff = 1; + gxditview = (arg ? arg : "75"); +# endif /* TROFF_IS_GROFF */ + return 0; + case 'Z': +# ifdef TROFF_IS_GROFF + ditroff = 1; + troff = 1; +# 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 (troff + whatis + apropos + catman + + (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 (regex_opt + 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; +} + +static char *help_filter (int key, const char *text, + void *input ATTRIBUTE_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; + } +} + +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); + + exit (FAIL); +} + +/* In case we're set-id, double-check that our standard file descriptors are + * open in a standard way. See: + * + * http://austingroupbugs.net/view.php?id=173 + */ +static void check_standard_fds (void) +{ + int flags, mode; + + /* We can't even write an error message in this case, so check it + * first. + */ + flags = fcntl (2, F_GETFL); + if (flags < 0) + exit (FATAL); + mode = flags & O_ACCMODE; + if (mode != O_WRONLY && mode != O_RDWR) + exit (FATAL); + + flags = fcntl (0, F_GETFL); + if (flags < 0) { + fprintf (stderr, "stdin not open!\n"); + exit (FATAL); + } + mode = flags & O_ACCMODE; + if (mode != O_RDONLY && mode != O_RDWR) { + fprintf (stderr, "stdin not open for reading!\n"); + exit (FATAL); + } + + flags = fcntl (1, F_GETFL); + if (flags < 0) { + fprintf (stderr, "stdout not open!\n"); + exit (FATAL); + } + mode = flags & O_ACCMODE; + if (mode != O_WRONLY && mode != O_RDWR) { + fprintf (stderr, "stdout not open for writing!\n"); + exit (FATAL); + } +} + +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); + } + } +} + +#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 (); + + /* groff >= 1.18 defaults to 78. */ + if ((!troff || ditroff) && line_length != 80) { + 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, int *save_cat_p) +{ + int length; + pipecmd *ret = NULL; + + 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 = 0; + } + } + + 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", + 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 (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) { + if (*string == '?' || + *string == ':' || + *string == '.' || + *string == '%' || + *string == '\\') + *ptr++ = '\\'; + + *ptr++ = *string++; + } + + *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 (int 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 *is_section (const char *name) +{ + const char **vs; + + for (vs = section_list; *vs; 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 (pipeline *decomp, int prefixes) +{ +#ifdef PP_COOKIE + 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 = pipeline_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); + } +#endif + return NULL; +} + + +/* Determine pre-processors, set save_cat and return string */ +static char *get_preprocessors (pipeline *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 = 1; + } else if (preprocessors) { + pp_string = xstrdup (preprocessors); + pp_source = "command line"; + save_cat = 0; + } else if ((pp_string = get_preprocessors_from_file (decomp, + prefixes))) { + pp_source = "file"; + save_cat = 1; + } else if ((env = getenv ("MANROFFSEQ"))) { + pp_string = xstrdup (env); + pp_source = "environment"; + save_cat = 0; + } else if (!dbfilters) { + pp_string = xstrdup (DEFAULT_MANROFFSEQ); + pp_source = "default"; + save_cat = 1; + } else { + pp_string = xstrdup (""); + pp_source = "no filters"; + save_cat = 1; + } + + 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 (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); +} + +/* Return pipeline to format file to stdout. */ +static pipeline *make_roff_command (const char *dir, const char *file, + pipeline *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); + 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) { +#ifndef GNU_NROFF + int using_tbl = 0; +#endif /* GNU_NROFF */ + + do { +#ifdef NROFF_WARNINGS + struct string_llist *cur; +#endif /* NROFF_WARNINGS */ + int wants_dev = 0; /* filter wants a dev argument */ + int wants_post = 0; /* postprocessor arguments */ + + cmd = NULL; + /* set cmd according to *pp_string, on + errors leave cmd as NULL */ + switch (*pp_string) { + case 'e': + if (troff) + cmd = pipecmd_new_argstr + (get_def ("eqn", EQN)); + else + cmd = pipecmd_new_argstr + (get_def ("neqn", NEQN)); + wants_dev = 1; + break; + case 'g': + cmd = pipecmd_new_argstr + (get_def ("grap", GRAP)); + break; + case 'p': + cmd = pipecmd_new_argstr + (get_def ("pic", PIC)); + break; + case 't': + cmd = pipecmd_new_argstr + (get_def ("tbl", TBL)); +#ifndef GNU_NROFF + using_tbl = 1; +#endif /* GNU_NROFF */ + break; + case 'v': + cmd = pipecmd_new_argstr + (get_def ("vgrind", VGRIND)); + break; + case 'r': + cmd = pipecmd_new_argstr + (get_def ("refer", REFER)); + break; + case ' ': + case '-': + case 0: + /* done with preprocessors, now add roff */ + if (troff) { + cmd = pipecmd_new_argstr + (get_def ("troff", TROFF)); + save_cat = 0; + } else + cmd = pipecmd_new_argstr + (get_def ("nroff", 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 NROFF_WARNINGS + for (cur = roff_warnings; cur; + cur = cur->next) + pipecmd_argf (cmd, "-w%s", cur->name); +#endif /* NROFF_WARNINGS */ + +#ifdef HEIRLOOM_NROFF + if (running_setuid ()) + pipecmd_unsetenv (cmd, "TROFFMACS"); +#endif /* HEIRLOOM_NROFF */ + + pipecmd_argstr (cmd, roff_opt); + + wants_dev = 1; + wants_post = 1; + break; + } + + if (!cmd) { + assert (*pp_string); /* didn't fail on roff */ + error (0, 0, + _("ignoring unknown preprocessor `%c'"), + *pp_string); + continue; + } + + 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); +#endif /* TROFF_IS_GROFF */ + } + + if (wants_post) { +#ifdef TROFF_IS_GROFF + if (gxditview) + pipecmd_arg (cmd, "-X"); +#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); + + if (*pp_string == ' ' || *pp_string == '-') + break; + } while (*pp_string++); + + if (!troff && *COL) { + 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 && *COL) + 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); + int found_percent_s = 0; + 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 = 1; + 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 ATTRIBUTE_UNUSED) +{ + char *line = NULL; + size_t len = 0; + + while (getline (&line, &len, stdin) != -1) { + int in_blank_line = 1; + int got_blank_line = 0; + + while (in_blank_line) { + char *p; + for (p = line; *p; ++p) { + if (!CTYPE (isspace, *p)) { + in_blank_line = 0; + break; + } + } + + if (in_blank_line) { + got_blank_line = 1; + 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 && *COL) { + /* 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); + } + + /* 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", 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 = 0; + + debug ("creating temporary cat for %s\n", cat_file); + + tmp_cat_file = tmp_cat_filename (cat_file); + if (tmp_cat_file) + created_tmp_cat = 1; + 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 + error (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", 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 (pipeline *decomp, + pipeline *format_cmd, + pipeline *disp_cmd, + const char *cat_file, const char *encoding) +{ + 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 */ + +/* 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 (pipeline *decomp, + pipeline *format_cmd, pipeline *disp_cmd, + const char *man_file) +{ + 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) + error (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) + error (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, 0) == -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"); + } + free (browser_list); + if (remove_directory (htmldir, 0) == -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, pipeline *decomp, + pipeline *format_cmd, const char *encoding) +{ + char *tmpcat = tmp_cat_filename (cat_file); +#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", 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); +} + +static void disable_hyphenation (void *data ATTRIBUTE_UNUSED) +{ + fputs (".nh\n" + ".de hy\n" + "..\n" + ".lf 1\n", stdout); +} + +static void disable_justification (void *data ATTRIBUTE_UNUSED) +{ + fputs (".na\n" + ".de ad\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 = 0; + 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 = 1; + 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; + int display_to_stdout; + pipeline *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); + else + decomp = decompress_fdopen (dup (STDIN_FILENO)); + + if (!recode && no_hyphenation) { + pipecmd *hcmd = pipecmd_new_function ( + "echo .nh && echo .de hy && echo ..", + disable_hyphenation, NULL, NULL); + pipecmd_sequence_command (seq, hcmd); + ++prefixes; + } + + if (!recode && no_justification) { + pipecmd *jcmd = pipecmd_new_function ( + "echo .na && echo .de ad && echo ..", + 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", + 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) { + assert (pipeline_get_ncommands (decomp) <= 1); + if (pipeline_get_ncommands (decomp)) { + pipecmd_sequence_command + (seq, + pipeline_get_command (decomp, 0)); + pipeline_set_command (decomp, 0, seq); + } else { + pipecmd_sequence_command + (seq, pipecmd_new_passthrough ()); + pipeline_command (decomp, seq); + } + } else + pipecmd_free (seq); + } + + if (decomp) { + char *pp_string; + + pipeline_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) + display_to_stdout = 0; +#endif + if (recode) + display_to_stdout = 1; + + 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) { + int status; + if (prompt && do_prompt (title)) { + pipeline_free (format_cmd); + pipeline_free (decomp); + free (formatted_encoding); + return 0; + } + 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 != 0) + gripe_system (format_cmd, status); + } + } else { + int format = 1; + 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 +#endif + || local_man_file + || recode + || disable_cache) + save_cat = 0; + + if (!man_file) { + /* Stray cat. */ + assert (cat_file); + format = 0; + } else if (!cat_file) { + assert (man_file); + save_cat = 0; + format = 1; + } 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 == 1 && *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", + format, save_cat, found); + + if (!found) { + pipeline_free (format_cmd); + pipeline_free (decomp); + return found; + } + + if (print_where || print_where_cat) { + int printed = 0; + if (print_where && man_file) { + printf ("%s", man_file); + printed = 1; + } + if (print_where_cat && cat_file && !format) { + if (printed) + putchar (' '); + printf ("%s", cat_file); + printed = 1; + } + 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); + pipeline_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; + pipeline *decomp_cat; + + if (prompt && do_prompt (title)) { + pipeline_free (format_cmd); + pipeline_free (decomp); + return 0; + } + + decomp_cat = decompress_open (cat_file); + if (!decomp_cat) { + error (0, errno, _("can't open %s"), cat_file); + pipeline_free (format_cmd); + pipeline_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); + pipeline_free (decomp_cat); + } + } + + free (formatted_encoding); + + pipeline_free (format_cmd); + pipeline_free (decomp); + + if (!prompt) + prompt = found; + + return found; +} + +static void gripe_converting_name (const char *name) ATTRIBUTE_NORETURN; +static void gripe_converting_name (const char *name) +{ + error (FATAL, 0, _("Can't convert %s to cat name"), name); + abort (); /* error should have exited; help compilers prove noreturn */ +} + +/* 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, int fsstnd) +{ + char *to_name, *t1 = NULL; + char *t2 = NULL; +#ifdef COMP_SRC + struct compression *comp; +#endif /* COMP_SRC */ + char *namestem; + +#ifdef COMP_SRC + comp = comp_info (name, 1); + if (comp) + namestem = comp->stem; + else +#endif /* COMP_SRC */ + 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, 1); + 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, 0); + free (cat_path); + } else if (STRNEQ (man_file, path, path_len) && + man_file[path_len] == '/') + cat_file = convert_name (man_file, 1); + 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, 0); + free (cat_path); + } else + cat_file = convert_name (original, 1); + + 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 non-zero if so, otherwise zero. + */ +static int duplicate_candidates (struct candidate *left, + struct candidate *right) +{ + const char *slash1, *slash2; + struct locale_bits bits1, bits2; + int ret; + + if (left->ult && right->ult && STREQ (left->ult, right->ult)) + return 1; /* 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 0; /* different name/section/extension */ + + if (STREQ (left->path, right->path)) + return 1; /* 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 0; /* 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 = 0; /* different language/territory/modifier */ + else + /* Everything seems to be the same; we can find nothing to + * choose between them. + */ + ret = 1; + + free_locale_bits (&bits1); + free_locale_bits (&bits2); + return ret; +} + +static int compare_candidates (const struct candidate *left, + const struct candidate *right) +{ + const struct mandata *lsource = left->source, *rsource = right->source; + int sec_left = 0, sec_right = 0; + 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; + } + + /* Compare pure sections first, then ids, then extensions. + * Rationale: whatis refs get the same section and extension as + * their source, but may be supplanted by a real page with a + * slightly different extension, possibly in another hierarchy (!); + * see Debian bug #204249 for the gory details. + * + * 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)) { + const char **sp; + + /* 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. + */ + for (sp = section_list; *sp; ++sp) { + if (!*(*sp + 1)) { + /* No extension */ + if (!sec_left && **sp == *(lsource->ext)) + sec_left = sp - section_list + 1; + if (!sec_right && **sp == *(rsource->ext)) + sec_right = sp - section_list + 1; + } else if (STREQ (*sp, lsource->ext)) { + sec_left = sp - section_list + 1; + } else if (STREQ (*sp, rsource->ext)) { + sec_right = sp - section_list + 1; + } + /* Keep looking for a more specific match */ + } + if (sec_left != sec_right) + return sec_left - sec_right; + + cmp = strcmp (lsource->sec, rsource->sec); + if (cmp) + return cmp; + } + + /* ULT_MAN comes first, etc. Consider SO_MAN equivalent to ULT_MAN. */ + cmp = compare_ids (lsource->id, rsource->id, 1); + 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; + + /* 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.territory, bits2.territory); \ + 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; + + 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 = ult_src (filename, path, NULL, + get_ult_flags (from_db, source->id), NULL); + 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) { + int 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, 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; + char **names = NULL, **np; + size_t names_len = 0; + 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 NROFF */ + /* + * Look for man page source files. + */ + + names = look_for_file (path, sec, name, 0, lff_opts); + if (!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) { + names = look_for_file (path, sec, name, 1, lff_opts); + cat = 1; + } + } + + for (np = names; np && *np; np++) + ++names_len; + order_files (path, names, names_len); + + for (np = names; np && *np; np++) { + struct mandata *info = infoalloc (); + char *info_buffer = filename_info (*np, info, name); + const char *ult; + int f; + + if (!info_buffer) { + free_mandata_struct (info); + continue; + } + info->addr = info_buffer; + + /* 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 (*np, path, NULL, ult_flags, NULL); + if (!ult) { + /* already warned */ + debug ("try_section(): bad link %s\n", *np); + free (info_buffer); + info->addr = NULL; + free_mandata_struct (info); + continue; + } + if (STREQ (ult, *np)) + info->id = ULT_MAN; + else + info->id = SO_MAN; + + f = add_candidate (cand_head, CANDIDATE_FILESYSTEM, + cat, name, path, ult, info); + found += f; + /* Free info and info_buffer if they weren't added to the + * candidates. + */ + if (f == 0) { + free (info_buffer); + info->addr = NULL; + free_mandata_struct (info); + } + /* Don't free info and info_buffer here. */ + } + + 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 char *man_file; + char *cat_file; + + man_file = ult_src (filename, candp->path, NULL, ult_flags, + NULL); + if (man_file == NULL) + goto out; + + debug ("found ultimate source file %s\n", man_file); + lang = lang_dir (man_file); + + cat_file = find_cat_file (candp->path, filename, man_file); + found = display (candp->path, man_file, 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) +{ + if (!catman) { + MYDBM_FILE dbf; + + dbf = MYDBM_RWOPEN (database); + if (dbf) { + if (dbdelete (dbf, page, info) == 1) + debug ("%s(%s) not in db!\n", page, info->ext); + MYDBM_CLOSE (dbf); + } + } +} +#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 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 char *man_file; + char *cat_file; + + man_file = ult_src (file, candp->path, NULL, + get_ult_flags (1, in->id), NULL); + if (man_file == NULL) { + free (title); + return found; /* zero */ + } + + debug ("found ultimate source file %s\n", man_file); + lang = lang_dir (man_file); + + cat_file = find_cat_file (candp->path, file, man_file); + found += display (candp->path, man_file, 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; + } + + /* show this page but force an update later to make sure + we haven't just added the new page */ + found_a_stray = 1; + + /* 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)\n", + candp->req_name, candp->source); + dbdelete_wrapper (candp->req_name, candp->source); + } +#endif /* MAN_DB_UPDATES */ + + return exists; +} + +static void db_hashtable_free (void *defn) +{ + free_mandata_struct (defn); +} + +#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 (0, 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 */ + +/* 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) +{ + struct mandata *loc, *data; + char *catpath; + int found = 0; +#ifdef MAN_DB_UPDATES + int found_stale = 0; +#endif /* MAN_DB_UPDATES */ + + /* find out where our db for this manpath should be */ + + catpath = get_catpath (manpath, global_manpath ? SYSTEM_CAT : USER_CAT); + free (database); + if (catpath) { + database = mkdbname (catpath); + free (catpath); + } else + database = mkdbname (manpath); + + if (!db_hash) + db_hash = hashtable_create (&db_hashtable_free); + + /* Have we looked here already? */ + data = hashtable_lookup (db_hash, manpath, strlen (manpath)); + + if (!data) { + MYDBM_FILE dbf; + + dbf = MYDBM_RDOPEN (database); + if (dbf && dbver_rd (dbf)) { + MYDBM_CLOSE (dbf); + dbf = NULL; + } + if (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) + data = dblookup_pattern + (dbf, name, section, match_case, + regex_opt, !names_only); + else + data = dblookup_all (dbf, name, section, + match_case); + hashtable_install (db_hash, manpath, strlen (manpath), + data); + MYDBM_CLOSE (dbf); + dbf = NULL; +#ifdef MAN_DB_CREATES + } else if (!global_manpath) { + /* create one */ + debug ("Failed to open %s O_RDONLY\n", database); + if (run_mandb (1, manpath, NULL)) { + data = infoalloc (); + data->next = NULL; + data->addr = NULL; + hashtable_install (db_hash, + manpath, strlen (manpath), + data); + return TRY_DATABASE_OPEN_FAILED; + } + return TRY_DATABASE_CREATED; +#endif /* MAN_DB_CREATES */ + } else { + debug ("Failed to open %s O_RDONLY\n", database); + data = infoalloc (); + data->next = (struct mandata *) NULL; + data->addr = NULL; + hashtable_install (db_hash, manpath, strlen (manpath), + data); + return TRY_DATABASE_OPEN_FAILED; + } + } + + /* if we already know that there is nothing here, get on with it */ + if (!data) + return 0; + + /* We already tried (and failed) to open this db before */ + if (!data->addr) + return TRY_DATABASE_OPEN_FAILED; + +#ifdef MAN_DB_UPDATES + /* Check that all the entries found are up to date. If not, the + * caller should try again. + */ + for (loc = data; loc; loc = loc->next) + 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 = 1; + + if (found_stale) { + hashtable_remove (db_hash, manpath, strlen (manpath)); + return TRY_DATABASE_UPDATED; + } +#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 */ + for (loc = data; loc; loc = loc->next) + 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); + + 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; + pipeline *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); + if (!decomp) + return 0; + pipeline_start (decomp); + while ((line = pipeline_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; + } + } + } + + pipeline_free (decomp); + return ret; +} + +static int do_global_apropos_section (const char *path, const char *sec, + const char *name) +{ + int found = 0; + char **names, **np; + size_t names_len = 0; + 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, "*", 0, LFF_WILDCARD); + if (regex_opt) + xregcomp (&search, name, + REG_EXTENDED | REG_NOSUB | + (match_case ? 0 : REG_ICASE)); + else + memset (&search, 0, sizeof search); + + for (np = names; np && *np; ++np) + ++names_len; + order_files (path, names, names_len); + + for (np = names; np && *np; ++np) { + struct mandata *info; + char *info_buffer; + char *title = NULL; + const char *man_file; + char *cat_file = NULL; + + if (!grep (*np, name, &search)) + continue; + + info = infoalloc (); + info_buffer = filename_info (*np, info, NULL); + if (!info_buffer) + goto next; + info->addr = info_buffer; + + title = xasprintf ("%s(%s)", strchr (info_buffer, '\0') + 1, + info->ext); + man_file = ult_src (*np, path, NULL, ult_flags, NULL); + if (!man_file) + goto next; + lang = lang_dir (man_file); + cat_file = find_cat_file (path, *np, man_file); + if (display (path, man_file, cat_file, title, NULL)) + found = 1; + free (lang); + lang = NULL; + +next: + free (cat_file); + free (title); + free_mandata_struct (info); + } + + if (regex_opt) + regfree (&search); + + if (!global_manpath) + regain_effective_privs (); + + return found; +} + +static int do_global_apropos (const char *name, int *found) +{ + const char **my_section_list; + const char **sp; + char **mp; + + if (section) { + my_section_list = XNMALLOC (2, const char *); + my_section_list[0] = section; + my_section_list[1] = NULL; + } else + my_section_list = section_list; + + for (sp = my_section_list; *sp; sp++) + for (mp = manpathlist; *mp; mp++) + *found += do_global_apropos_section (*mp, *sp, name); + + if (section) + free (my_section_list); + + return *found ? OK : NOT_FOUND; +} + +/* Each of local_man_loop and man sometimes calls the other. */ +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; + int local_mf = local_man_file; + + drop_effective_privs (); + local_man_file = 1; + 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; + char **old_manpathlist, **mp; + + debug ("recalculating manpath for executable " + "in %s\n", argv_dir); + + new_manp = get_manpath_from_path (argv_dir, 0); + 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 = XNMALLOC (MAXDIRS, char *); + memcpy (old_manpathlist, manpathlist, + MAXDIRS * sizeof (*manpathlist)); + create_pathlist (new_manp, manpathlist); + + man (argv_base, &found); + + for (mp = manpathlist; *mp; ++mp) + free (*mp); + memcpy (manpathlist, old_manpathlist, + MAXDIRS * sizeof (*manpathlist)); + free (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]" 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; + + dot = strrchr (page_name, '.'); + + if (dot && is_section (dot + 1)) { + *ret_name = xstrndup (page_name, dot - page_name); + *ret_section = xstrdup (dot + 1); + } else { + *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; + + for (mp = manpathlist; *mp; 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 (strchr (name, '/')) { + int status = local_man_loop (name); + if (status == OK) + *found = 1; + return status; + } + + if (section) + locate_page_in_manpath (section, name, &candidates, found); + else { + const char **sp; + + for (sp = section_list; *sp; sp++) { + locate_page_in_manpath (*sp, 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 const char **get_section_list (void) +{ + int i = 0; + const char **config_sections; + const char **sections = NULL; + const char *sec; + + /* Section list from configuration file, or STD_SECTIONS if it's + * empty. + */ + config_sections = get_sections (); + if (!*config_sections) { + free (config_sections); + config_sections = std_sections; + } + + 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. + */ + for (sec = strtok (colon_sep_section_list, ":,"); sec; + sec = strtok (NULL, ":,")) { + sections = xnrealloc (sections, i + 2, sizeof *sections); + sections[i++] = sec; + } + + if (i > 0) { + sections[i] = NULL; + return sections; + } else { + 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]); + + check_standard_fds (); + + 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"); + +/* 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 */ + + /* 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 */ +#ifdef MAN_OWNER + debug ("real user = %d; effective user = %d\n", ruid, euid); +#endif /* MAN_OWNER */ + + /* 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 (PAGER); + if (pathsearch_executable (pager_program)) + pager = PAGER; + else + pager = ""; + free (pager_program); + } + if (*pager == '\0') + pager = get_def_user ("cat", 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 ("\nusing %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)); + + debug ("manpath search path (with duplicates) = %s\n", manp); + + create_pathlist (manp, manpathlist); + + /* 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 (0, 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 int maybe_section = 0; + 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 = 1; + } + } + + 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 = 0; + if (global_apropos) + status = do_global_apropos (nextarg, &found); + else { + int found_subpage = 0; + if (subpages && first_arg < argc) { + char *subname = xasprintf ( + "%s-%s", nextarg, argv[first_arg]); + status = man (subname, &found); + free (subname); + if (status == OK) { + found_subpage = 1; + ++first_arg; + } + } + if (!found_subpage && subpages && first_arg < argc) { + char *subname = xasprintf ( + "%s_%s", nextarg, argv[first_arg]); + status = man (subname, &found); + free (subname); + if (status == OK) { + found_subpage = 1; + ++first_arg; + } + } + if (!found_subpage) + status = man (nextarg, &found); + } + + /* clean out the cache of database lookups for each man page */ + hashtable_free (db_hash); + db_hash = NULL; + + if (section && maybe_section) { + if (status != OK && !catman) { + /* Maybe the section wasn't a section after + * all? e.g. 'man 9wm fvwm'. + */ + int found_subpage = 0; + 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 = 1; + ++first_arg; + } + } + if (!found_subpage) + status = man (tmp, &found); + hashtable_free (db_hash); + db_hash = 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 = 0; + + chkr_garbage_detector (); + } + hashtable_free (db_hash); + db_hash = NULL; + + drop_effective_privs (); + + free (database); + free_pathlist (manpathlist); + free (internal_locale); + exit (exit_status); +} |