/*****************************************************************************

NAME:
  bogoutil.c -- dumps & loads bogofilter text files from/to Berkeley DB format.

AUTHORS:
  Gyepi Sam    <gyepi@praxis-sw.com>
  David Relson <relson@osagesoftware.com>

******************************************************************************/

#include "common.h"

#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
 
#include "getopt.h"

#include "bogoconfig.h"
#include "bogofilter.h"
#include "bogohist.h"
#include "bool.h"
#include "buff.h"
#include "configfile.h"
#include "datastore.h"
#include "datastore_db.h"
#include "error.h"
#include "longoptions.h"
#include "maint.h"
#include "msgcounts.h"
#include "paths.h"
#include "prob.h"
#include "rand_sleep.h"
#include "robx.h"
#include "sighandler.h"
#include "swap.h"
#include "wordlists.h"
#include "xmalloc.h"
#include "xstrdup.h"

/* prototypes for dummies below: */
#include "score.h"

const char *progname = "bogoutil";

static int token_count = 0;

static bool maintain = false;
static bool onlyprint = false;

/* Function Definitions */

/* dummies to avoid score.o */
double msg_spamicity(void) { return .0; }
rc_t msg_status(void) { return RC_OK; }

static void ds_open_failure(bfpath *bfp, void *dbe)
{
    fprintf(stderr, "Error accessing file or directory '%s'.\n", bfp->filepath);
    if (errno != 0)
	fprintf(stderr, "error #%d - %s.\n", errno, strerror(errno));
    if (dbe != NULL)
	ds_cleanup(dbe);
    exit(EX_ERROR);
}

static ex_t ds_dump_hook(word_t *key, dsv_t *data,
			/*@unused@*/ void *userdata)
{
    (void)userdata;

    if (fDie)
	exit(EX_ERROR);

    token_count += 1;

    if (maintain && discard_token(key, data))
	return EX_OK;

    if (replace_nonascii_characters)
	do_replace_nonascii_characters(key->u.text, key->leng);

    fprintf(fpo, "%.*s %lu %lu",
	    CLAMP_INT_MAX(key->leng), key->u.text,
	    (unsigned long)data->spamcount,
	    (unsigned long)data->goodcount);
    if (data->date)
	fprintf(fpo, " %lu", (unsigned long)data->date);
    fprintf(fpo, "\n");

    fflush(stdout); /* solicit ferror flag if output is shorter than buffer */
    return ferror(stdout) ? EX_ERROR : EX_OK;
}

static ex_t dump_wordlist(bfpath *bfp)
{
    ex_t rc;
    void *dbe;

    token_count = 0;

    dbe = ds_init(bfp);
    rc = ds_oper(dbe, bfp, DS_READ, ds_dump_hook, NULL);
    ds_cleanup(dbe);

    if (rc != EX_OK)
	fprintf(stderr, "error dumping tokens!\n");
    else
	if (verbose)
	    fprintf(dbgout, "%d tokens dumped\n", token_count);

    return rc;
}

#define BUFSIZE 512
const char POSIX_space[] = " \f\n\r\t\v";

static byte *spanword(byte *t)
{
    /* skip leading whitespace */
    t += strspn((const char *)t, POSIX_space);
    /* span current word */
    t += strcspn((const char *)t, POSIX_space);
    if (*t)
	*t++ = '\0';
    return t;
}

/** determines if the token is a regular token or a special non-count
 * token (.ROBX, .WORDLIST_VERSION), returns true if the token is a
 * count token */
static bool is_count(const char *in)
{
    static const char *const msgc = MSG_COUNT;
    static const char *const enco = WORDLIST_ENCODING;

    /* anything that doesn't start with a . is a count */
    if (in[0] != '.')
	return true;
    /* .MSG_COUNT is also a count */
    if (strcmp(in, msgc) == 0)
	return true;
    /* .ENCODING is also a count */
    if (strcmp(in, enco) == 0)
	return true;

    return false;
}

static int load_wordlist(bfpath *bfp)
{
    void *dsh;
    byte buf[BUFSIZE];
    byte *p;
    int rv = 0;
    size_t len;
    int load_count = 0;
    unsigned long line = 0;
    unsigned long count[IX_SIZE], date;
    YYYYMMDD today_save = today;

    void *dbe = ds_init(bfp);

    dsh = ds_open(dbe, bfp, (dbmode_t)(DS_WRITE | DS_LOAD));
    if (dsh == NULL)
	/* print error, cleanup, and exit */
	ds_open_failure(bfp, dbe);

    memset(buf, '\0', BUFSIZE);

    if (DST_OK != ds_txn_begin(dsh))
	exit(EX_ERROR);

    for (;;) {
	dsv_t data;
	word_t *token;
	if (fgets((char *)buf, BUFSIZE, fpin) == NULL) {
	    if (ferror(fpin)) {
		perror(progname);
		rv = 2;
	    }
	    break;
	}

	line++;

	len = strlen((char *)buf);

	/* too short. */
	if (len < 4)
	    continue;

	p = spanword(buf);
	len = strlen((const char *)buf);

	if (max_token_len != 0 &&
	    len > max_token_len)
	    continue;		/* too long - discard */

	spamcount = (uint) atoi((const char *)p);
	if ((int) spamcount < 0)
	    spamcount = 0;
	p = spanword(p);

	goodcount = (uint) atoi((const char *)p);
	if ((int) goodcount < 0)
	    goodcount = 0;
	p = spanword(p);

	date = (uint) atoi((const char *)p);
	p = spanword(p);

	if (*p != '\0') {
	    fprintf(stderr,
		    "%s: Unexpected input [%s] on line %lu. "
		    "Expecting whitespace before count.\n",
		    progname, buf, line);
	    rv = 1;
	    break;
	}

	if (date == 0)				/* date as YYYYMMDD */
	    date = today_save;

	if (replace_nonascii_characters)
	    do_replace_nonascii_characters(buf, len);
 
 	token = word_new(buf, len);
	data.goodcount = goodcount;
	data.spamcount = spamcount;
	data.date = date;

	if (is_count((const char *)buf)
		&& !(maintain && discard_token(token, &data))) {
	    load_count += 1;
	    /* Slower, but allows multiple lists to be concatenated */
	    set_date(date);
	    switch (ds_read(dsh, token, &data)) {
		case 0:
		case 1:
		    break;
		default:
		    rv = 1;
	    }
	    data.spamcount += spamcount;
	    data.goodcount += goodcount;
	    if (ds_write(dsh, token, &data)) rv = 1;
	}
	word_free(token);
    }

    if (rv) {
	fprintf(stderr, "read or write error, aborting.\n");
	ds_txn_abort(dsh);
    } else {
	switch (ds_txn_commit(dsh)) {
	    case DST_FAILURE:
	    case DST_TEMPFAIL:
		fprintf(stderr, "commit failed\n");
		exit(EX_ERROR);
	    case DST_OK:
		break;
	}
    }

    ds_close(dsh);

    ds_cleanup(dbe);

    if (verbose)
	fprintf(dbgout, "%d tokens loaded\n", load_count);

    return rv;
}

static int get_token(buff_t *buff, FILE *fp)
{
    int rv = 0;

    if (fgets((char *)buff->t.u.text, buff->size, fp) == NULL) {
	if (ferror(fp)) {
	    perror(progname);
	    rv = 2;
	} else {
	    rv = 1;
	}
    } else {
	buff->t.leng = (uint) strlen((const char *)buff->t.u.text);
	if (buff->t.u.text[buff->t.leng - 1] == '\n' ) {
	    buff->t.leng -= 1;
	    buff->t.u.text[buff->t.leng] = (byte) '\0';
	}
	else
	{
	    fprintf(stderr,
		    "%s: Unexpected input [%s]. Does not end with newline "
		    "or line too long.\n",
		    progname, buff->t.u.text);
	    rv = 1;
	}
    }
    return rv;
}

static ex_t display_words(bfpath *bfp, int argc, char **argv, bool show_probability)
{
    byte buf[BUFSIZE];
    buff_t *buff = buff_new(buf, 0, BUFSIZE);
    const byte *word;

    const char *path = bfp->filepath;

    const char *head_format = !show_probability ? "%-30s %6s %6s\n"   : "%-30s %6s  %6s  %6s\n";
    const char *data_format = !show_probability ? "%-30s %6lu %6lu\n" : "%-30s %6lu  %6lu  %f\n";

    void *dsh = NULL; /* initialize to silence bogus gcc warning */
    void *dbe;

    int rv = 0;
    ex_t ec = EX_OK;

    dsv_t msgcnts;

    /* protect against broken stat(2) that succeeds for empty names */
    if (path == NULL || *path == '\0') {
        fprintf(stderr, "Expecting non-empty directory or file name.\n");
        return EX_ERROR;
    }

    dbe = ds_init(bfp);
    dsh = ds_open(dbe, bfp, DS_READ);;
    if (dsh == NULL)
	/* print error, cleanup, and exit */
	ds_open_failure(bfp, dbe);

    if (DST_OK != ds_txn_begin(dsh)) {
	ds_close(dsh);
	ds_cleanup(dbe);
	fprintf(stderr, "Cannot begin transaction.\n");
	return EX_ERROR;
    }

    if (show_probability)
    {
	ds_get_msgcounts(dsh, &msgcnts);
	robs = ROBS;
	robx = ROBX;
    }

    fprintf(fpo, head_format, "", "spam", "good", "  Fisher");
    while (argc >= 0)
    {
	dsv_t val;
	word_t *token;
	int rc;

	unsigned long spam_count;
	unsigned long good_count;
	double rob_prob = 0.0;
	
	if (argc == 0)
	{
	    if (get_token(buff, stdin) != 0)
		break;
	    token = &buff->t;
	} else {
	    word = (const byte *) *argv++;
	    if (--argc == 0)
		argc = -1;
	    token = word_news((const char *)word);
	}

	rc = ds_read(dsh, token, &val);
	switch (rc) {
	    case 0:
		spam_count = val.spamcount;
		good_count = val.goodcount;

		if (!show_probability)
		    fprintf(fpo, data_format, token->u.text, spam_count, good_count);
		else
		{
		    rob_prob = calc_prob(good_count, spam_count, msgcnts.goodcount, msgcnts.spamcount);
		    fprintf(fpo, data_format, token->u.text, spam_count, good_count, rob_prob);
		}
		break;
	    case 1:
		break;
	    default:
		fprintf(stderr, "Cannot read from database.\n");
		ec = EX_ERROR;
		goto finish;
	}

	if (token != &buff->t)
	    word_free(token);
    }

finish:
    if (DST_OK != rv ? ds_txn_abort(dsh) : ds_txn_commit(dsh)) {
	fprintf(stderr, "Cannot %s transaction.\n", rv ? "abort" : "commit");
	ec = EX_ERROR;
    }
    ds_close(dsh);
    ds_cleanup(dbe);

    buff_free(buff);

    return ec;
}

static ex_t get_robx(bfpath *bfp)
{
    double rx;
    int ret = 0;

    init_wordlist("word", bfp->filepath, 0, WL_REGULAR);
    rx = compute_robinson_x();
    if (rx < 0)
	return EX_ERROR;

    if (onlyprint)
	printf("%f\n", rx);
    else {
	dsv_t val;
	word_t *word_robx = word_news(ROBX_W);

	/* since compute_robinson_x() closes the wordlists, 
	   init_wordlist() must be called again */
	init_wordlist("word", bfp->filepath, 0, WL_REGULAR);

	open_wordlists(DS_WRITE);

	val.goodcount = 0;
	val.spamcount = (uint32_t) (rx * 1000000);
	do {
	    ret = ds_write(word_lists->dsh, word_robx, &val);
	    if (ret == DS_ABORT_RETRY) {
		rand_sleep(1000, 1000000);
		begin_wordlist(word_lists);
	    }
	} while (ret == DS_ABORT_RETRY);

	close_wordlists(true);
	free_wordlists();

	word_free(word_robx);
    }

    return ret ? EX_ERROR : EX_OK;
}

static void print_version(void)
{
    (void)fprintf(stdout,
		  "%s version %s\n"
		  "    Database: %s\n"
		  "Copyright (C) 2002-2010 David Relson\n"
		  "Copyright (C) 2003-2019 Matthias Andree\n"
		  "Copyright (C) 2002-2003 Gyepi Sam.\n\n"
		  "%s comes with ABSOLUTELY NO WARRANTY.  "
		  "This is free software, and\nyou are welcome to "
		  "redistribute it under the General Public License.  "
		  "See\nthe COPYING file with the source distribution for "
		  "details.\n"
		  "\n", 
		  progname, version, ds_version_str(), PACKAGE);
}

static void usage(FILE *fp)
{
    fprintf(fp, "Usage: %s {-h|-V}\n", progname);
    fprintf(fp, "   or: %s [OPTIONS] {-d|-l|-u|-m|-w|-p|--db-verify} file%s\n",
	    progname, DB_EXT);
    fprintf(fp, "   or: %s [OPTIONS] {-H|-r|-R} file\n", progname);
#if defined (ENABLE_DB_DATASTORE) || defined (ENABLE_SQLITE_DATASTORE)
    fprintf(fp, "   or: %s [OPTIONS] {--db-print-leafpage-count} file%s\n",
	    progname, DB_EXT);
    fprintf(fp, "   or: %s [OPTIONS] {--db-print-pagesize} file%s\n",
	    progname, DB_EXT);
#endif
#if	defined(ENABLE_DB_DATASTORE) && !defined(DISABLE_TRANSACTIONS)
    fprintf(fp, "   or: %s [OPTIONS] {--db-checkpoint} directory\n",
	    progname);
    fprintf(fp, "   or: %s [OPTIONS] {--db-list-logfiles} directory [list options]\n",
	    progname);
    fprintf(fp, "   or: %s [OPTIONS] {--db-prune|--db-remove-environment} directory\n",
	    progname);
    fprintf(fp, "   or: %s [OPTIONS] {--db-recover|--db-recover-harder} directory\n",
	    progname);
#endif
}

static const char *help_text[] = {
    "\n",
    "OPTIONS are:\n",
    "  -C, --no-config-file        - don't read standard config files.\n",
    "  -D, --debug-to-stdout       - direct debug output to stdout.\n",
#ifdef	ENABLE_DB_DATASTORE
    "  -k, --db-cachesize=size     - set Berkeley DB cache size (MB).\n",
#endif
    "  -v, --verbosity             - set debug verbosity level.\n",
    "  -x, --debug-flags=list      - set flags to display debug information.\n",
    "  -y, --timestamp-date=date   - set default date (format YYYYMMDD).\n",
    "\n",
    "Modes of operation are:\n",

    "  -h, --help                  - print this help message and exit.\n",
    "  -V, --version               - print version information and exit.\n",
    "\n",
    "  -d, --dump=file             - dump data from file to stdout.\n",
    "  -l, --load=file             - load data from stdin into file.\n",
    "  -u, --upgrade=file          - upgrade wordlist version.\n",
    "\n",

    "info options:\n",
    "  -w file                     - display counts for words from stdin.\n",
    "  -p file                     - display word counts and probabilities.\n",
    "  -I, --input-file=file       - read 'file' instead of standard input.\n",
    "  -H file                     - display histogram and statistics for wordlist.\n",
    "                                - use with -v  to exclude hapaxes.\n",
    "                                - use with -vv to exclude pure spam/ham.\n",
    "  -r file                     - compute Robinson's X for the specified file.\n",
    "  -R file                     - compute Robinson's X and save it in wordlist.\n",
    "\n",

    "database maintenance, the \"-m file\" option is required in this group:\n",
    "  -m file                     - enable maintenance works (expiring tokens).\n",
    "  -n                          - replace non-ascii characters with '?'.\n",
    "  -a age                      - exclude tokens with older ages.\n",
    "  -c cnt                      - exclude tokens with lower counts.\n",
    "  -s l,h                      - exclude tokens with lengths between 'l' and 'h'\n"
    "                                (low and high).\n",
#ifndef	DISABLE_UNICODE
    "  --unicode=yes/no            - convert wordlist to/from unicode\n",
#endif
    "\n",

    "token parsing options:\n",
    "  --min-token-len             - min len for single tokens\n",
    "  --max-token-len             - max len for single tokens\n",
    "  --max-multi-token-len       - max len for multi-word tokens\n",
    "  --multi-token-count         - number of tokens per multi-word token\n",
    "\n",

    NULL
    };

static void help(FILE *fp)
{
    uint i;
    const char **messages;
    usage(fp);
    messages = help_text;
    for (i=0; messages[i] != NULL; i++)
	(void)fprintf(fp, "%s", messages[i]);
    messages = dsm_help_bogoutil();
    for (i=0; messages[i] != NULL; i++)
	(void)fprintf(fp, "%s", messages[i]);
    (void)fprintf(fp,
		  "%s (version %s) is part of the bogofilter package.\n",
                  progname, version
	);
}

static const char *ds_file = NULL;
static bool  prob = false;

static cmd_t flag = M_NONE;

static struct option longopts_bogoutil[] = {
    /* longoptions.h - common options */
    LONGOPTIONS_COMMON
    /* longoptions.h - options for bogofilter and bogoutil */
    LONGOPTIONS_DB
    /* longoptions.h - options for bogolexer and bogoutil */
    LONGOPTIONS_LEX_UTIL

    /* bogoutil specific options */
    { "db-prune",                       R, 0, O_DB_PRUNE },
    { "db-checkpoint",                  R, 0, O_DB_CHECKPOINT },
    { "db-list-logfiles",               R, 0, O_DB_LIST_LOGFILES },
    { "db-print-leafpage-count",	R, 0, O_DB_PRINT_LEAFPAGE_COUNT },
    { "db-print-pagesize",		R, 0, O_DB_PRINT_PAGESIZE },
    { "db-recover",                     R, 0, O_DB_RECOVER },
    { "db-recover-harder",              R, 0, O_DB_RECOVER_HARDER },
    { "db-remove-environment",		R, 0, O_DB_REMOVE_ENVIRONMENT },
    { "db-verify",                      R, 0, O_DB_VERIFY },

    /* end of list */
    { NULL,				0, 0, 0 }
};

#define	OPTIONS	":a:c:Cd:DhH:I:k:l:m:nO:p:r:R:s:u:vVw:x:X:y:"

static int process_arglist(int argc, char **argv)
{
    int option;
    int count = 0;

    fpin = stdin;
    fpo  = stdout;
    dbgout = stderr;

#ifdef __EMX__
    _response (&argc, &argv);	/* expand response files (@filename) */
    _wildcard (&argc, &argv);	/* expand wildcards (*.*) */
#endif

    /* default: no token length checking */
    max_token_len = 0;

    while (1)
    {
	int option_index = 0;
	int this_option_optind = optind ? optind : 1;
	const char *name;

	option = getopt_long_chk(argc, argv, OPTIONS,
			     longopts_bogoutil, &option_index);

	if (option == -1)
 	    break;

	name = (option_index == 0) ? argv[this_option_optind] : longopts_bogoutil[option_index].name;
	count += process_arg(option, name, optarg, PR_NONE, PASS_1_CLI);
    }

    if (max_token_len != 0 && max_multi_token_len == 0) {
	/* token length checking ... */
	if (multi_token_count == 1)
	    max_multi_token_len = max_token_len + MAX_PREFIX_LEN;
	else
	    max_multi_token_len = max_token_len = (max_token_len+1) * multi_token_count + MAX_PREFIX_LEN;
    }

    if (count != 1)
    {
	usage(stderr);
	fprintf(stderr, "%s: Exactly one of the file or directory commands must be present.\n", progname);
	exit(EX_ERROR);
    }

    return count;
}

int process_arg(int option, const char *name, const char *val, priority_t precedence, arg_pass_t pass)
{
    int count = 0;

    (void) precedence;		/* suppress compiler warning */
    (void) pass;		/* suppress compiler warning */

    switch (option) {
    case '?':
	fprintf(stderr, "Unknown option '%s'.\n", name);
	break;

    case 'd':
	flag = M_DUMP;
	count += 1;
	ds_file = val;
	break;

    case O_CONFIG_FILE:
	read_config_file(val, false, false, PR_COMMAND, longopts_bogoutil);
	/*@fallthrough@*/
	/* fall through to suppress reading config files */

    case 'C':
	suppress_config_file = true;
	break;

    case 'k':
	db_cachesize=(uint) atoi(val);
	break;

    case 'l':
	flag = M_LOAD;
	count += 1;
	ds_file = val;
	break;

    case 'm':
	flag = M_MAINTAIN;
	count += 1;
	ds_file = val;
	break;

    case 'p':
	prob = true;
	/*@fallthrough@*/

    case 'w':
	flag = M_WORD;
	count += 1;
	ds_file = val;
	break;

    case O_DB_PRINT_LEAFPAGE_COUNT:
	flag = M_LEAFPAGES;
	count += 1;
	ds_file = val;
	break;

    case O_DB_PRINT_PAGESIZE:
	flag = M_PAGESIZE;
	count += 1;
	ds_file = val;
	break;

    case 'r':
	onlyprint = true;
	/* fallthrough */
    case 'R':
	flag = M_ROBX;
	count += 1;
	ds_file = val;
	break;

    case 'u':
	upgrade_wordlist_version = true;
	flag = M_MAINTAIN;
	count += 1;
	ds_file = val;
	break;

    case 'v':
	verbose++;
	break;

    case ':':
	fprintf(stderr, "Option %s requires an argument.\n", name);
	exit(EX_ERROR);

    case 'h':
	help(stdout);
	exit(EX_OK);

    case 'H':
	flag = M_HIST;
	count += 1;
	ds_file = val;
	break;

    case 'V':
	print_version();
	exit(EX_OK);

    case 'x':
	set_debug_mask(val);
	break;

    case 'X':
	set_bogotest(val);
	break;

    case 'a':
	maintain = true;
	thresh_date = string_to_date(val);
	break;

    case 'c':
	maintain = true;
	thresh_count = (uint) atoi(val);
	break;

    case 's':
    {
	unsigned long mi, ma;

	maintain = true;
	    
	if (2 == sscanf(val, "%lu,%lu", &mi, &ma)) {
	    size_min = mi;
	    size_max = ma;
	} else {
	    fprintf(stderr, "syntax error in argument \"%s\" of -s\n.",
		    val);
	    exit(EX_ERROR);
	}
    }
    break;

    case 'n':
	maintain = true;
	replace_nonascii_characters ^= true;
	break;

    case 'y':		/* date as YYYYMMDD */
    {
	YYYYMMDD date = string_to_date(val);
	maintain = true;
	if (date != 0 && date < 19990000) {
	    fprintf(stderr, "Date format for '-y' option is YYYYMMDD\n");
	    exit(EX_ERROR);
	}
	set_date( date );
	break;
    }

    case 'I':
	fpin = fopen(val, "r");
	if (fpin == NULL) {
	    fprintf(stderr, "Can't read file '%s'\n", val);
	    exit(EX_ERROR);
	}
	break;

    case 'O':
	fpo = fopen(val, "wt");
	if (fpo == NULL) {
	    fprintf(stderr, "Can't write file '%s'\n", val);
	    exit(EX_ERROR);
	}
	break;

    case 'D':
	dbgout = stdout;
	break;

    case O_DB_VERIFY:
	flag = M_VERIFY;
	count += 1;
	ds_file = val;
	break;

    case O_UNICODE:
	encoding = str_to_bool(val) ? E_UNICODE : E_RAW;
	break;

    case O_MAX_TOKEN_LEN:
	max_token_len = atoi(val);
	break;

    case O_MIN_TOKEN_LEN:
	min_token_len = atoi(val);
	break;

    case O_MAX_MULTI_TOKEN_LEN:
	max_multi_token_len=atoi(val);
	break;

    case O_MULTI_TOKEN_COUNT:
	multi_token_count=atoi(val);
	break;

    default:
	if (!dsm_options_bogoutil(option, &flag, &count, &ds_file, name, val)) {
	    fprintf(stderr, "Invalid option '%s'\n", name);
	    exit(EX_ERROR);
	}
    }

    return count;
}

static bfpath_mode get_mode(cmd_t cmd)
{
    bfpath_mode mode = BFP_ERROR;

    switch (cmd) {
    case M_LOAD:
	mode = BFP_MAY_CREATE;
	break;
    case M_DUMP:
    case M_HIST:
    case M_MAINTAIN:
    case M_ROBX:
    case M_VERIFY:
    case M_WORD:
    case M_CHECKPOINT:	/* database transaction/integrity operations */
    case M_CRECOVER:
    case M_LEAFPAGES:
    case M_PAGESIZE:
    case M_PURGELOGS:
    case M_RECOVER:
    case M_REMOVEENV:
    case M_LIST_LOGFILES:
	mode = BFP_MUST_EXIST;
	break;
    case M_NONE:
	usage(stderr);
	exit(EX_ERROR);
    }

    return mode;
}

int main(int argc, char *argv[])
{
    ex_t rc = EX_OK;

    bfpath *bfp;
    bfpath_mode mode;

    fBogoutil = true;

    init_globals();

    signal_setup();			/* setup to catch signals */
    atexit(bf_exit);

    progtype = build_progtype(progname, DB_TYPE);

    set_today();			/* compute current date for token age */

    process_arglist(argc, argv);
    process_config_files(false, longopts_bogoutil);	/* need to read lock sizes */

    /* Extra or missing parameters */
    if (flag != M_WORD && flag != M_LIST_LOGFILES && argc != optind) {
	fprintf(stderr, "Missing or extraneous argument.\n");
	usage(stderr);
	exit(EX_ERROR);
    }

    bfp = bfpath_create(ds_file);
    if (bogohome == NULL)
	set_bogohome( "." );		/* set default */

    bfpath_set_bogohome(bfp);

    mode = get_mode(flag);
    if (bfpath_check_mode(bfp, mode)) {
	if (bfp->isdir)
	    bfpath_set_filename(bfp, WORDLIST);
    }

    if (!bfpath_check_mode(bfp, mode)) {
	fprintf(stderr, "Can't open wordlist '%s'\n", bfp->filepath);
	exit(EX_ERROR);
    }

    errno = 0;		/* clear error status */

    switch (flag) {
	case M_RECOVER:
	    dsm_init(bfp);
	    rc = ds_recover(bfp, false);
	    break;
	case M_CRECOVER:
	    dsm_init(bfp);
	    rc = ds_recover(bfp, true);
	    break;
	case M_CHECKPOINT:
	    dsm_init(bfp);
	    rc = ds_checkpoint(bfp);
	    break;
	case M_LIST_LOGFILES:
	    dsm_init(bfp);
	    rc = ds_list_logfiles(bfp, argc - optind, argv + optind);
	    break;
	case M_PURGELOGS:
	    dsm_init(bfp);
	    rc = ds_purgelogs(bfp);
	    break;
	case M_REMOVEENV:
	    dsm_init(bfp);
	    rc = ds_remove(bfp);
	    break;
	case M_VERIFY:
	    dsm_init(bfp);
	    rc = ds_verify(bfp);
	    break;
	case M_LEAFPAGES:
	    {
		u_int32_t c;

		dsm_init(bfp);
		c = ds_leafpages(bfp);
		if (c == 0xffffffff) {
		    fprintf(stderr, "%s: error getting leaf page count.\n", ds_file);
		    rc = EX_ERROR;
		} else if (c == 0) {
		    puts("UNKNOWN");
		} else {
		    printf("%lu\n", (unsigned long)c);
		}
	    }
	    break;
	case M_PAGESIZE:
	    {
		u_int32_t s;

		dsm_init(bfp);
		s = ds_pagesize(bfp);
		if (s == 0xffffffff) {
		    fprintf(stderr, "%s: error getting page size.\n", ds_file);
		} else if (s == 0) {
		    puts("UNKNOWN");
		} else {
		    printf("%lu\n", (unsigned long)s);
		}
	    }
	    break;
	case M_DUMP:
	    rc = dump_wordlist(bfp);
	    break;
	case M_LOAD:
	    rc = load_wordlist(bfp) ? EX_ERROR : EX_OK;
	    break;
	case M_MAINTAIN:
	    maintain = true;
	    rc = maintain_wordlist_file(bfp);
	    break;
	case M_WORD:
	    argc -= optind;
	    argv += optind;
	    rc = display_words(bfp, argc, argv, prob);
	    break;
	case M_HIST:
	    rc = histogram(bfp);
	    break;
	case M_ROBX:
	    rc = get_robx(bfp);
	    break;
	case M_NONE:
	default:
	    /* should have been handled above */
	    abort();
	    break;
    }

    bfpath_free(bfp);

    return rc;
}
