[BACK]Return to mandocdb.c CVS log [TXT][DIR] Up to [cvsweb.bsd.lv] / mandoc

File: [cvsweb.bsd.lv] / mandoc / mandocdb.c (download)

Revision 1.78, Thu Dec 26 22:12:46 2013 UTC (10 years, 3 months ago) by schwarze
Branch: MAIN
Changes since 1.77: +183 -178 lines

To better support MLINKS, we will have to split the "docs" database
table into two tables, on for actual files on disk, one for (often
multiple) directory entries pointing to them.  That implies splitting
struct of into two structs, to be called "mpage" and "mlink",
respectively.  As a preparation, globally rename "of" and "inos"
to "mpage".  No functional change.

/*	$Id: mandocdb.c,v 1.78 2013/12/26 22:12:46 schwarze Exp $ */
/*
 * Copyright (c) 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
 * Copyright (c) 2011, 2012, 2013 Ingo Schwarze <schwarze@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <sys/stat.h>

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <fts.h>
#include <getopt.h>
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HAVE_OHASH
#include <ohash.h>
#else
#include "compat_ohash.h"
#endif
#include <sqlite3.h>

#include "mdoc.h"
#include "man.h"
#include "mandoc.h"
#include "manpath.h"
#include "mansearch.h"

#define	SQL_EXEC(_v) \
	if (SQLITE_OK != sqlite3_exec(db, (_v), NULL, NULL, NULL)) \
		fprintf(stderr, "%s\n", sqlite3_errmsg(db))
#define	SQL_BIND_TEXT(_s, _i, _v) \
	if (SQLITE_OK != sqlite3_bind_text \
		((_s), (_i)++, (_v), -1, SQLITE_STATIC)) \
		fprintf(stderr, "%s\n", sqlite3_errmsg(db))
#define	SQL_BIND_INT(_s, _i, _v) \
	if (SQLITE_OK != sqlite3_bind_int \
		((_s), (_i)++, (_v))) \
		fprintf(stderr, "%s\n", sqlite3_errmsg(db))
#define	SQL_BIND_INT64(_s, _i, _v) \
	if (SQLITE_OK != sqlite3_bind_int64 \
		((_s), (_i)++, (_v))) \
		fprintf(stderr, "%s\n", sqlite3_errmsg(db))
#define SQL_STEP(_s) \
	if (SQLITE_DONE != sqlite3_step((_s))) \
		fprintf(stderr, "%s\n", sqlite3_errmsg(db))

enum	op {
	OP_DEFAULT = 0, /* new dbs from dir list or default config */
	OP_CONFFILE, /* new databases from custom config file */
	OP_UPDATE, /* delete/add entries in existing database */
	OP_DELETE, /* delete entries from existing database */
	OP_TEST /* change no databases, report potential problems */
};

enum	form {
	FORM_SRC, /* format is -man or -mdoc */
	FORM_CAT, /* format is cat */
	FORM_NONE /* format is unknown */
};

struct	str {
	char		*utf8; /* key in UTF-8 form */
	const struct mpage *mpage; /* if set, the owning parse */
	uint64_t	 mask; /* bitmask in sequence */
	char		 key[]; /* the string itself */
};

struct	id {
	ino_t		 ino;
	dev_t		 dev;
};

struct	mpage {
	struct id	 id; /* used for hashing routine */
	struct mpage	*next; /* next in mpages_list */
	enum form	 dform; /* path-cued form */
	enum form	 sform; /* suffix-cued form */
	char		 file[PATH_MAX]; /* filename rel. to manpath */
	char		*desc; /* parsed description */
	char		*name; /* name (from filename) (not empty) */
	char		*sec; /* suffix-cued section (or empty) */
	char		*dsec; /* path-cued section (or empty) */
	char		*arch; /* path-cued arch. (or empty) */
};

struct	title {
	char		*title; /* name(sec/arch) given inside the file */
	char		*file; /* file name in case of mismatch */
};

enum	stmt {
	STMT_DELETE = 0, /* delete manpage */
	STMT_INSERT_DOC, /* insert manpage */
	STMT_INSERT_KEY, /* insert parsed key */
	STMT__MAX
};

typedef	int (*mdoc_fp)(struct mpage *, const struct mdoc_node *);

struct	mdoc_handler {
	mdoc_fp		 fp; /* optional handler */
	uint64_t	 mask;  /* set unless handler returns 0 */
};

static	void	 dbclose(int);
static	void	 dbindex(struct mchars *, int, const struct mpage *);
static	int	 dbopen(int);
static	void	 dbprune(void);
static	void	 fileadd(struct mpage *);
static	int	 filecheck(const char *);
static	void	 filescan(const char *);
static	void	*hash_alloc(size_t, void *);
static	void	 hash_free(void *, size_t, void *);
static	void	*hash_halloc(size_t, void *);
static	void	 inoadd(const struct stat *, struct mpage *);
static	int	 inocheck(const struct stat *);
static	void	 ofadd(int, const char *, const char *, const char *,
			const char *, const char *, const struct stat *);
static	void	 mpages_free(void);
static	void	 mpages_merge(struct mchars *, struct mparse *, int);
static	void	 parse_cat(struct mpage *);
static	void	 parse_man(struct mpage *, const struct man_node *);
static	void	 parse_mdoc(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_body(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_head(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Fd(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Fn(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_In(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Nd(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Nm(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Sh(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_St(struct mpage *, const struct mdoc_node *);
static	int	 parse_mdoc_Xr(struct mpage *, const struct mdoc_node *);
static	int	 set_basedir(const char *);
static	void	 putkey(const struct mpage *,
			const char *, uint64_t);
static	void	 putkeys(const struct mpage *,
			const char *, size_t, uint64_t);
static	void	 putmdockey(const struct mpage *,
			const struct mdoc_node *, uint64_t);
static	void	 say(const char *, const char *, ...);
static	int	 treescan(void);
static	size_t	 utf8(unsigned int, char [7]);
static	void	 utf8key(struct mchars *, struct str *);

static	char		*progname;
static	int	 	 use_all; /* use all found files */
static	int		 nodb; /* no database changes */
static	int	  	 verb; /* print what we're doing */
static	int	  	 warnings; /* warn about crap */
static	int		 exitcode; /* to be returned by main */
static	enum op	  	 op; /* operational mode */
static	char		 basedir[PATH_MAX]; /* current base directory */
static	struct ohash	 mpages; /* table of distinct manual pages */
static	struct ohash	 filenames; /* table of filenames */
static	struct ohash	 strings; /* table of all strings */
static	struct mpage	*mpages_list = NULL; /* vector of files to parse */
static	sqlite3		*db = NULL; /* current database */
static	sqlite3_stmt	*stmts[STMT__MAX]; /* current statements */

static	const struct mdoc_handler mdocs[MDOC_MAX] = {
	{ NULL, 0 },  /* Ap */
	{ NULL, 0 },  /* Dd */
	{ NULL, 0 },  /* Dt */
	{ NULL, 0 },  /* Os */
	{ parse_mdoc_Sh, TYPE_Sh }, /* Sh */
	{ parse_mdoc_head, TYPE_Ss }, /* Ss */
	{ NULL, 0 },  /* Pp */
	{ NULL, 0 },  /* D1 */
	{ NULL, 0 },  /* Dl */
	{ NULL, 0 },  /* Bd */
	{ NULL, 0 },  /* Ed */
	{ NULL, 0 },  /* Bl */
	{ NULL, 0 },  /* El */
	{ NULL, 0 },  /* It */
	{ NULL, 0 },  /* Ad */
	{ NULL, TYPE_An },  /* An */
	{ NULL, TYPE_Ar },  /* Ar */
	{ NULL, TYPE_Cd },  /* Cd */
	{ NULL, TYPE_Cm },  /* Cm */
	{ NULL, TYPE_Dv },  /* Dv */
	{ NULL, TYPE_Er },  /* Er */
	{ NULL, TYPE_Ev },  /* Ev */
	{ NULL, 0 },  /* Ex */
	{ NULL, TYPE_Fa },  /* Fa */
	{ parse_mdoc_Fd, 0 },  /* Fd */
	{ NULL, TYPE_Fl },  /* Fl */
	{ parse_mdoc_Fn, 0 },  /* Fn */
	{ NULL, TYPE_Ft },  /* Ft */
	{ NULL, TYPE_Ic },  /* Ic */
	{ parse_mdoc_In, TYPE_In },  /* In */
	{ NULL, TYPE_Li },  /* Li */
	{ parse_mdoc_Nd, TYPE_Nd },  /* Nd */
	{ parse_mdoc_Nm, TYPE_Nm },  /* Nm */
	{ NULL, 0 },  /* Op */
	{ NULL, 0 },  /* Ot */
	{ NULL, TYPE_Pa },  /* Pa */
	{ NULL, 0 },  /* Rv */
	{ parse_mdoc_St, 0 },  /* St */
	{ NULL, TYPE_Va },  /* Va */
	{ parse_mdoc_body, TYPE_Va },  /* Vt */
	{ parse_mdoc_Xr, 0 },  /* Xr */
	{ NULL, 0 },  /* %A */
	{ NULL, 0 },  /* %B */
	{ NULL, 0 },  /* %D */
	{ NULL, 0 },  /* %I */
	{ NULL, 0 },  /* %J */
	{ NULL, 0 },  /* %N */
	{ NULL, 0 },  /* %O */
	{ NULL, 0 },  /* %P */
	{ NULL, 0 },  /* %R */
	{ NULL, 0 },  /* %T */
	{ NULL, 0 },  /* %V */
	{ NULL, 0 },  /* Ac */
	{ NULL, 0 },  /* Ao */
	{ NULL, 0 },  /* Aq */
	{ NULL, TYPE_At },  /* At */
	{ NULL, 0 },  /* Bc */
	{ NULL, 0 },  /* Bf */
	{ NULL, 0 },  /* Bo */
	{ NULL, 0 },  /* Bq */
	{ NULL, TYPE_Bsx },  /* Bsx */
	{ NULL, TYPE_Bx },  /* Bx */
	{ NULL, 0 },  /* Db */
	{ NULL, 0 },  /* Dc */
	{ NULL, 0 },  /* Do */
	{ NULL, 0 },  /* Dq */
	{ NULL, 0 },  /* Ec */
	{ NULL, 0 },  /* Ef */
	{ NULL, TYPE_Em },  /* Em */
	{ NULL, 0 },  /* Eo */
	{ NULL, TYPE_Fx },  /* Fx */
	{ NULL, TYPE_Ms },  /* Ms */
	{ NULL, 0 },  /* No */
	{ NULL, 0 },  /* Ns */
	{ NULL, TYPE_Nx },  /* Nx */
	{ NULL, TYPE_Ox },  /* Ox */
	{ NULL, 0 },  /* Pc */
	{ NULL, 0 },  /* Pf */
	{ NULL, 0 },  /* Po */
	{ NULL, 0 },  /* Pq */
	{ NULL, 0 },  /* Qc */
	{ NULL, 0 },  /* Ql */
	{ NULL, 0 },  /* Qo */
	{ NULL, 0 },  /* Qq */
	{ NULL, 0 },  /* Re */
	{ NULL, 0 },  /* Rs */
	{ NULL, 0 },  /* Sc */
	{ NULL, 0 },  /* So */
	{ NULL, 0 },  /* Sq */
	{ NULL, 0 },  /* Sm */
	{ NULL, 0 },  /* Sx */
	{ NULL, TYPE_Sy },  /* Sy */
	{ NULL, TYPE_Tn },  /* Tn */
	{ NULL, 0 },  /* Ux */
	{ NULL, 0 },  /* Xc */
	{ NULL, 0 },  /* Xo */
	{ parse_mdoc_head, 0 },  /* Fo */
	{ NULL, 0 },  /* Fc */
	{ NULL, 0 },  /* Oo */
	{ NULL, 0 },  /* Oc */
	{ NULL, 0 },  /* Bk */
	{ NULL, 0 },  /* Ek */
	{ NULL, 0 },  /* Bt */
	{ NULL, 0 },  /* Hf */
	{ NULL, 0 },  /* Fr */
	{ NULL, 0 },  /* Ud */
	{ NULL, TYPE_Lb },  /* Lb */
	{ NULL, 0 },  /* Lp */
	{ NULL, TYPE_Lk },  /* Lk */
	{ NULL, TYPE_Mt },  /* Mt */
	{ NULL, 0 },  /* Brq */
	{ NULL, 0 },  /* Bro */
	{ NULL, 0 },  /* Brc */
	{ NULL, 0 },  /* %C */
	{ NULL, 0 },  /* Es */
	{ NULL, 0 },  /* En */
	{ NULL, TYPE_Dx },  /* Dx */
	{ NULL, 0 },  /* %Q */
	{ NULL, 0 },  /* br */
	{ NULL, 0 },  /* sp */
	{ NULL, 0 },  /* %U */
	{ NULL, 0 },  /* Ta */
};

int
main(int argc, char *argv[])
{
	int		  ch, i;
	size_t		  j, sz;
	const char	 *path_arg;
	struct mchars	 *mc;
	struct manpaths	  dirs;
	struct mparse	 *mp;
	struct ohash_info mpages_info, filename_info;

	memset(stmts, 0, STMT__MAX * sizeof(sqlite3_stmt *));
	memset(&dirs, 0, sizeof(struct manpaths));

	mpages_info.alloc  = filename_info.alloc  = hash_alloc;
	mpages_info.halloc = filename_info.halloc = hash_halloc;
	mpages_info.hfree  = filename_info.hfree  = hash_free;

	mpages_info.key_offset = offsetof(struct mpage, id);
	filename_info.key_offset = offsetof(struct mpage, file);

	progname = strrchr(argv[0], '/');
	if (progname == NULL)
		progname = argv[0];
	else
		++progname;

	/*
	 * We accept a few different invocations.  
	 * The CHECKOP macro makes sure that invocation styles don't
	 * clobber each other.
	 */
#define	CHECKOP(_op, _ch) do \
	if (OP_DEFAULT != (_op)) { \
		fprintf(stderr, "-%c: Conflicting option\n", (_ch)); \
		goto usage; \
	} while (/*CONSTCOND*/0)

	path_arg = NULL;
	op = OP_DEFAULT;

	while (-1 != (ch = getopt(argc, argv, "aC:d:ntu:vW")))
		switch (ch) {
		case ('a'):
			use_all = 1;
			break;
		case ('C'):
			CHECKOP(op, ch);
			path_arg = optarg;
			op = OP_CONFFILE;
			break;
		case ('d'):
			CHECKOP(op, ch);
			path_arg = optarg;
			op = OP_UPDATE;
			break;
		case ('n'):
			nodb = 1;
			break;
		case ('t'):
			CHECKOP(op, ch);
			dup2(STDOUT_FILENO, STDERR_FILENO);
			op = OP_TEST;
			nodb = warnings = 1;
			break;
		case ('u'):
			CHECKOP(op, ch);
			path_arg = optarg;
			op = OP_DELETE;
			break;
		case ('v'):
			verb++;
			break;
		case ('W'):
			warnings = 1;
			break;
		default:
			goto usage;
		}

	argc -= optind;
	argv += optind;

	if (OP_CONFFILE == op && argc > 0) {
		fprintf(stderr, "-C: Too many arguments\n");
		goto usage;
	}

	exitcode = (int)MANDOCLEVEL_OK;
	mp = mparse_alloc(MPARSE_AUTO, 
		MANDOCLEVEL_FATAL, NULL, NULL, NULL);
	mc = mchars_alloc();

	ohash_init(&mpages, 6, &mpages_info);
	ohash_init(&filenames, 6, &filename_info);

	if (OP_UPDATE == op || OP_DELETE == op || OP_TEST == op) {
		/* 
		 * Force processing all files.
		 */
		use_all = 1;

		/*
		 * All of these deal with a specific directory.
		 * Jump into that directory then collect files specified
		 * on the command-line.
		 */
		if (0 == set_basedir(path_arg))
			goto out;
		for (i = 0; i < argc; i++)
			filescan(argv[i]);
		if (0 == dbopen(1))
			goto out;
		if (OP_TEST != op)
			dbprune();
		if (OP_DELETE != op)
			mpages_merge(mc, mp, 0);
		dbclose(1);
	} else {
		/*
		 * If we have arguments, use them as our manpaths.
		 * If we don't, grok from manpath(1) or however else
		 * manpath_parse() wants to do it.
		 */
		if (argc > 0) {
			dirs.paths = mandoc_calloc
				(argc, sizeof(char *));
			dirs.sz = (size_t)argc;
			for (i = 0; i < argc; i++)
				dirs.paths[i] = mandoc_strdup(argv[i]);
		} else
			manpath_parse(&dirs, path_arg, NULL, NULL);

		/*
		 * First scan the tree rooted at a base directory, then
		 * build a new database and finally move it into place.
		 * Ignore zero-length directories and strip trailing
		 * slashes.
		 */
		for (j = 0; j < dirs.sz; j++) {
			sz = strlen(dirs.paths[j]);
			if (sz && '/' == dirs.paths[j][sz - 1])
				dirs.paths[j][--sz] = '\0';
			if (0 == sz)
				continue;

			if (j) {
				ohash_init(&mpages, 6, &mpages_info);
				ohash_init(&filenames, 6, &filename_info);
			}

			if (0 == set_basedir(dirs.paths[j]))
				goto out;
			if (0 == treescan())
				goto out;
			if (0 == set_basedir(dirs.paths[j]))
				goto out;
			if (0 == dbopen(0))
				goto out;

			mpages_merge(mc, mp, warnings && !use_all);
			dbclose(0);

			if (j + 1 < dirs.sz) {
				ohash_delete(&mpages);
				ohash_delete(&filenames);
				mpages_free();
			}
		}
	}
out:
	set_basedir(NULL);
	manpath_free(&dirs);
	mchars_free(mc);
	mparse_free(mp);
	ohash_delete(&mpages);
	ohash_delete(&filenames);
	mpages_free();
	return(exitcode);
usage:
	fprintf(stderr, "usage: %s [-anvW] [-C file]\n"
			"       %s [-anvW] dir ...\n"
			"       %s [-nvW] -d dir [file ...]\n"
			"       %s [-nvW] -u dir [file ...]\n"
			"       %s -t file ...\n",
		       progname, progname, progname, 
		       progname, progname);

	return((int)MANDOCLEVEL_BADARG);
}

/*
 * Scan a directory tree rooted at "basedir" for manpages.
 * We use fts(), scanning directory parts along the way for clues to our
 * section and architecture.
 *
 * If use_all has been specified, grok all files.
 * If not, sanitise paths to the following:
 *
 *   [./]man*[/<arch>]/<name>.<section> 
 *   or
 *   [./]cat<section>[/<arch>]/<name>.0
 *
 * TODO: accomodate for multi-language directories.
 */
static int
treescan(void)
{
	FTS		*f;
	FTSENT		*ff;
	int		 dform;
	char		*sec;
	const char	*dsec, *arch, *cp, *path;
	const char	*argv[2];

	argv[0] = ".";
	argv[1] = (char *)NULL;

	/*
	 * Walk through all components under the directory, using the
	 * logical descent of files.
	 */
	f = fts_open((char * const *)argv, FTS_LOGICAL, NULL);
	if (NULL == f) {
		exitcode = (int)MANDOCLEVEL_SYSERR;
		say("", NULL);
		return(0);
	}

	dsec = arch = NULL;
	dform = FORM_NONE;

	while (NULL != (ff = fts_read(f))) {
		path = ff->fts_path + 2;
		/*
		 * If we're a regular file, add an mpage by using the
		 * stored directory data and handling the filename.
		 * Disallow duplicate (hard-linked) files.
		 */
		if (FTS_F == ff->fts_info) {
			if (0 == strcmp(path, MANDOC_DB))
				continue;
			if ( ! use_all && ff->fts_level < 2) {
				if (warnings)
					say(path, "Extraneous file");
				continue;
			} else if (inocheck(ff->fts_statp)) {
				if (warnings)
					say(path, "Duplicate file");
				continue;
			} else if (NULL == (sec =
					strrchr(ff->fts_name, '.'))) {
				if ( ! use_all) {
					if (warnings)
						say(path,
						    "No filename suffix");
					continue;
				}
			} else if (0 == strcmp(++sec, "html")) {
				if (warnings)
					say(path, "Skip html");
				continue;
			} else if (0 == strcmp(sec, "gz")) {
				if (warnings)
					say(path, "Skip gz");
				continue;
			} else if (0 == strcmp(sec, "ps")) {
				if (warnings)
					say(path, "Skip ps");
				continue;
			} else if (0 == strcmp(sec, "pdf")) {
				if (warnings)
					say(path, "Skip pdf");
				continue;
			} else if ( ! use_all &&
			    ((FORM_SRC == dform && strcmp(sec, dsec)) ||
			     (FORM_CAT == dform && strcmp(sec, "0")))) {
				if (warnings)
					say(path, "Wrong filename suffix");
				continue;
			} else
				sec[-1] = '\0';
			ofadd(dform, path, ff->fts_name, dsec, sec,
					arch, ff->fts_statp);
			continue;
		} else if (FTS_D != ff->fts_info && 
				FTS_DP != ff->fts_info) {
			if (warnings)
				say(path, "Not a regular file");
			continue;
		}

		switch (ff->fts_level) {
		case (0):
			/* Ignore the root directory. */
			break;
		case (1):
			/*
			 * This might contain manX/ or catX/.
			 * Try to infer this from the name.
			 * If we're not in use_all, enforce it.
			 */
			dsec = NULL;
			dform = FORM_NONE;
			cp = ff->fts_name;
			if (FTS_DP == ff->fts_info)
				break;

			if (0 == strncmp(cp, "man", 3)) {
				dform = FORM_SRC;
				dsec = cp + 3;
			} else if (0 == strncmp(cp, "cat", 3)) {
				dform = FORM_CAT;
				dsec = cp + 3;
			}

			if (NULL != dsec || use_all) 
				break;

			if (warnings)
				say(path, "Unknown directory part");
			fts_set(f, ff, FTS_SKIP);
			break;
		case (2):
			/*
			 * Possibly our architecture.
			 * If we're descending, keep tabs on it.
			 */
			arch = NULL;
			if (FTS_DP != ff->fts_info && NULL != dsec)
				arch = ff->fts_name;
			break;
		default:
			if (FTS_DP == ff->fts_info || use_all)
				break;
			if (warnings)
				say(path, "Extraneous directory part");
			fts_set(f, ff, FTS_SKIP);
			break;
		}
	}

	fts_close(f);
	return(1);
}

/*
 * Add a file to the file vector.
 * Do not verify that it's a "valid" looking manpage (we'll do that
 * later).
 *
 * Try to infer the manual section, architecture, and page name from the
 * path, assuming it looks like
 *
 *   [./]man*[/<arch>]/<name>.<section> 
 *   or
 *   [./]cat<section>[/<arch>]/<name>.0
 *
 * Stuff this information directly into the mpage vector.
 * See treescan() for the fts(3) version of this.
 */
static void
filescan(const char *file)
{
	char		 buf[PATH_MAX];
	const char	*sec, *arch, *name, *dsec;
	char		*p, *start;
	int		 dform;
	struct stat	 st;

	assert(use_all);

	if (0 == strncmp(file, "./", 2))
		file += 2;

	if (NULL == realpath(file, buf)) {
		exitcode = (int)MANDOCLEVEL_BADARG;
		say(file, NULL);
		return;
	} else if (OP_TEST != op && strstr(buf, basedir) != buf) {
		exitcode = (int)MANDOCLEVEL_BADARG;
		say("", "%s: outside base directory", buf);
		return;
	} else if (-1 == stat(buf, &st)) {
		exitcode = (int)MANDOCLEVEL_BADARG;
		say(file, NULL);
		return;
	} else if ( ! (S_IFREG & st.st_mode)) {
		exitcode = (int)MANDOCLEVEL_BADARG;
		say(file, "Not a regular file");
		return;
	} else if (inocheck(&st)) {
		if (warnings)
			say(file, "Duplicate file");
		return;
	}
	start = buf + strlen(basedir);
	sec = arch = name = dsec = NULL;
	dform = FORM_NONE;

	/*
	 * First try to guess our directory structure.
	 * If we find a separator, try to look for man* or cat*.
	 * If we find one of these and what's underneath is a directory,
	 * assume it's an architecture.
	 */
	if (NULL != (p = strchr(start, '/'))) {
		*p++ = '\0';
		if (0 == strncmp(start, "man", 3)) {
			dform = FORM_SRC;
			dsec = start + 3;
		} else if (0 == strncmp(start, "cat", 3)) {
			dform = FORM_CAT;
			dsec = start + 3;
		}

		start = p;
		if (NULL != dsec && NULL != (p = strchr(start, '/'))) {
			*p++ = '\0';
			arch = start;
			start = p;
		} 
	}

	/*
	 * Now check the file suffix.
	 * Suffix of `.0' indicates a catpage, `.1-9' is a manpage.
	 */
	p = strrchr(start, '\0');
	while (p-- > start && '/' != *p && '.' != *p)
		/* Loop. */ ;

	if ('.' == *p) {
		*p++ = '\0';
		sec = p;
	}

	/*
	 * Now try to parse the name.
	 * Use the filename portion of the path.
	 */
	name = start;
	if (NULL != (p = strrchr(start, '/'))) {
		name = p + 1;
		*p = '\0';
	} 

	ofadd(dform, file, name, dsec, sec, arch, &st);
}

/*
 * See fileadd(). 
 */
static int
filecheck(const char *name)
{

	return(NULL != ohash_find(&filenames,
			ohash_qlookup(&filenames, name)));
}

/*
 * Use the standard hashing mechanism (K&R) to see if the given filename
 * already exists.
 */
static void
fileadd(struct mpage *mpage)
{
	unsigned int	 slot;

	slot = ohash_qlookup(&filenames, mpage->file);
	assert(NULL == ohash_find(&filenames, slot));
	ohash_insert(&filenames, slot, mpage);
}

/*
 * See inoadd().
 */
static int
inocheck(const struct stat *st)
{
	struct id	 id;
	uint32_t	 hash;

	memset(&id, 0, sizeof(id));
	id.ino = hash = st->st_ino;
	id.dev = st->st_dev;

	return(NULL != ohash_find(&mpages, ohash_lookup_memory(
			&mpages, (char *)&id, sizeof(id), hash)));
}

/*
 * The hashing function used here is quite simple: simply take the inode
 * and use uint32_t of its bits.
 * Then when we do the lookup, use both the inode and device identifier.
 */
static void
inoadd(const struct stat *st, struct mpage *mpage)
{
	uint32_t	 hash;
	unsigned int	 slot;

	mpage->id.ino = hash = st->st_ino;
	mpage->id.dev = st->st_dev;
	slot = ohash_lookup_memory
		(&mpages, (char *)&mpage->id, sizeof(mpage->id), hash);

	assert(NULL == ohash_find(&mpages, slot));
	ohash_insert(&mpages, slot, mpage);
}

static void
ofadd(int dform, const char *file, const char *name, const char *dsec,
	const char *sec, const char *arch, const struct stat *st)
{
	struct mpage	*mpage;
	int		 sform;

	assert(NULL != file);

	if (NULL == name)
		name = "";
	if (NULL == sec)
		sec = "";
	if (NULL == dsec)
		dsec = "";
	if (NULL == arch)
		arch = "";

	if ('0' == *sec) {
		sec = dsec;
		sform = FORM_CAT;
	} else if ('1' <= *sec && '9' >= *sec)
		sform = FORM_SRC;
	else
		sform = FORM_NONE;

	mpage = mandoc_calloc(1, sizeof(struct mpage));
	strlcpy(mpage->file, file, PATH_MAX);
	mpage->name = mandoc_strdup(name);
	mpage->sec = mandoc_strdup(sec);
	mpage->dsec = mandoc_strdup(dsec);
	mpage->arch = mandoc_strdup(arch);
	mpage->sform = sform;
	mpage->dform = dform;
	mpage->next = mpages_list;
	mpages_list = mpage;

	/*
	 * Add to unique identifier hash.
	 * Then if it's a source manual and we're going to use source in
	 * favour of catpages, add it to that hash.
	 */
	inoadd(st, mpage);
	fileadd(mpage);
}

static void
mpages_free(void)
{
	struct mpage	*mpage;

	while (NULL != (mpage = mpages_list)) {
		mpages_list = mpage->next;
		free(mpage->name);
		free(mpage->sec);
		free(mpage->dsec);
		free(mpage->arch);
		free(mpage);
	}
}

/*
 * Run through the files in the global vector "mpages_list" and add them to the
 * database specified in "basedir".
 *
 * This handles the parsing scheme itself, using the cues of directory
 * and filename to determine whether the file is parsable or not.
 */
static void
mpages_merge(struct mchars *mc, struct mparse *mp, int check_reachable)
{
	struct ohash		 title_table;
	struct ohash_info	 title_info, str_info;
	char			 buf[PATH_MAX];
	struct mpage		*mpage;
	struct mdoc		*mdoc;
	struct man		*man;
	struct title		*title_entry;
	char			*bufp, *title_str;
	const char		*msec, *march, *mtitle, *cp;
	size_t			 sz;
	int			 form;
	int			 match;
	unsigned int		 slot;
	enum mandoclevel	 lvl;

	str_info.alloc = hash_alloc;
	str_info.halloc = hash_halloc;
	str_info.hfree = hash_free;
	str_info.key_offset = offsetof(struct str, key);

	if (check_reachable) {
		title_info.alloc = hash_alloc;
		title_info.halloc = hash_halloc;
		title_info.hfree = hash_free;
		title_info.key_offset = offsetof(struct title, title);
		ohash_init(&title_table, 6, &title_info);
	}

	for (mpage = mpages_list; NULL != mpage; mpage = mpage->next) {
		/*
		 * If we're a catpage (as defined by our path), then see
		 * if a manpage exists by the same name (ignoring the
		 * suffix).
		 * If it does, then we want to use it instead of our
		 * own.
		 */
		if ( ! use_all && FORM_CAT == mpage->dform) {
			sz = strlcpy(buf, mpage->file, PATH_MAX);
			if (sz >= PATH_MAX) {
				if (warnings)
					say(mpage->file,
					    "Filename too long");
				continue;
			}
			bufp = strstr(buf, "cat");
			assert(NULL != bufp);
			memcpy(bufp, "man", 3);
			if (NULL != (bufp = strrchr(buf, '.')))
				*++bufp = '\0';
			strlcat(buf, mpage->dsec, PATH_MAX);
			if (filecheck(buf)) {
				if (warnings)
					say(mpage->file, "Man "
					    "source exists: %s", buf);
				continue;
			}
		}

		ohash_init(&strings, 6, &str_info);
		mparse_reset(mp);
		mdoc = NULL;
		man = NULL;
		match = 1;

		/*
		 * Try interpreting the file as mdoc(7) or man(7)
		 * source code, unless it is already known to be
		 * formatted.  Fall back to formatted mode.
		 */
		if (FORM_CAT != mpage->dform || FORM_CAT != mpage->sform) {
			lvl = mparse_readfd(mp, -1, mpage->file);
			if (lvl < MANDOCLEVEL_FATAL)
				mparse_result(mp, &mdoc, &man);
		}

		if (NULL != mdoc) {
			form = 1;
			msec = mdoc_meta(mdoc)->msec;
			march = mdoc_meta(mdoc)->arch;
			mtitle = mdoc_meta(mdoc)->title;
		} else if (NULL != man) {
			form = 1;
			msec = man_meta(man)->msec;
			march = mpage->arch;
			mtitle = man_meta(man)->title;
		} else {
			form = 0;
			msec = mpage->dsec;
			march = mpage->arch;
			mtitle = mpage->name;
		}

		if (NULL == msec)
			msec = "";
		if (NULL == march)
			march = "";
		if (NULL == mtitle)
			mtitle = "";

		/*
		 * Check whether the manual section given in a file
		 * agrees with the directory where the file is located.
		 * Some manuals have suffixes like (3p) on their
		 * section number either inside the file or in the
		 * directory name, some are linked into more than one
		 * section, like encrypt(1) = makekey(8).  Do not skip
		 * manuals for such reasons.
		 */
		if (warnings && !use_all && form &&
				strcasecmp(msec, mpage->dsec)) {
			match = 0;
			say(mpage->file, "Section \"%s\" "
				"manual in %s directory",
				msec, mpage->dsec);
		}

		/*
		 * Manual page directories exist for each kernel
		 * architecture as returned by machine(1).
		 * However, many manuals only depend on the
		 * application architecture as returned by arch(1).
		 * For example, some (2/ARM) manuals are shared
		 * across the "armish" and "zaurus" kernel
		 * architectures.
		 * A few manuals are even shared across completely
		 * different architectures, for example fdformat(1)
		 * on amd64, i386, sparc, and sparc64.
		 * Thus, warn about architecture mismatches,
		 * but don't skip manuals for this reason.
		 */
		if (warnings && !use_all && strcasecmp(march, mpage->arch)) {
			match = 0;
			say(mpage->file, "Architecture \"%s\" "
				"manual in \"%s\" directory",
				march, mpage->arch);
		}
		if (warnings && !use_all && strcasecmp(mtitle, mpage->name))
			match = 0;

		putkey(mpage, mpage->name, TYPE_Nm);

		if (NULL != mdoc) {
			if (NULL != (cp = mdoc_meta(mdoc)->name))
				putkey(mpage, cp, TYPE_Nm);
			assert(NULL == mpage->desc);
			parse_mdoc(mpage, mdoc_node(mdoc));
			putkey(mpage, NULL != mpage->desc ?
				mpage->desc : mpage->name, TYPE_Nd);
		} else if (NULL != man)
			parse_man(mpage, man_node(man));
		else
			parse_cat(mpage);

		/*
		 * Build a title string for the file.  If it matches
		 * the location of the file, remember the title as
		 * found; else, remember it as missing.
		 */

		if (check_reachable) {
			if (-1 == asprintf(&title_str, "%s(%s%s%s)", mtitle,
			    msec, '\0' == *march ? "" : "/", march)) {
				perror(NULL);
				exit((int)MANDOCLEVEL_SYSERR);
			}
			slot = ohash_qlookup(&title_table, title_str);
			title_entry = ohash_find(&title_table, slot);
			if (NULL == title_entry) {
				title_entry = mandoc_malloc(
						sizeof(struct title));
				title_entry->title = title_str;
				title_entry->file = mandoc_strdup(
				    match ? "" : mpage->file);
				ohash_insert(&title_table, slot,
						title_entry);
			} else {
				if (match)
					*title_entry->file = '\0';
				free(title_str);
			}
		}

		dbindex(mc, form, mpage);
		ohash_delete(&strings);
	}

	if (check_reachable) {
		title_entry = ohash_first(&title_table, &slot);
		while (NULL != title_entry) {
			if ('\0' != *title_entry->file)
				say(title_entry->file,
			    	    "Probably unreachable, title is %s",
				    title_entry->title);
			free(title_entry->title);
			free(title_entry->file);
			free(title_entry);
			title_entry = ohash_next(&title_table, &slot);
		}
		ohash_delete(&title_table);
	}
}

static void
parse_cat(struct mpage *mpage)
{
	FILE		*stream;
	char		*line, *p, *title;
	size_t		 len, plen, titlesz;

	if (NULL == (stream = fopen(mpage->file, "r"))) {
		if (warnings)
			say(mpage->file, NULL);
		return;
	}

	/* Skip to first blank line. */

	while (NULL != (line = fgetln(stream, &len)))
		if ('\n' == *line)
			break;

	/*
	 * Assume the first line that is not indented
	 * is the first section header.  Skip to it.
	 */

	while (NULL != (line = fgetln(stream, &len)))
		if ('\n' != *line && ' ' != *line)
			break;
	
	/*
	 * Read up until the next section into a buffer.
	 * Strip the leading and trailing newline from each read line,
	 * appending a trailing space.
	 * Ignore empty (whitespace-only) lines.
	 */

	titlesz = 0;
	title = NULL;

	while (NULL != (line = fgetln(stream, &len))) {
		if (' ' != *line || '\n' != line[len - 1])
			break;
		while (len > 0 && isspace((unsigned char)*line)) {
			line++;
			len--;
		}
		if (1 == len)
			continue;
		title = mandoc_realloc(title, titlesz + len);
		memcpy(title + titlesz, line, len);
		titlesz += len;
		title[titlesz - 1] = ' ';
	}

	/*
	 * If no page content can be found, or the input line
	 * is already the next section header, or there is no
	 * trailing newline, reuse the page title as the page
	 * description.
	 */

	if (NULL == title || '\0' == *title) {
		if (warnings)
			say(mpage->file, "Cannot find NAME section");
		assert(NULL == mpage->desc);
		mpage->desc = mandoc_strdup(mpage->name);
		putkey(mpage, mpage->name, TYPE_Nd);
		fclose(stream);
		free(title);
		return;
	}

	title = mandoc_realloc(title, titlesz + 1);
	title[titlesz] = '\0';

	/*
	 * Skip to the first dash.
	 * Use the remaining line as the description (no more than 70
	 * bytes).
	 */

	if (NULL != (p = strstr(title, "- "))) {
		for (p += 2; ' ' == *p || '\b' == *p; p++)
			/* Skip to next word. */ ;
	} else {
		if (warnings)
			say(mpage->file, "No dash in title line");
		p = title;
	}

	plen = strlen(p);

	/* Strip backspace-encoding from line. */

	while (NULL != (line = memchr(p, '\b', plen))) {
		len = line - p;
		if (0 == len) {
			memmove(line, line + 1, plen--);
			continue;
		} 
		memmove(line - 1, line + 1, plen - len);
		plen -= 2;
	}

	assert(NULL == mpage->desc);
	mpage->desc = mandoc_strdup(p);
	putkey(mpage, mpage->desc, TYPE_Nd);
	fclose(stream);
	free(title);
}

/*
 * Put a type/word pair into the word database for this particular file.
 */
static void
putkey(const struct mpage *mpage, const char *value, uint64_t type)
{

	assert(NULL != value);
	putkeys(mpage, value, strlen(value), type);
}

/*
 * Grok all nodes at or below a certain mdoc node into putkey().
 */
static void
putmdockey(const struct mpage *mpage,
	const struct mdoc_node *n, uint64_t m)
{

	for ( ; NULL != n; n = n->next) {
		if (NULL != n->child)
			putmdockey(mpage, n->child, m);
		if (MDOC_TEXT == n->type)
			putkey(mpage, n->string, m);
	}
}

static void
parse_man(struct mpage *mpage, const struct man_node *n)
{
	const struct man_node *head, *body;
	char		*start, *sv, *title;
	char		 byte;
	size_t		 sz, titlesz;

	if (NULL == n)
		return;

	/*
	 * We're only searching for one thing: the first text child in
	 * the BODY of a NAME section.  Since we don't keep track of
	 * sections in -man, run some hoops to find out whether we're in
	 * the correct section or not.
	 */

	if (MAN_BODY == n->type && MAN_SH == n->tok) {
		body = n;
		assert(body->parent);
		if (NULL != (head = body->parent->head) &&
				1 == head->nchild &&
				NULL != (head = (head->child)) &&
				MAN_TEXT == head->type &&
				0 == strcmp(head->string, "NAME") &&
				NULL != (body = body->child) &&
				MAN_TEXT == body->type) {

			title = NULL;
			titlesz = 0;

			/*
			 * Suck the entire NAME section into memory.
			 * Yes, we might run away.
			 * But too many manuals have big, spread-out
			 * NAME sections over many lines.
			 */

			for ( ; NULL != body; body = body->next) {
				if (MAN_TEXT != body->type)
					break;
				if (0 == (sz = strlen(body->string)))
					continue;
				title = mandoc_realloc
					(title, titlesz + sz + 1);
				memcpy(title + titlesz, body->string, sz);
				titlesz += sz + 1;
				title[titlesz - 1] = ' ';
			}
			if (NULL == title)
				return;

			title = mandoc_realloc(title, titlesz + 1);
			title[titlesz] = '\0';

			/* Skip leading space.  */

			sv = title;
			while (isspace((unsigned char)*sv))
				sv++;

			if (0 == (sz = strlen(sv))) {
				free(title);
				return;
			}

			/* Erase trailing space. */

			start = &sv[sz - 1];
			while (start > sv && isspace((unsigned char)*start))
				*start-- = '\0';

			if (start == sv) {
				free(title);
				return;
			}

			start = sv;

			/* 
			 * Go through a special heuristic dance here.
			 * Conventionally, one or more manual names are
			 * comma-specified prior to a whitespace, then a
			 * dash, then a description.  Try to puzzle out
			 * the name parts here.
			 */

			for ( ;; ) {
				sz = strcspn(start, " ,");
				if ('\0' == start[sz])
					break;

				byte = start[sz];
				start[sz] = '\0';

				putkey(mpage, start, TYPE_Nm);

				if (' ' == byte) {
					start += sz + 1;
					break;
				}

				assert(',' == byte);
				start += sz + 1;
				while (' ' == *start)
					start++;
			}

			if (sv == start) {
				putkey(mpage, start, TYPE_Nm);
				free(title);
				return;
			}

			while (isspace((unsigned char)*start))
				start++;

			if (0 == strncmp(start, "-", 1))
				start += 1;
			else if (0 == strncmp(start, "\\-\\-", 4))
				start += 4;
			else if (0 == strncmp(start, "\\-", 2))
				start += 2;
			else if (0 == strncmp(start, "\\(en", 4))
				start += 4;
			else if (0 == strncmp(start, "\\(em", 4))
				start += 4;

			while (' ' == *start)
				start++;

			assert(NULL == mpage->desc);
			mpage->desc = mandoc_strdup(start);
			putkey(mpage, mpage->desc, TYPE_Nd);
			free(title);
			return;
		}
	}

	for (n = n->child; n; n = n->next) {
		if (NULL != mpage->desc)
			break;
		parse_man(mpage, n);
	}
}

static void
parse_mdoc(struct mpage *mpage, const struct mdoc_node *n)
{

	assert(NULL != n);
	for (n = n->child; NULL != n; n = n->next) {
		switch (n->type) {
		case (MDOC_ELEM):
			/* FALLTHROUGH */
		case (MDOC_BLOCK):
			/* FALLTHROUGH */
		case (MDOC_HEAD):
			/* FALLTHROUGH */
		case (MDOC_BODY):
			/* FALLTHROUGH */
		case (MDOC_TAIL):
			if (NULL != mdocs[n->tok].fp)
			       if (0 == (*mdocs[n->tok].fp)(mpage, n))
				       break;
			if (mdocs[n->tok].mask)
				putmdockey(mpage, n->child,
				    mdocs[n->tok].mask);
			break;
		default:
			assert(MDOC_ROOT != n->type);
			continue;
		}
		if (NULL != n->child)
			parse_mdoc(mpage, n);
	}
}

static int
parse_mdoc_Fd(struct mpage *mpage, const struct mdoc_node *n)
{
	const char	*start, *end;
	size_t		 sz;

	if (SEC_SYNOPSIS != n->sec ||
			NULL == (n = n->child) || 
			MDOC_TEXT != n->type)
		return(0);

	/*
	 * Only consider those `Fd' macro fields that begin with an
	 * "inclusion" token (versus, e.g., #define).
	 */

	if (strcmp("#include", n->string))
		return(0);

	if (NULL == (n = n->next) || MDOC_TEXT != n->type)
		return(0);

	/*
	 * Strip away the enclosing angle brackets and make sure we're
	 * not zero-length.
	 */

	start = n->string;
	if ('<' == *start || '"' == *start)
		start++;

	if (0 == (sz = strlen(start)))
		return(0);

	end = &start[(int)sz - 1];
	if ('>' == *end || '"' == *end)
		end--;

	if (end > start)
		putkeys(mpage, start, end - start + 1, TYPE_In);
	return(1);
}

static int
parse_mdoc_In(struct mpage *mpage, const struct mdoc_node *n)
{

	if (NULL != n->child && MDOC_TEXT == n->child->type)
		return(0);

	putkey(mpage, n->child->string, TYPE_In);
	return(1);
}

static int
parse_mdoc_Fn(struct mpage *mpage, const struct mdoc_node *n)
{
	const char	*cp;

	if (NULL == (n = n->child) || MDOC_TEXT != n->type)
		return(0);

	/* 
	 * Parse: .Fn "struct type *name" "char *arg".
	 * First strip away pointer symbol. 
	 * Then store the function name, then type.
	 * Finally, store the arguments. 
	 */

	if (NULL == (cp = strrchr(n->string, ' ')))
		cp = n->string;

	while ('*' == *cp)
		cp++;

	putkey(mpage, cp, TYPE_Fn);

	if (n->string < cp)
		putkeys(mpage, n->string, cp - n->string, TYPE_Ft);

	for (n = n->next; NULL != n; n = n->next)
		if (MDOC_TEXT == n->type)
			putkey(mpage, n->string, TYPE_Fa);

	return(0);
}

static int
parse_mdoc_St(struct mpage *mpage, const struct mdoc_node *n)
{

	if (NULL == n->child || MDOC_TEXT != n->child->type)
		return(0);

	putkey(mpage, n->child->string, TYPE_St);
	return(1);
}

static int
parse_mdoc_Xr(struct mpage *mpage, const struct mdoc_node *n)
{
	char	*cp;

	if (NULL == (n = n->child))
		return(0);

	if (NULL == n->next) {
		putkey(mpage, n->string, TYPE_Xr);
		return(0);
	}

	if (-1 == asprintf(&cp, "%s(%s)", n->string, n->next->string)) {
		perror(NULL);
		exit((int)MANDOCLEVEL_SYSERR);
	}
	putkey(mpage, cp, TYPE_Xr);
	free(cp);
	return(0);
}

static int
parse_mdoc_Nd(struct mpage *mpage, const struct mdoc_node *n)
{
	size_t		 sz;

	if (MDOC_BODY != n->type)
		return(0);

	/*
	 * Special-case the `Nd' because we need to put the description
	 * into the document table.
	 */

	for (n = n->child; NULL != n; n = n->next) {
		if (MDOC_TEXT == n->type) {
			if (NULL != mpage->desc) {
				sz = strlen(mpage->desc) +
				     strlen(n->string) + 2;
				mpage->desc = mandoc_realloc(
				    mpage->desc, sz);
				strlcat(mpage->desc, " ", sz);
				strlcat(mpage->desc, n->string, sz);
			} else
				mpage->desc = mandoc_strdup(n->string);
		}
		if (NULL != n->child)
			parse_mdoc_Nd(mpage, n);
	}
	return(1);
}

static int
parse_mdoc_Nm(struct mpage *mpage, const struct mdoc_node *n)
{

	if (SEC_NAME == n->sec)
		return(1);
	else if (SEC_SYNOPSIS != n->sec || MDOC_HEAD != n->type)
		return(0);

	return(1);
}

static int
parse_mdoc_Sh(struct mpage *mpage, const struct mdoc_node *n)
{

	return(SEC_CUSTOM == n->sec && MDOC_HEAD == n->type);
}

static int
parse_mdoc_head(struct mpage *mpage, const struct mdoc_node *n)
{

	return(MDOC_HEAD == n->type);
}

static int
parse_mdoc_body(struct mpage *mpage, const struct mdoc_node *n)
{

	return(MDOC_BODY == n->type);
}

/*
 * Add a string to the hash table for the current manual.
 * Each string has a bitmask telling which macros it belongs to.
 * When we finish the manual, we'll dump the table.
 */
static void
putkeys(const struct mpage *mpage,
	const char *cp, size_t sz, uint64_t v)
{
	struct str	*s;
	unsigned int	 slot;
	const char	*end;

	if (0 == sz)
		return;

	end = cp + sz;
	slot = ohash_qlookupi(&strings, cp, &end);
	s = ohash_find(&strings, slot);

	if (NULL != s && mpage == s->mpage) {
		s->mask |= v;
		return;
	} else if (NULL == s) {
		s = mandoc_calloc(sizeof(struct str) + sz + 1, 1);
		memcpy(s->key, cp, sz);
		ohash_insert(&strings, slot, s);
	}
	s->mpage = mpage;
	s->mask = v;
}

/*
 * Take a Unicode codepoint and produce its UTF-8 encoding.
 * This isn't the best way to do this, but it works.
 * The magic numbers are from the UTF-8 packaging.
 * They're not as scary as they seem: read the UTF-8 spec for details.
 */
static size_t
utf8(unsigned int cp, char out[7])
{
	size_t		 rc;

	rc = 0;
	if (cp <= 0x0000007F) {
		rc = 1;
		out[0] = (char)cp;
	} else if (cp <= 0x000007FF) {
		rc = 2;
		out[0] = (cp >> 6  & 31) | 192;
		out[1] = (cp       & 63) | 128;
	} else if (cp <= 0x0000FFFF) {
		rc = 3;
		out[0] = (cp >> 12 & 15) | 224;
		out[1] = (cp >> 6  & 63) | 128;
		out[2] = (cp       & 63) | 128;
	} else if (cp <= 0x001FFFFF) {
		rc = 4;
		out[0] = (cp >> 18 &  7) | 240;
		out[1] = (cp >> 12 & 63) | 128;
		out[2] = (cp >> 6  & 63) | 128;
		out[3] = (cp       & 63) | 128;
	} else if (cp <= 0x03FFFFFF) {
		rc = 5;
		out[0] = (cp >> 24 &  3) | 248;
		out[1] = (cp >> 18 & 63) | 128;
		out[2] = (cp >> 12 & 63) | 128;
		out[3] = (cp >> 6  & 63) | 128;
		out[4] = (cp       & 63) | 128;
	} else if (cp <= 0x7FFFFFFF) {
		rc = 6;
		out[0] = (cp >> 30 &  1) | 252;
		out[1] = (cp >> 24 & 63) | 128;
		out[2] = (cp >> 18 & 63) | 128;
		out[3] = (cp >> 12 & 63) | 128;
		out[4] = (cp >> 6  & 63) | 128;
		out[5] = (cp       & 63) | 128;
	} else
		return(0);

	out[rc] = '\0';
	return(rc);
}

/*
 * Store the UTF-8 version of a key, or alias the pointer if the key has
 * no UTF-8 transcription marks in it.
 */
static void
utf8key(struct mchars *mc, struct str *key)
{
	size_t		 sz, bsz, pos;
	char		 utfbuf[7], res[5];
	char		*buf;
	const char	*seq, *cpp, *val;
	int		 len, u;
	enum mandoc_esc	 esc;

	assert(NULL == key->utf8);

	res[0] = '\\';
	res[1] = '\t';
	res[2] = ASCII_NBRSP;
	res[3] = ASCII_HYPH;
	res[4] = '\0';

	val = key->key;
	bsz = strlen(val);

	/*
	 * Pre-check: if we have no stop-characters, then set the
	 * pointer as ourselvse and get out of here.
	 */
	if (strcspn(val, res) == bsz) {
		key->utf8 = key->key;
		return;
	} 

	/* Pre-allocate by the length of the input */

	buf = mandoc_malloc(++bsz);
	pos = 0;

	while ('\0' != *val) {
		/*
		 * Halt on the first escape sequence.
		 * This also halts on the end of string, in which case
		 * we just copy, fallthrough, and exit the loop.
		 */
		if ((sz = strcspn(val, res)) > 0) {
			memcpy(&buf[pos], val, sz);
			pos += sz;
			val += sz;
		}

		if (ASCII_HYPH == *val) {
			buf[pos++] = '-';
			val++;
			continue;
		} else if ('\t' == *val || ASCII_NBRSP == *val) {
			buf[pos++] = ' ';
			val++;
			continue;
		} else if ('\\' != *val)
			break;

		/* Read past the slash. */

		val++;
		u = 0;

		/*
		 * Parse the escape sequence and see if it's a
		 * predefined character or special character.
		 */
		esc = mandoc_escape
			((const char **)&val, &seq, &len);
		if (ESCAPE_ERROR == esc)
			break;

		if (ESCAPE_SPECIAL != esc)
			continue;
		if (0 == (u = mchars_spec2cp(mc, seq, len)))
			continue;

		/*
		 * If we have a Unicode codepoint, try to convert that
		 * to a UTF-8 byte string.
		 */
		cpp = utfbuf;
		if (0 == (sz = utf8(u, utfbuf)))
			continue;

		/* Copy the rendered glyph into the stream. */

		sz = strlen(cpp);
		bsz += sz;

		buf = mandoc_realloc(buf, bsz);

		memcpy(&buf[pos], cpp, sz);
		pos += sz;
	}

	buf[pos] = '\0';
	key->utf8 = buf;
}

/*
 * Flush the current page's terms (and their bits) into the database.
 * Wrap the entire set of additions in a transaction to make sqlite be a
 * little faster.
 * Also, UTF-8-encode the description at the last possible moment.
 */
static void
dbindex(struct mchars *mc, int form, const struct mpage *mpage)
{
	struct str	*key;
	const char	*desc;
	int64_t		 recno;
	size_t		 i;
	unsigned int	 slot;

	if (verb)
		say(mpage->file, "Adding to index");

	if (nodb)
		return;

	desc = "";
	if (NULL != mpage->desc && '\0' != *mpage->desc) {
		key = ohash_find(&strings,
			ohash_qlookup(&strings, mpage->desc));
		assert(NULL != key);
		if (NULL == key->utf8)
			utf8key(mc, key);
		desc = key->utf8;
	}

	SQL_EXEC("BEGIN TRANSACTION");

	i = 1;
	SQL_BIND_TEXT(stmts[STMT_INSERT_DOC], i, mpage->file);
	SQL_BIND_TEXT(stmts[STMT_INSERT_DOC], i, mpage->sec);
	SQL_BIND_TEXT(stmts[STMT_INSERT_DOC], i, mpage->arch);
	SQL_BIND_TEXT(stmts[STMT_INSERT_DOC], i, desc);
	SQL_BIND_INT(stmts[STMT_INSERT_DOC], i, form);
	SQL_STEP(stmts[STMT_INSERT_DOC]);
	recno = sqlite3_last_insert_rowid(db);
	sqlite3_reset(stmts[STMT_INSERT_DOC]);

	for (key = ohash_first(&strings, &slot); NULL != key;
	     key = ohash_next(&strings, &slot)) {
		assert(key->mpage == mpage);
		if (NULL == key->utf8)
			utf8key(mc, key);
		i = 1;
		SQL_BIND_INT64(stmts[STMT_INSERT_KEY], i, key->mask);
		SQL_BIND_TEXT(stmts[STMT_INSERT_KEY], i, key->utf8);
		SQL_BIND_INT64(stmts[STMT_INSERT_KEY], i, recno);
		SQL_STEP(stmts[STMT_INSERT_KEY]);
		sqlite3_reset(stmts[STMT_INSERT_KEY]);
		if (key->utf8 != key->key)
			free(key->utf8);
		free(key);
	}

	SQL_EXEC("END TRANSACTION");
}

static void
dbprune(void)
{
	struct mpage	*mpage;
	size_t		 i;

	if (nodb)
		return;

	for (mpage = mpages_list; NULL != mpage; mpage = mpage->next) {
		i = 1;
		SQL_BIND_TEXT(stmts[STMT_DELETE], i, mpage->file);
		SQL_STEP(stmts[STMT_DELETE]);
		sqlite3_reset(stmts[STMT_DELETE]);
		if (verb)
			say(mpage->file, "Deleted from index");
	}
}

/*
 * Close an existing database and its prepared statements.
 * If "real" is not set, rename the temporary file into the real one.
 */
static void
dbclose(int real)
{
	size_t		 i;

	if (nodb)
		return;

	for (i = 0; i < STMT__MAX; i++) {
		sqlite3_finalize(stmts[i]);
		stmts[i] = NULL;
	}

	sqlite3_close(db);
	db = NULL;

	if (real)
		return;

	if (-1 == rename(MANDOC_DB "~", MANDOC_DB)) {
		exitcode = (int)MANDOCLEVEL_SYSERR;
		say(MANDOC_DB, NULL);
	}
}

/*
 * This is straightforward stuff.
 * Open a database connection to a "temporary" database, then open a set
 * of prepared statements we'll use over and over again.
 * If "real" is set, we use the existing database; if not, we truncate a
 * temporary one.
 * Must be matched by dbclose().
 */
static int
dbopen(int real)
{
	const char	*file, *sql;
	int		 rc, ofl;

	if (nodb) 
		return(1);

	ofl = SQLITE_OPEN_READWRITE;
	if (0 == real) {
		file = MANDOC_DB "~";
		if (-1 == remove(file) && ENOENT != errno) {
			exitcode = (int)MANDOCLEVEL_SYSERR;
			say(file, NULL);
			return(0);
		}
		ofl |= SQLITE_OPEN_EXCLUSIVE;
	} else
		file = MANDOC_DB;

	rc = sqlite3_open_v2(file, &db, ofl, NULL);
	if (SQLITE_OK == rc) 
		goto prepare_statements;
	if (SQLITE_CANTOPEN != rc) {
		exitcode = (int)MANDOCLEVEL_SYSERR;
		say(file, NULL);
		return(0);
	}

	sqlite3_close(db);
	db = NULL;

	if (SQLITE_OK != (rc = sqlite3_open(file, &db))) {
		exitcode = (int)MANDOCLEVEL_SYSERR;
		say(file, NULL);
		return(0);
	}

	sql = "CREATE TABLE \"docs\" (\n"
	      " \"file\" TEXT NOT NULL,\n"
	      " \"sec\" TEXT NOT NULL,\n"
	      " \"arch\" TEXT NOT NULL,\n"
	      " \"desc\" TEXT NOT NULL,\n"
	      " \"form\" INTEGER NOT NULL,\n"
	      " \"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL\n"
	      ");\n"
	      "\n"
	      "CREATE TABLE \"keys\" (\n"
	      " \"bits\" INTEGER NOT NULL,\n"
	      " \"key\" TEXT NOT NULL,\n"
	      " \"docid\" INTEGER NOT NULL REFERENCES docs(id) "
	      	"ON DELETE CASCADE,\n"
	      " \"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL\n"
	      ");\n"
	      "\n"
	      "CREATE INDEX \"key_index\" ON keys (key);\n";

	if (SQLITE_OK != sqlite3_exec(db, sql, NULL, NULL, NULL)) {
		exitcode = (int)MANDOCLEVEL_SYSERR;
		say(file, "%s", sqlite3_errmsg(db));
		return(0);
	}

prepare_statements:
	SQL_EXEC("PRAGMA foreign_keys = ON");
	sql = "DELETE FROM docs where file=?";
	sqlite3_prepare_v2(db, sql, -1, &stmts[STMT_DELETE], NULL);
	sql = "INSERT INTO docs "
		"(file,sec,arch,desc,form) VALUES (?,?,?,?,?)";
	sqlite3_prepare_v2(db, sql, -1, &stmts[STMT_INSERT_DOC], NULL);
	sql = "INSERT INTO keys "
		"(bits,key,docid) VALUES (?,?,?)";
	sqlite3_prepare_v2(db, sql, -1, &stmts[STMT_INSERT_KEY], NULL);

#ifndef __APPLE__
	/*
	 * When opening a new database, we can turn off
	 * synchronous mode for much better performance.
	 */

	if (real)
		SQL_EXEC("PRAGMA synchronous = OFF");
#endif

	return(1);
}

static void *
hash_halloc(size_t sz, void *arg)
{

	return(mandoc_calloc(sz, 1));
}

static void *
hash_alloc(size_t sz, void *arg)
{

	return(mandoc_malloc(sz));
}

static void
hash_free(void *p, size_t sz, void *arg)
{

	free(p);
}

static int
set_basedir(const char *targetdir)
{
	static char	 startdir[PATH_MAX];
	static int	 fd;

	/*
	 * Remember where we started by keeping a fd open to the origin
	 * path component: throughout this utility, we chdir() a lot to
	 * handle relative paths, and by doing this, we can return to
	 * the starting point.
	 */
	if ('\0' == *startdir) {
		if (NULL == getcwd(startdir, PATH_MAX)) {
			exitcode = (int)MANDOCLEVEL_SYSERR;
			if (NULL != targetdir)
				say(".", NULL);
			return(0);
		}
		if (-1 == (fd = open(startdir, O_RDONLY, 0))) {
			exitcode = (int)MANDOCLEVEL_SYSERR;
			say(startdir, NULL);
			return(0);
		}
		if (NULL == targetdir)
			targetdir = startdir;
	} else {
		if (-1 == fd)
			return(0);
		if (-1 == fchdir(fd)) {
			close(fd);
			basedir[0] = '\0';
			exitcode = (int)MANDOCLEVEL_SYSERR;
			say(startdir, NULL);
			return(0);
		}
		if (NULL == targetdir) {
			close(fd);
			return(1);
		}
	}
	if (NULL == realpath(targetdir, basedir)) {
		basedir[0] = '\0';
		exitcode = (int)MANDOCLEVEL_BADARG;
		say(targetdir, NULL);
		return(0);
	} else if (-1 == chdir(basedir)) {
		exitcode = (int)MANDOCLEVEL_BADARG;
		say("", NULL);
		return(0);
	}
	return(1);
}

static void
say(const char *file, const char *format, ...)
{
	va_list		 ap;

	if ('\0' != *basedir)
		fprintf(stderr, "%s", basedir);
	if ('\0' != *basedir && '\0' != *file)
		fputs("//", stderr);
	if ('\0' != *file)
		fprintf(stderr, "%s", file);
	fputs(": ", stderr);

	if (NULL == format) {
		perror(NULL);
		return;
	}

	va_start(ap, format);
	vfprintf(stderr, format, ap);
	va_end(ap);

	fputc('\n', stderr);
}