root/games/fortune/fortune/fortune.c
/*      $NetBSD: fortune.c,v 1.65 2020/07/21 03:05:40 nia Exp $ */

/*-
 * Copyright (c) 1986, 1993
 *      The Regents of the University of California.  All rights reserved.
 *
 * This code is derived from software contributed to Berkeley by
 * Ken Arnold.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#ifndef lint
__COPYRIGHT("@(#) Copyright (c) 1986, 1993\
 The Regents of the University of California.  All rights reserved.");
#endif /* not lint */

#ifndef lint
#if 0
static char sccsid[] = "@(#)fortune.c   8.1 (Berkeley) 5/31/93";
#else
__RCSID("$NetBSD: fortune.c,v 1.65 2020/07/21 03:05:40 nia Exp $");
#endif
#endif /* not lint */

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/endian.h>

#include <dirent.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#include <time.h>

#ifndef NO_REGEX
#include <regex.h>
#endif

#include "strfile.h"
#include "pathnames.h"

#define TRUE    1
#define FALSE   0
#define bool    short

#define MINW    6               /* minimum wait if desired */
#define CPERS   20              /* # of chars for each sec */
#define SLEN    160             /* # of chars in short fortune */

#define POS_UNKNOWN     ((off_t) -1)    /* pos for file unknown */
#define NO_PROB         (-1)            /* no prob specified for file */

#ifdef DEBUG
#define DPRINTF(l,x)    if (Debug >= l) fprintf x; else
#undef NDEBUG
#else
#define DPRINTF(l,x)
#define NDEBUG          1
#endif

typedef struct fd {
        int             percent;
        int             fd, datfd;
        off_t           pos;
        FILE            *inf;
        const char      *name;
        const char      *path;
        char            *datfile, *posfile;
        bool            read_tbl;
        bool            was_pos_file;
        STRFILE         tbl;
        int             num_children;
        struct fd       *child, *parent;
        struct fd       *next, *prev;
} FILEDESC;

static bool Found_one;                  /* did we find a match? */
static bool Find_files  = FALSE;        /* just find a list of proper fortune files */
static bool Wait        = FALSE;        /* wait desired after fortune */
static bool Short_only  = FALSE;        /* short fortune desired */
static bool Long_only   = FALSE;        /* long fortune desired */
static bool Offend      = FALSE;        /* offensive fortunes only */
static bool All_forts   = FALSE;        /* any fortune allowed */
static bool Equal_probs = FALSE;        /* scatter un-allocted prob equally */

#ifndef NO_REGEX
struct re {
        bool valid;
        regex_t regex;
};

static bool Match       = FALSE;        /* dump fortunes matching a pattern */
struct re Re_pat;
struct re Re_pat13;
static struct re *Re_use = NULL;
#endif

#ifdef DEBUG
static bool Debug = FALSE;              /* print debug messages */
#endif

static char *Fortbuf = NULL;            /* fortune buffer for -m */

static size_t Fort_len = 0;

static off_t Seekpts[2];                /* seek pointers to fortunes */

static FILEDESC *File_list = NULL,      /* Head of file list */
                *File_tail = NULL;      /* Tail of file list */
static FILEDESC *Fortfile;              /* Fortune file to use */

static STRFILE Noprob_tbl;              /* sum of data for all no prob files */

static int add_dir(FILEDESC *);
static int add_file(int,
            const char *, const char *, FILEDESC **, FILEDESC **, FILEDESC *);
static void all_forts(FILEDESC *, const char *);
static char *copy(const char *, size_t);
static void rot13(char *line, int len);
static void display(FILEDESC *);
static void do_free(void *);
static void *do_malloc(size_t);
static int form_file_list(char **, int);
static size_t fortlen(void);
static void get_fort(void);
static void get_pos(FILEDESC *);
static void get_tbl(FILEDESC *);
static void getargs(int, char *[]);
static void init_prob(void);
static int is_dir(const char *);
static int is_fortfile(const char *, char **, char **, int);
static int is_off_name(const char *);
static int max(int, int);
static FILEDESC *new_fp(void);
static char *off_name(const char *);
static void open_dat(FILEDESC *);
static void open_fp(FILEDESC *);
static FILEDESC *pick_child(FILEDESC *);
static void print_file_list(void);
static void print_list(FILEDESC *, int);
static void sum_noprobs(FILEDESC *);
static void sum_tbl(STRFILE *, STRFILE *);
static void usage(void) __dead;
static void zero_tbl(STRFILE *);

int main(int, char *[]);

#ifndef NO_REGEX
static void re_setup(struct re *rx, const char *pattern, bool ignore_case);
static void re_cleanup(struct re *rx);
static int find_matches(void);
static void matches_in_list(FILEDESC *);
static size_t maxlen_in_list(FILEDESC *);
#endif

#ifndef NAMLEN
#define         NAMLEN(d)       ((d)->d_namlen)
#endif

int
main(int ac, char *av[])
{
#ifdef OK_TO_WRITE_DISK
        int fd;
#endif /* OK_TO_WRITE_DISK */

        getargs(ac, av);

#ifndef NO_REGEX
        if (Match)
                exit(find_matches() != 0);
#endif

        init_prob();
        do {
                get_fort();
        } while ((Short_only && fortlen() > SLEN) ||
                 (Long_only && fortlen() <= SLEN));

        display(Fortfile);

#ifdef OK_TO_WRITE_DISK
        if ((fd = creat(Fortfile->posfile, 0666)) < 0)
                err(1, "Can't create `%s'", Fortfile->posfile);
#ifdef LOCK_EX
        /*
         * if we can, we exclusive lock, but since it isn't very
         * important, we just punt if we don't have easy locking
         * available.
         */
        (void) flock(fd, LOCK_EX);
#endif /* LOCK_EX */
        write(fd, (char *) &Fortfile->pos, sizeof Fortfile->pos);
        if (!Fortfile->was_pos_file)
                (void) chmod(Fortfile->path, 0666);
#ifdef LOCK_EX
        (void) flock(fd, LOCK_UN);
#endif /* LOCK_EX */
        close(fd);
#endif /* OK_TO_WRITE_DISK */
        if (Wait) {
                if (Fort_len == 0)
                        (void) fortlen();
                sleep(max(Fort_len / CPERS, MINW));
        }
        return(0);
}

static void
rot13(char *line, int len)
{
        char *p, ch;

        if (len == 0)
                len = strlen(line);

        for (p = line; (ch = *p) != 0; ++p)
                if (isupper((unsigned char)ch))
                        *p = 'A' + (ch - 'A' + 13) % 26;
                else if (islower((unsigned char)ch))
                        *p = 'a' + (ch - 'a' + 13) % 26;
}

static void
display(FILEDESC *fp)
{
        char line[BUFSIZ];

        open_fp(fp);
        (void) fseek(fp->inf, (long)Seekpts[0], SEEK_SET);
        for (Fort_len = 0; fgets(line, sizeof line, fp->inf) != NULL &&
            !STR_ENDSTRING(line, fp->tbl); Fort_len++) {
                if (fp->tbl.str_flags & STR_ROTATED)
                        rot13(line, 0);
                fputs(line, stdout);
        }
        (void) fflush(stdout);
}

/*
 * fortlen:
 *      Return the length of the fortune.
 */
static size_t
fortlen(void)
{
        size_t nchar;
        char line[BUFSIZ];

        if (!(Fortfile->tbl.str_flags & (STR_RANDOM | STR_ORDERED)))
                nchar = Seekpts[1] - Seekpts[0];
        else {
                open_fp(Fortfile);
                (void) fseek(Fortfile->inf, (long)Seekpts[0], SEEK_SET);
                nchar = 0;
                while (fgets(line, sizeof line, Fortfile->inf) != NULL &&
                       !STR_ENDSTRING(line, Fortfile->tbl))
                        nchar += strlen(line);
        }
        Fort_len = nchar;
        return nchar;
}

/*
 *      This routine evaluates the arguments on the command line
 */
static void
getargs(int argc, char **argv)
{
        int ignore_case;
#ifndef NO_REGEX
        char *pat = NULL;
#endif /* NO_REGEX */
        int ch;

        ignore_case = FALSE;

#ifdef DEBUG
        while ((ch = getopt(argc, argv, "aDefilm:osw")) != -1)
#else
        while ((ch = getopt(argc, argv, "aefilm:osw")) != -1)
#endif /* DEBUG */
                switch(ch) {
                case 'a':               /* any fortune */
                        All_forts++;
                        break;
#ifdef DEBUG
                case 'D':
                        Debug++;
                        break;
#endif /* DEBUG */
                case 'e':
                        Equal_probs++;  /* scatter un-allocted prob equally */
                        break;
                case 'f':               /* find fortune files */
                        Find_files++;
                        break;
                case 'l':               /* long ones only */
                        Long_only++;
                        Short_only = FALSE;
                        break;
                case 'o':               /* offensive ones only */
                        Offend++;
                        break;
                case 's':               /* short ones only */
                        Short_only++;
                        Long_only = FALSE;
                        break;
                case 'w':               /* give time to read */
                        Wait++;
                        break;
#ifdef NO_REGEX
                case 'i':                       /* case-insensitive match */
                case 'm':                       /* dump out the fortunes */
                        errx(1, "Can't match fortunes on this system (Sorry)");
#else /* NO_REGEX */
                case 'm':                       /* dump out the fortunes */
                        Match++;
                        pat = optarg;
                        break;
                case 'i':                       /* case-insensitive match */
                        ignore_case++;
                        break;
#endif /* NO_REGEX */
                case '?':
                default:
                        usage();
                }
        argc -= optind;
        argv += optind;

        if (!form_file_list(argv, argc))
                exit(1);        /* errors printed through form_file_list() */
#ifdef DEBUG
        if (Debug >= 1)
                print_file_list();
#endif /* DEBUG */
        if (Find_files) {
                print_file_list();
                exit(0);
        }

#ifndef NO_REGEX
        if (pat != NULL) {
                re_setup(&Re_pat, pat, ignore_case);
                rot13(pat, 0);
                re_setup(&Re_pat13, pat, ignore_case);
        }
#endif /* NO_REGEX */
}

/*
 * form_file_list:
 *      Form the file list from the file specifications.
 */
static int
form_file_list(char **files, int file_cnt)
{
        int i, percent;
        const char *sp;

        if (file_cnt == 0) {
                if (All_forts)
                        return add_file(NO_PROB, FORTDIR, NULL, &File_list,
                                        &File_tail, NULL);
                else
                        return add_file(NO_PROB, "fortunes", FORTDIR,
                                        &File_list, &File_tail, NULL);
        }
        for (i = 0; i < file_cnt; i++) {
                percent = NO_PROB;
                if (!isdigit((unsigned char)files[i][0]))
                        sp = files[i];
                else {
                        percent = 0;
                        for (sp = files[i]; isdigit((unsigned char)*sp); sp++)
                                percent = percent * 10 + *sp - '0';
                        if (percent > 100) {
                                warnx("Percentages must be <= 100");
                                return FALSE;
                        }
                        if (*sp == '.') {
                                warnx("Percentages must be integers");
                                return FALSE;
                        }
                        /*
                         * If the number isn't followed by a '%', then
                         * it was not a percentage, just the first part
                         * of a file name which starts with digits.
                         */
                        if (*sp != '%') {
                                percent = NO_PROB;
                                sp = files[i];
                        }
                        else if (*++sp == '\0') {
                                if (++i >= file_cnt) {
                                        warnx("Percentages must precede files");
                                        return FALSE;
                                }
                                sp = files[i];
                        }
                }
                if (strcmp(sp, "all") == 0)
                        sp = FORTDIR;
                if (!add_file(percent, sp, NULL, &File_list, &File_tail, NULL))
                        return FALSE;
        }
        return TRUE;
}

/*
 * add_file:
 *      Add a file to the file list.
 */
static int
add_file(int percent, const char *file, const char *dir,
         FILEDESC **head, FILEDESC **tail, FILEDESC *parent)
{
        FILEDESC *fp;
        int fd;
        const char *path;
        char *tpath, *offensive, *tfile = strdup(file), *tf;
        bool isdir;

        if (dir == NULL) {
                path = tfile;
                tpath = NULL;
        }
        else {
                tpath = do_malloc(strlen(dir) + strlen(file) + 2);
                (void) strcat(strcat(strcpy(tpath, dir), "/"), file);
                path = tpath;
        }
        if ((isdir = is_dir(path)) && parent != NULL) {
                if (tpath)
                        free(tpath);
                free(tfile);
                return FALSE;   /* don't recurse */
        }
        offensive = NULL;
        if (!isdir && parent == NULL && (All_forts || Offend) &&
            !is_off_name(path)) {
                offensive = off_name(path);
                if (Offend) {
                        if (tpath) {
                                free(tpath);
                                tpath = NULL;
                        }
                        path = offensive;
                        tf = off_name(tfile);
                        free(tfile);
                        tfile = tf;
                }
        }

        DPRINTF(1, (stderr, "adding file \"%s\"\n", path));
over:
        if ((fd = open(path, O_RDONLY)) < 0) {
                /*
                 * This is a sneak.  If the user said -a, and if the
                 * file we're given isn't a file, we check to see if
                 * there is a -o version.  If there is, we treat it as
                 * if *that* were the file given.  We only do this for
                 * individual files -- if we're scanning a directory,
                 * we'll pick up the -o file anyway.
                 */
                if (All_forts && offensive != NULL && path != offensive) {
                        path = offensive;
                        if (tpath) {
                                free(tpath);
                                tpath = NULL;
                        }
                        DPRINTF(1, (stderr, "\ttrying \"%s\"\n", tfile));
                        tf = off_name(tfile);
                        free(tfile);
                        tfile = tf;
                        goto over;
                }
                if (dir == NULL && tfile[0] != '/') {
                        int n = add_file(percent, tfile, FORTDIR, head, tail,
                                        parent);
                        free(tfile);
                        if (offensive)
                                free(offensive);
                        return n;
                }
                if (parent == NULL)
                        warn("Cannot open `%s'", path);
                if (tpath) {
                        free(tpath);
                        tpath = NULL;
                }
                free(tfile);
                if (offensive)
                        free(offensive);
                return FALSE;
        }

        DPRINTF(2, (stderr, "path = \"%s\"\n", path));

        fp = new_fp();
        fp->fd = fd;
        fp->percent = percent;
        fp->name = tfile;
        fp->path = path;
        fp->parent = parent;

        if ((isdir && !add_dir(fp)) ||
            (!isdir &&
             !is_fortfile(path, &fp->datfile, &fp->posfile, (parent != NULL))))
        {
                if (parent == NULL)
                        warnx("`%s' not a fortune file or directory", path);
                if (tpath) {
                        free(tpath);
                        tpath = NULL;
                }
                do_free(fp->datfile);
                do_free(fp->posfile);
                free(fp);
                do_free(offensive);
                return FALSE;
        }
        /*
         * If the user said -a, we need to make this node a pointer to
         * both files, if there are two.  We don't need to do this if
         * we are scanning a directory, since the scan will pick up the
         * -o file anyway.
         */
        if (All_forts && parent == NULL && !is_off_name(path) && offensive)
                all_forts(fp, offensive);
        if (*head == NULL)
                *head = *tail = fp;
        else if (fp->percent == NO_PROB) {
                (*tail)->next = fp;
                fp->prev = *tail;
                *tail = fp;
        }
        else {
                (*head)->prev = fp;
                fp->next = *head;
                *head = fp;
        }
#ifdef OK_TO_WRITE_DISK
        fp->was_pos_file = (access(fp->posfile, W_OK) >= 0);
#endif /* OK_TO_WRITE_DISK */

        return TRUE;
}

/*
 * new_fp:
 *      Return a pointer to an initialized new FILEDESC.
 */
static FILEDESC *
new_fp(void)
{
        FILEDESC *fp;

        fp = do_malloc(sizeof *fp);
        fp->datfd = -1;
        fp->pos = POS_UNKNOWN;
        fp->inf = NULL;
        fp->fd = -1;
        fp->percent = NO_PROB;
        fp->read_tbl = FALSE;
        fp->next = NULL;
        fp->prev = NULL;
        fp->child = NULL;
        fp->parent = NULL;
        fp->datfile = NULL;
        fp->posfile = NULL;
        return fp;
}

/*
 * off_name:
 *      Return a pointer to the offensive version of a file of this name.
 */
static char *
off_name(const char *file)
{
        char *new;

        new = copy(file, strlen(file) + 2);
        return strcat(new, "-o");
}

/*
 * is_off_name:
 *      Is the file an offensive-style name?
 */
static int
is_off_name(const char *file)
{
        int len;

        len = strlen(file);
        return (len >= 3 && file[len - 2] == '-' && file[len - 1] == 'o');
}

/*
 * all_forts:
 *      Modify a FILEDESC element to be the parent of two children if
 *      there are two children to be a parent of.
 */
static void
all_forts(FILEDESC *fp, const char *offensive)
{
        char *sp;
        FILEDESC *scene, *obscene;
        int fd;
        char *datfile, *posfile;

        posfile = NULL;

        if (fp->child != NULL)  /* this is a directory, not a file */
                return;
        if (!is_fortfile(offensive, &datfile, &posfile, FALSE))
                return;
        if ((fd = open(offensive, O_RDONLY)) < 0)
                return;
        DPRINTF(1, (stderr, "adding \"%s\" because of -a\n", offensive));
        scene = new_fp();
        obscene = new_fp();
        *scene = *fp;

        fp->num_children = 2;
        fp->child = scene;
        scene->next = obscene;
        obscene->next = NULL;
        scene->child = obscene->child = NULL;
        scene->parent = obscene->parent = fp;

        fp->fd = -1;
        scene->percent = obscene->percent = NO_PROB;

        obscene->fd = fd;
        obscene->inf = NULL;
        obscene->path = offensive;
        if ((sp = rindex(offensive, '/')) == NULL)
                obscene->name = offensive;
        else
                obscene->name = ++sp;
        obscene->datfile = datfile;
        obscene->posfile = posfile;
        obscene->read_tbl = FALSE;
#ifdef OK_TO_WRITE_DISK
        obscene->was_pos_file = (access(obscene->posfile, W_OK) >= 0);
#endif /* OK_TO_WRITE_DISK */
}

/*
 * add_dir:
 *      Add the contents of an entire directory.
 */
static int
add_dir(FILEDESC *fp)
{
        DIR *dir;
        struct dirent *dirent;
        FILEDESC *tailp;
        char *name;

        (void) close(fp->fd);
        fp->fd = -1;
        if ((dir = opendir(fp->path)) == NULL) {
                warn("Cannot open `%s'", fp->path);
                return FALSE;
        }
        tailp = NULL;
        DPRINTF(1, (stderr, "adding dir \"%s\"\n", fp->path));
        fp->num_children = 0;
        while ((dirent = readdir(dir)) != NULL) {
                if (NAMLEN(dirent) == 0)
                        continue;
                name = copy(dirent->d_name, NAMLEN(dirent));
                if (add_file(NO_PROB, name, fp->path, &fp->child, &tailp, fp))
                        fp->num_children++;
                else
                        free(name);
        }
        (void) closedir(dir);
        if (fp->num_children == 0) {
                warnx("`%s': No fortune files in directory.", fp->path);
                return FALSE;
        }
        return TRUE;
}

/*
 * is_dir:
 *      Return TRUE if the file is a directory, FALSE otherwise.
 */
static int
is_dir(const char *file)
{
        struct stat sbuf;

        if (stat(file, &sbuf) < 0)
                return FALSE;
        return (S_ISDIR(sbuf.st_mode));
}

/*
 * is_fortfile:
 *      Return TRUE if the file is a fortune database file.  We try and
 *      exclude files without reading them if possible to avoid
 *      overhead.  Files which start with ".", or which have "illegal"
 *      suffixes, as contained in suflist[], are ruled out.
 */
/* ARGSUSED */
static int
is_fortfile(const char *file, char **datp, char **posp, int check_for_offend)
{
        int i;
        const char *sp;
        char *datfile;
        static const char *const suflist[] = {
                /* list of "illegal" suffixes" */
                "dat", "pos", "c", "h", "p", "i", "f",
                "pas", "ftn", "ins.c", "ins,pas",
                "ins.ftn", "sml",
                NULL
        };

        DPRINTF(2, (stderr, "is_fortfile(%s) returns ", file));

        /*
         * Preclude any -o files for offendable people, and any non -o
         * files for completely offensive people.
         */
        if (check_for_offend && !All_forts) {
                i = strlen(file);
                if (Offend ^ (file[i - 2] == '-' && file[i - 1] == 'o'))
                        return FALSE;
        }

        if ((sp = rindex(file, '/')) == NULL)
                sp = file;
        else
                sp++;
        if (*sp == '.') {
                DPRINTF(2, (stderr, "FALSE (file starts with '.')\n"));
                return FALSE;
        }
        if ((sp = rindex(sp, '.')) != NULL) {
                sp++;
                for (i = 0; suflist[i] != NULL; i++)
                        if (strcmp(sp, suflist[i]) == 0) {
                                DPRINTF(2, (stderr, "FALSE (file has suffix \".%s\")\n", sp));
                                return FALSE;
                        }
        }

        datfile = copy(file, strlen(file) + 4); /* +4 for ".dat" */
        strcat(datfile, ".dat");
        if (access(datfile, R_OK) < 0) {
                free(datfile);
                DPRINTF(2, (stderr, "FALSE (no \".dat\" file)\n"));
                return FALSE;
        }
        if (datp != NULL)
                *datp = datfile;
        else
                free(datfile);
#ifdef OK_TO_WRITE_DISK
        if (posp != NULL) {
                *posp = copy(file, strlen(file) + 4); /* +4 for ".dat" */
                (void) strcat(*posp, ".pos");
        }
#else
        if (posp != NULL) {
                *posp = NULL;
        }
#endif /* OK_TO_WRITE_DISK */
        DPRINTF(2, (stderr, "TRUE\n"));
        return TRUE;
}

/*
 * copy:
 *      Return a malloc()'ed copy of the string
 */
static char *
copy(const char *str, size_t len)
{
        char *new, *sp;

        new = do_malloc(len + 1);
        sp = new;
        do {
                *sp++ = *str;
        } while (*str++);
        return new;
}

/*
 * do_malloc:
 *      Do a malloc, checking for NULL return.
 */
static void *
do_malloc(size_t size)
{
        void *new;

        if ((new = malloc(size)) == NULL)
                err(1, NULL);
        return new;
}

/*
 * do_free:
 *      Free malloc'ed space, if any.
 */
static void
do_free(void *ptr)
{
        if (ptr != NULL)
                free(ptr);
}

/*
 * init_prob:
 *      Initialize the fortune probabilities.
 */
static void
init_prob(void)
{
        FILEDESC *fp, *last;
        int percent, num_noprob, frac;

        last = NULL;
        /*
         * Distribute the residual probability (if any) across all
         * files with unspecified probability (i.e., probability of 0)
         * (if any).
         */

        percent = 0;
        num_noprob = 0;
        for (fp = File_tail; fp != NULL; fp = fp->prev)
                if (fp->percent == NO_PROB) {
                        num_noprob++;
                        if (Equal_probs)
                                last = fp;
                } else
                        percent += fp->percent;
        DPRINTF(1, (stderr, "summing probabilities:%d%% with %d NO_PROB's",
                    percent, num_noprob));
        if (percent > 100)
                errx(1, "Probabilities sum to %d%%!", percent);
        else if (percent < 100 && num_noprob == 0)
                errx(1, "No place to put residual probability (%d%%)",
                    100 - percent);
        else if (percent == 100 && num_noprob != 0)
                errx(1, "No probability left to put in residual files");
        percent = 100 - percent;
        if (Equal_probs) {
                if (num_noprob != 0) {
                        if (num_noprob > 1) {
                                frac = percent / num_noprob;
                                DPRINTF(1, (stderr, ", frac = %d%%", frac));
                                for (fp = File_tail; fp != last; fp = fp->prev)
                                        if (fp->percent == NO_PROB) {
                                                fp->percent = frac;
                                                percent -= frac;
                                        }
                        }
                        last->percent = percent;
                        DPRINTF(1, (stderr, ", residual = %d%%", percent));
                }
        } else {
                DPRINTF(1, (stderr,
                            ", %d%% distributed over remaining fortunes\n",
                            percent));
        }
        DPRINTF(1, (stderr, "\n"));

#ifdef DEBUG
        if (Debug >= 1)
                print_file_list();
#endif
}

/*
 * get_fort:
 *      Get the fortune data file's seek pointer for the next fortune.
 */
static void
get_fort(void)
{
        FILEDESC *fp;
        int choice;

        if (File_list->next == NULL || File_list->percent == NO_PROB)
                fp = File_list;
        else {
                choice = arc4random_uniform(100);
                DPRINTF(1, (stderr, "choice = %d\n", choice));
                for (fp = File_list; fp->percent != NO_PROB; fp = fp->next)
                        if (choice < fp->percent)
                                break;
                        else {
                                choice -= fp->percent;
                                DPRINTF(1, (stderr,
                                            "    skip \"%s\", %d%% (choice = %d)\n",
                                            fp->name, fp->percent, choice));
                        }
                        DPRINTF(1, (stderr,
                                    "using \"%s\", %d%% (choice = %d)\n",
                                    fp->name, fp->percent, choice));
        }
        if (fp->percent != NO_PROB)
                get_tbl(fp);
        else {
                if (fp->next != NULL) {
                        sum_noprobs(fp);
                        choice = arc4random_uniform(Noprob_tbl.str_numstr);
                        DPRINTF(1, (stderr, "choice = %d (of %d) \n", choice,
                                    Noprob_tbl.str_numstr));
                        while ((u_int32_t)choice >= fp->tbl.str_numstr) {
                                choice -= fp->tbl.str_numstr;
                                fp = fp->next;
                                DPRINTF(1, (stderr,
                                            "    skip \"%s\", %d (choice = %d)\n",
                                            fp->name, fp->tbl.str_numstr,
                                            choice));
                        }
                        DPRINTF(1, (stderr, "using \"%s\", %d\n", fp->name,
                                    fp->tbl.str_numstr));
                }
                get_tbl(fp);
        }
        if (fp->child != NULL) {
                DPRINTF(1, (stderr, "picking child\n"));
                fp = pick_child(fp);
        }
        Fortfile = fp;
        get_pos(fp);
        open_dat(fp);
        (void) lseek(fp->datfd,
                     (off_t) (sizeof fp->tbl + fp->pos * sizeof Seekpts[0]), SEEK_SET);
        read(fp->datfd, Seekpts, sizeof Seekpts);
        BE64TOH(Seekpts[0]);
        BE64TOH(Seekpts[1]);
}

/*
 * pick_child
 *      Pick a child from a chosen parent.
 */
static FILEDESC *
pick_child(FILEDESC *parent)
{
        FILEDESC *fp;
        int  choice;

        if (Equal_probs) {
                choice = arc4random_uniform(parent->num_children);
                DPRINTF(1, (stderr, "    choice = %d (of %d)\n",
                            choice, parent->num_children));
                for (fp = parent->child; choice--; fp = fp->next)
                        continue;
                DPRINTF(1, (stderr, "    using %s\n", fp->name));
                return fp;
        }
        else {
                get_tbl(parent);
                choice = arc4random_uniform(parent->tbl.str_numstr);
                DPRINTF(1, (stderr, "    choice = %d (of %d)\n",
                            choice, parent->tbl.str_numstr));
                for (fp = parent->child; (u_int32_t)choice >= fp->tbl.str_numstr;
                     fp = fp->next) {
                        choice -= fp->tbl.str_numstr;
                        DPRINTF(1, (stderr, "\tskip %s, %d (choice = %d)\n",
                                    fp->name, fp->tbl.str_numstr, choice));
                }
                DPRINTF(1, (stderr, "    using %s, %d\n", fp->name,
                            fp->tbl.str_numstr));
                return fp;
        }
}

/*
 * sum_noprobs:
 *      Sum up all the noprob probabilities, starting with fp.
 */
static void
sum_noprobs(FILEDESC *fp)
{
        static bool did_noprobs = FALSE;

        if (did_noprobs)
                return;
        zero_tbl(&Noprob_tbl);
        while (fp != NULL) {
                get_tbl(fp);
                sum_tbl(&Noprob_tbl, &fp->tbl);
                fp = fp->next;
        }
        did_noprobs = TRUE;
}

static int
max(int i, int j)
{
        return (i >= j ? i : j);
}

/*
 * open_fp:
 *      Assocatiate a FILE * with the given FILEDESC.
 */
static void
open_fp(FILEDESC *fp)
{
        if (fp->inf == NULL && (fp->inf = fdopen(fp->fd, "r")) == NULL)
                err(1, "Cannot open `%s'", fp->path);
}

/*
 * open_dat:
 *      Open up the dat file if we need to.
 */
static void
open_dat(FILEDESC *fp)
{
        if (fp->datfd < 0 && (fp->datfd = open(fp->datfile, O_RDONLY)) < 0)
                err(1, "Cannot open `%s'", fp->datfile);
}

/*
 * get_pos:
 *      Get the position from the pos file, if there is one.  If not,
 *      return a random number.
 */
static void
get_pos(FILEDESC *fp)
{
#ifdef OK_TO_WRITE_DISK
        int fd;
#endif /* OK_TO_WRITE_DISK */

        assert(fp->read_tbl);
        if (fp->pos == POS_UNKNOWN) {
#ifdef OK_TO_WRITE_DISK
                if ((fd = open(fp->posfile, O_RDONLY)) < 0 ||
                    read(fd, &fp->pos, sizeof fp->pos) != sizeof fp->pos)
                        fp->pos = arc4random_uniform(fp->tbl.str_numstr);
                else if (fp->pos >= fp->tbl.str_numstr)
                        fp->pos %= fp->tbl.str_numstr;
                if (fd >= 0)
                        (void) close(fd);
#else
                fp->pos = arc4random_uniform(fp->tbl.str_numstr);
#endif /* OK_TO_WRITE_DISK */
        }
        if ((u_int64_t)++(fp->pos) >= fp->tbl.str_numstr)
                fp->pos -= fp->tbl.str_numstr;
        DPRINTF(1, (stderr, "pos for %s is %lld\n", fp->name,
            (long long)fp->pos));
}

/*
 * get_tbl:
 *      Get the tbl data file the datfile.
 */
static void
get_tbl(FILEDESC *fp)
{
        int fd;
        FILEDESC *child;

        if (fp->read_tbl)
                return;
        if (fp->child == NULL) {
                if ((fd = open(fp->datfile, O_RDONLY)) < 0)
                        err(1, "Cannot open `%s'", fp->datfile);
                if (read(fd, (char *) &fp->tbl, sizeof fp->tbl) != sizeof fp->tbl) {
                        errx(1, "Database `%s' corrupted", fp->path);
                }
                /* BE32TOH(fp->tbl.str_version); */
                BE32TOH(fp->tbl.str_numstr);
                BE32TOH(fp->tbl.str_longlen);
                BE32TOH(fp->tbl.str_shortlen);
                BE32TOH(fp->tbl.str_flags);
                (void) close(fd);
        }
        else {
                zero_tbl(&fp->tbl);
                for (child = fp->child; child != NULL; child = child->next) {
                        get_tbl(child);
                        sum_tbl(&fp->tbl, &child->tbl);
                }
        }
        fp->read_tbl = TRUE;
}

/*
 * zero_tbl:
 *      Zero out the fields we care about in a tbl structure.
 */
static void
zero_tbl(STRFILE *tp)
{
        tp->str_numstr = 0;
        tp->str_longlen = 0;
        tp->str_shortlen = -1;
}

/*
 * sum_tbl:
 *      Merge the tbl data of t2 into t1.
 */
static void
sum_tbl(STRFILE *t1, STRFILE *t2)
{
        t1->str_numstr += t2->str_numstr;
        if (t1->str_longlen < t2->str_longlen)
                t1->str_longlen = t2->str_longlen;
        if (t1->str_shortlen > t2->str_shortlen)
                t1->str_shortlen = t2->str_shortlen;
}

#define STR(str)        ((str) == NULL ? "NULL" : (str))

/*
 * print_file_list:
 *      Print out the file list
 */
static void
print_file_list(void)
{
        print_list(File_list, 0);
}

/*
 * print_list:
 *      Print out the actual list, recursively.
 */
static void
print_list(FILEDESC *list, int lev)
{
        while (list != NULL) {
                fprintf(stderr, "%*s", lev * 4, "");
                if (list->percent == NO_PROB)
                        fprintf(stderr, "___%%");
                else
                        fprintf(stderr, "%3d%%", list->percent);
                fprintf(stderr, " %s", STR(list->name));
                DPRINTF(1, (stderr, " (%s, %s, %s)\n", STR(list->path),
                            STR(list->datfile), STR(list->posfile)));
                putc('\n', stderr);
                if (list->child != NULL)
                        print_list(list->child, lev + 1);
                list = list->next;
        }
}

#ifndef NO_REGEX

/*
 * re_setup:
 *      Initialize regular expression pattern.
 */
static void
re_setup(struct re *rx, const char *pattern, bool ignore_case)
{
        int code, flags;
        char errbuf[1024];

        assert(!rx->valid);

        flags = REG_EXTENDED | REG_NOSUB;
        if (ignore_case) {
                flags |= REG_ICASE;
        }
        code = regcomp(&rx->regex, pattern, flags);

        if (code != 0) {
                regerror(code, &rx->regex, errbuf, sizeof(errbuf));
                warnx("%s: `%s'", errbuf, pattern);
                regfree(&rx->regex);
                rx->valid = FALSE;
        }
        rx->valid = TRUE;
}

/*
 * re_cleanup:
 *      Undo re_setup.
 */
static void
re_cleanup(struct re *rx)
{
        if (rx->valid) {
                regfree(&rx->regex);
                rx->valid = FALSE;
        }
}

static bool
re_match(struct re *rx, const char *string)
{
        return regexec(&rx->regex, string, 0, NULL, 0) == 0;
}

/*
 * find_matches:
 *      Find all the fortunes which match the pattern we've been given.
 */
static int
find_matches(void)
{
        Fort_len = maxlen_in_list(File_list);
        DPRINTF(2, (stderr, "Maximum length is %d\n", Fort_len));
        /* extra length, "%\n" is appended */
        Fortbuf = do_malloc(Fort_len + 10);

        Found_one = FALSE;
        matches_in_list(File_list);
        return Found_one;
        /* NOTREACHED */
}

/*
 * maxlen_in_list
 *      Return the maximum fortune len in the file list.
 */
static size_t
maxlen_in_list(FILEDESC *list)
{
        FILEDESC *fp;
        size_t len, maxlen;

        maxlen = 0;
        for (fp = list; fp != NULL; fp = fp->next) {
                if (fp->child != NULL) {
                        if ((len = maxlen_in_list(fp->child)) > maxlen)
                                maxlen = len;
                }
                else {
                        get_tbl(fp);
                        if (fp->tbl.str_longlen > maxlen)
                                maxlen = fp->tbl.str_longlen;
                }
        }
        return maxlen;
}

/*
 * matches_in_list
 *      Print out the matches from the files in the list.
 */
static void
matches_in_list(FILEDESC *list)
{
        char *sp;
        FILEDESC *fp;
        int in_file;

        if (!Re_pat.valid || !Re_pat13.valid)
                return;

        for (fp = list; fp != NULL; fp = fp->next) {
                if (fp->child != NULL) {
                        matches_in_list(fp->child);
                        continue;
                }
                DPRINTF(1, (stderr, "searching in %s\n", fp->path));
                open_fp(fp);
                sp = Fortbuf;
                in_file = FALSE;
                while (fgets(sp, Fort_len, fp->inf) != NULL)
                        if (!STR_ENDSTRING(sp, fp->tbl))
                                sp += strlen(sp);
                        else {
                                *sp = '\0';
                                if (fp->tbl.str_flags & STR_ROTATED)
                                        Re_use = &Re_pat13;
                                else
                                        Re_use = &Re_pat;
                                if (re_match(Re_use, Fortbuf)) {
                                        printf("%c%c", fp->tbl.str_delim,
                                            fp->tbl.str_delim);
                                        if (!in_file) {
                                                printf(" (%s)", fp->name);
                                                Found_one = TRUE;
                                                in_file = TRUE;
                                        }
                                        putchar('\n');
                                        if (fp->tbl.str_flags & STR_ROTATED)
                                                rot13(Fortbuf, (sp - Fortbuf));
                                        (void) fwrite(Fortbuf, 1, (sp - Fortbuf), stdout);
                                }
                                sp = Fortbuf;
                        }
        }
        re_cleanup(&Re_pat);
        re_cleanup(&Re_pat13);
}
#endif /* NO_REGEX */

static void
usage(void)
{

        (void) fprintf(stderr, "Usage: %s [-ae", getprogname());
#ifdef DEBUG
        (void) fprintf(stderr, "D");
#endif /* DEBUG */
        (void) fprintf(stderr, "f");
#ifndef NO_REGEX
        (void) fprintf(stderr, "i");
#endif /* NO_REGEX */
        (void) fprintf(stderr, "losw]");
#ifndef NO_REGEX
        (void) fprintf(stderr, " [-m pattern]");
#endif /* NO_REGEX */
        (void) fprintf(stderr, "[ [#%%] file/directory/all]\n");
        exit(1);
}