/*
 * db_lookup.c: low level database interface routines for man.
 *
 * Copyright (C) 1994, 1995 Graeme W. Wilford. (Wilf.)
 * Copyright (C) 2001, 2002, 2003, 2006, 2007, 2008, 2009, 2012
 *               Colin Watson.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Mon Aug  8 20:35:30 BST 1994  Wilf. (G.Wilford@ee.surrey.ac.uk)
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif /* HAVE_CONFIG_H */

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "attribute.h"
#include "error.h"
#include "fnmatch.h"
#include "gl_array_list.h"
#include "gl_xlist.h"
#include "regex.h"
#include "xalloc.h"
#include "xvasprintf.h"

#include "gettext.h"
#define _(String) gettext (String)

#include "manconfig.h"

#include "debug.h"
#include "fatal.h"
#include "filenames.h"
#include "glcontainers.h"
#include "wordfnmatch.h"
#include "xregcomp.h"

#include "mydbm.h"
#include "db_storage.h"

/* If using ndbm or BTREE, copy the static storage before doing anything
 * interesting with it. If using gdbm, firstkey and nextkey need to copy the
 * storage because our ordered wrappers keep an effectively static copy.
 */
datum copy_datum (datum dat)
{
	if (MYDBM_DPTR (dat)) {
		MYDBM_SET_DPTR (dat, memcpy (xmalloc (MYDBM_DSIZE (dat) + 1),
						      MYDBM_DPTR (dat),
						      MYDBM_DSIZE (dat)));
		MYDBM_DPTR (dat)[MYDBM_DSIZE (dat)] = '\0';
	}
	return dat;
}

/* gdbm does locking itself. */
#if defined(NDBM) || defined(BTREE)
void gripe_lock (const char *filename)
{
	error (0, errno, _("can't lock index cache %s"), filename);
}
#endif /* NDBM || BTREE */

/* issue fatal message, then exit */
_Noreturn void gripe_corrupt_data (MYDBM_FILE dbf)
{
	fatal (0, _("index cache %s corrupt"), dbf->name);
}

/* deal with situation where we cannot replace a key */
_Noreturn void gripe_replace_key (MYDBM_FILE dbf, const char *data)
{
	error (0, 0, _("cannot replace key %s"), data);
	gripe_corrupt_data (dbf);
}

static char *copy_if_set (const char *str)
{
	if (STREQ (str, "-"))
		return NULL;
	else
		return xstrdup (str);
}

const char * ATTRIBUTE_CONST dash_if_unset (const char *str)
{
	if (str)
		return str;
	else
		return "-";
}

/* Just print out what would be stored in the db */
void dbprintf (const struct mandata *info)
{
	debug ("name:      %s\n"
	       "sec. ext:  %s\n"
	       "section:   %s\n"
	       "comp. ext: %s\n"
	       "id:        %c\n"
	       "mtime:     %ld.%09ld\n"
	       "pointer:   %s\n"
	       "filter:    %s\n"
	       "whatis:    %s\n\n",
	       dash_if_unset (info->name),
	       info->ext, info->sec, info->comp,
	       info->id, (long) info->mtime.tv_sec, (long) info->mtime.tv_nsec,
	       info->pointer, info->filter, info->whatis);
}

/* Form a multi-style key from page and extension info. The page should
 * *not* be name_to_key()'d - that should only happen to the parent.
 */
datum make_multi_key (const char *page, const char *ext)
{
	datum key;
	char *value;

	value = xasprintf ("%s\t%s", page, ext);
	assert (value);
	memset (&key, 0, sizeof key);
	MYDBM_SET (key, value);
	return key;
}

/* Get the key that should be used for a given name. The caller is
 * responsible for freeing the return value.
 */
char *name_to_key (const char *name)
{
	char *low, *p;

	p = low = xmalloc (strlen (name) + 1);
	while (*name)
		*p++ = CTYPE (tolower, *name++);
	*p = *name;
	return low;
}

/* return char ptr array to the data's fields */
static char **split_data (MYDBM_FILE dbf, char *content, char *start[])
{
        int count;

	/* initialise pointers to first N-1 fields */
	for (count = 0; count < FIELDS - 1 ; count++) {
		start[count] = strsep (&content, "\t");
		if (!start[count]) {
			error (0, 0,
			       ngettext ("only %d field in content",
					 "only %d fields in content", count),
			       count);
			gripe_corrupt_data (dbf);
		}
	}

	/* initialise pointer to Nth field (whatis) */
	start[FIELDS - 1] = content;
	if (!start[FIELDS - 1]) {
		error (0, 0,
		       ngettext ("only %d field in content",
				 "only %d fields in content", FIELDS - 1),
		       FIELDS - 1);
		gripe_corrupt_data (dbf);
	}

	return start;
}

/* Parse the db-returned data and put it into a mandata format */
struct mandata *split_content (MYDBM_FILE dbf, char *cont_ptr)
{
	struct mandata *info;
	char *start[FIELDS];
	char **data;

	data = split_data (dbf, cont_ptr, start);

	info = XZALLOC (struct mandata);
	info->name = copy_if_set (*(data++));
	info->ext = xstrdup (*(data++));
	info->sec = xstrdup (*(data++));
	info->mtime.tv_sec = (time_t) atol (*(data++));
	info->mtime.tv_nsec = atol (*(data++));
	info->id = **(data++);				/* single char id */
	info->pointer = xstrdup (*(data++));
	info->filter = xstrdup (*(data++));
	info->comp = xstrdup (*(data++));
	info->whatis = xstrdup (*(data));
	return info;
}

bool ATTRIBUTE_PURE name_ext_equals (const void *elt1, const void *elt2)
{
	const struct name_ext *ref1 = elt1, *ref2 = elt2;
	return STREQ (ref1->name, ref2->name) && STREQ (ref1->ext, ref2->ext);
}

int ATTRIBUTE_PURE name_ext_compare (const void *elt1, const void *elt2)
{
	const struct name_ext *ref1 = elt1, *ref2 = elt2;
	int name_cmp = strcmp (ref1->name, ref2->name);
	if (name_cmp)
		return name_cmp;
	return strcmp (ref1->ext, ref2->ext);
}

/* Extract all of the names/extensions associated with this key. Each case
 * variant of a name will be returned separately.
 *
 * This returns a newly-allocated list of struct name_ext, which the caller
 * is expected to free.
 */
gl_list_t list_extensions (char *data)
{
	gl_list_t list = gl_list_create_empty (GL_ARRAY_LIST, name_ext_equals,
					       NULL, plain_free, true);
	char *name;

	while ((name = strsep (&data, "\t")) != NULL) {
		char *ext;
		struct name_ext *name_ext;

		ext = strsep (&data, "\t");
		if (!ext)
			break;

		name_ext = XMALLOC (struct name_ext);
		/* Don't copy these; they will point into the given string. */
		name_ext->name = name;
		name_ext->ext = ext;
		gl_sortedlist_add (list, name_ext_compare, name_ext);
	}

	debug ("found %zu names/extensions\n", gl_list_size (list));
	return list;
}

/* These should be bitwise-ored together. */
#define ALL	    0
#define	EXACT	    1
#define MATCH_CASE  2

/*
 There are three possibilities on lookup:

 1) No data exists, lookup will fail, zero-length list will be returned.
 2) One data item exists. Item is returned as first in list of structures.
 3) Many items exist. They are all returned, in a multiple structure list.
 */
static gl_list_t dblookup (MYDBM_FILE dbf, const char *page,
			   const char *section, int flags)
{
	gl_list_t infos;
	struct mandata *info = NULL;
	datum key, cont;

	infos = gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL,
				      (gl_listelement_dispose_fn)
				      free_mandata_struct,
				      true);

	memset (&key, 0, sizeof key);
	memset (&cont, 0, sizeof cont);

	MYDBM_SET (key, name_to_key (page));
	cont = MYDBM_FETCH (dbf, key);
	MYDBM_FREE_DPTR (key);

	if (MYDBM_DPTR (cont) == NULL)		/* No entries at all */
		;
	else if (*MYDBM_DPTR (cont) != '\t') {	/* Just one entry */
		bool matches = false;

		info = split_content (dbf, MYDBM_DPTR (cont));
		if (!info->name)
			info->name = xstrdup (page);
		if (!(flags & MATCH_CASE) || STREQ (info->name, page)) {
			if (section == NULL)
				matches = true;
			else if (flags & EXACT) {
				if (STREQ (section, info->ext))
					matches = true;
			} else {
				if (STRNEQ (section, info->ext,
					    strlen (section)))
					matches = true;
			}
		}
		if (matches)
			gl_list_add_last (infos, info);
		else
			free_mandata_struct (info);
	} else {				/* Multiple entries */
		gl_list_t refs;
		struct name_ext *ref;

		/* Extract all of the case-variant-names/extensions
		 * associated with this key.
		 */

		refs = list_extensions (MYDBM_DPTR (cont) + 1);

		/* Make the multi keys and look them up */

		GL_LIST_FOREACH (refs, ref) {
			datum multi_cont;

			memset (&multi_cont, 0, sizeof multi_cont);

			/* Decide whether this part of a multi key is
			 * suitable.
			 */

			if ((flags & MATCH_CASE) && !STREQ (ref->name, page))
				continue;

			if (section != NULL) {
				if (flags & EXACT) {
					if (!STREQ (section, ref->ext))
						continue;
				} else {
					if (!STRNEQ (section, ref->ext,
						     strlen (section)))
						continue;
				}
			}

			/* So the key is suitable ... */
			key = make_multi_key (ref->name, ref->ext);
			debug ("multi key lookup (%s)\n", MYDBM_DPTR (key));
			multi_cont = MYDBM_FETCH (dbf, key);
			if (MYDBM_DPTR (multi_cont) == NULL) {
				error (0, 0, _("bad fetch on multi key %s"),
				       MYDBM_DPTR (key));
				gripe_corrupt_data (dbf);
			}
			MYDBM_FREE_DPTR (key);

			/* Allocate info struct and add it to the list. */
			info = split_content (dbf, MYDBM_DPTR (multi_cont));
			if (!info->name)
				info->name = xstrdup (ref->name);
			gl_list_add_last (infos, info);
		}

		gl_list_free (refs);
	}
	MYDBM_FREE_DPTR (cont);

	return infos;
}

gl_list_t dblookup_all (MYDBM_FILE dbf, const char *page,
			const char *section, bool match_case)
{
	return dblookup (dbf, page, section,
			 ALL | (match_case ? MATCH_CASE : 0));
}

struct mandata *dblookup_exact (MYDBM_FILE dbf, const char *page,
				const char *section, bool match_case)
{
	gl_list_t infos = dblookup (dbf, page, section,
				    EXACT | (match_case ? MATCH_CASE : 0));
	struct mandata *info = NULL;

	if (gl_list_size (infos)) {
		/* Return the first item and free the rest of the list. */
		info = (struct mandata *) gl_list_get_at (infos, 0);
		gl_list_set_at (infos, 0, NULL);	/* steal memory */
	}
	gl_list_free (infos);
	return info;
}

gl_list_t dblookup_pattern (MYDBM_FILE dbf, const char *pattern,
			    const char *section, bool match_case,
			    bool pattern_regex, bool try_descriptions)
{
	gl_list_t infos;
	datum key, cont;
	regex_t preg;

	infos = gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL,
				      (gl_listelement_dispose_fn)
				      free_mandata_struct,
				      true);

	if (pattern_regex)
		xregcomp (&preg, pattern,
			  REG_EXTENDED | REG_NOSUB |
			  (match_case ? 0 : REG_ICASE));

#ifndef BTREE
	datum nextkey;

	key = MYDBM_FIRSTKEY (dbf);
	while (MYDBM_DPTR (key)) {
		cont = MYDBM_FETCH (dbf, key);
#else /* BTREE */
	int end;

	end = man_btree_nextkeydata (dbf, &key, &cont);
	while (!end) {
#endif /* !BTREE */
		struct mandata *info = NULL;
		char *tab;
		bool got_match;

		if (!MYDBM_DPTR (cont))
		{
			debug ("key was %s\n", MYDBM_DPTR (key));
			fatal (0,
			       _("Database %s corrupted; rebuild with "
				 "mandb --create"),
			       dbf->name);
		}

		if (*MYDBM_DPTR (key) == '$')
			goto nextpage;

#pragma GCC diagnostic push
#if GNUC_PREREQ(10,0)
#  pragma GCC diagnostic ignored "-Wanalyzer-use-after-free"
#endif
		if (*MYDBM_DPTR (cont) == '\t')
			goto nextpage;
#pragma GCC diagnostic pop

		/* a real page */

		info = split_content (dbf, MYDBM_DPTR (cont));

		/* If there's a section given, does it match either the
		 * section or extension of this page?
		 */
		if (section &&
		    (!STREQ (section, info->sec) &&
		     !STREQ (section, info->ext)))
			goto nextpage;

		tab = strrchr (MYDBM_DPTR (key), '\t');
		if (tab)
			 *tab = '\0';

		if (!info->name)
			info->name = xstrdup (MYDBM_DPTR (key));

		if (pattern_regex)
			got_match = (regexec (&preg, info->name,
					      0, NULL, 0) == 0);
		else
			got_match = fnmatch (pattern, info->name,
					     match_case ? 0
							: FNM_CASEFOLD) == 0;
		if (try_descriptions && !got_match && info->whatis) {
			if (pattern_regex)
				got_match = (regexec (&preg, info->whatis,
						      0, NULL, 0) == 0);
			else
				got_match = word_fnmatch (pattern,
							  info->whatis);
		}
		if (!got_match)
			goto nextpage_tab;

		gl_list_add_last (infos, info);
		info = NULL; /* avoid freeing later */

nextpage_tab:
		if (tab)
			*tab = '\t';
nextpage:
#ifndef BTREE
		nextkey = MYDBM_NEXTKEY (dbf, key);
		MYDBM_FREE_DPTR (cont);
		MYDBM_FREE_DPTR (key);
		key = nextkey;
#else /* BTREE */
		MYDBM_FREE_DPTR (cont);
		MYDBM_FREE_DPTR (key);
		end = man_btree_nextkeydata (dbf, &key, &cont);
#endif /* !BTREE */
		free_mandata_struct (info);
	}

	if (pattern_regex)
		regfree (&preg);

	return infos;
}
