/*-
 * Copyright (c) 2001-2005 Allan Saddi <allan@saddi.com>
 * All rights reserved.
 *
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY ALLAN SADDI AND HIS 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 ALLAN SADDI OR HIS 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.
 *
 * $Id: yafic.c 1824 2005-11-02 08:17:48Z asaddi $
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HAVE_DIRENT_H
#include <dirent.h>
#define NAMLEN(dirent) strlen((dirent)->d_name)
#else
#define dirent direct
#define NAMLEN(dirent) (dirent)->d_namlen
#ifdef HAVE_SYS_NDIR_H
#include <sys/ndir.h>
#endif
#ifdef HAVE_SYS_DIR_H
#include <sys/dir.h>
#endif
#ifdef HAVE_NDIR_H
#include <ndir.h>
#endif
#endif

#if HAVE_INTTYPES_H
# include <inttypes.h>
#else
# if HAVE_STDINT_H
#  include <stdint.h>
# endif
#endif

#include "yafic-db.h"

#include "common.h"

#include "yafic.h"
#include "statpack.h"
#include "sha1.h"

#ifdef YAFIC_CRYPTO
#include "crypto.h"
#endif

#include "version.h"

#ifndef lint
static const char rcsid[] =
	"$Id: yafic.c 1824 2005-11-02 08:17:48Z asaddi $";
#endif /* !lint */

int Verbosity;
int DisplayHashes;
int SignVerifyFiles;

static int SimpleOutput, KeyIsPublic;
static DB *CheckDB, *UpdateDB, *CompareDB;

void
DisplayFileHash (int fd, const char *filename)
{
  SHA1Context sha;
  ssize_t len;

  uint8_t hash[SHA1_HASH_SIZE];

  SHA1Init (&sha);

  while ((len = read (fd, HashBuffer, HASH_BUFFER_SIZE)) > 0)
    SHA1Update (&sha, HashBuffer, len);

  if (len < 0)
    yaficError (filename);

  SHA1Final (&sha, hash);

  fprintf (stderr, "%s %s\n", ToHexStr (hash, sizeof (hash)), filename);
}

static void
displayDBHash (DB *db, const char *dbName)
{
  int fd;
  off_t oldpos;

  if ((fd = db->fd (db)) != -1) {
    if ((oldpos = lseek (fd, 0, SEEK_CUR)) != -1) {
      if (lseek (fd, 0, SEEK_SET) != -1) {
	DisplayFileHash (fd, dbName);
	if (lseek (fd, oldpos, SEEK_SET) != -1)
	  return;
      }
    }
  }
  yaficError (dbName);
}

#ifdef YAFIC_CRYPTO
static void
verifyDB (DB *db, const char *dbName)
{
  int fd;
  off_t oldpos;

  if ((fd = db->fd (db)) != -1) {
    if ((oldpos = lseek (fd, 0, SEEK_CUR)) != -1) {
      if (lseek (fd, 0, SEEK_SET) != -1) {
	VerifyFile (fd, dbName, NULL);
	if (lseek (fd, oldpos, SEEK_SET) != -1)
	  return;
      }
    }
  }
  yaficError (dbName);
}
#endif

static int
hashFile (const char *path, struct stat *sb, uint8_t hash[SHA1_HASH_SIZE])
{
  SHA1Context sha;
  int f;
  ssize_t len;
  int error = 1;

  SHA1Init (&sha);

  if (S_ISLNK (sb->st_mode)) {
    if ((len = readlink (path, HashBuffer, HASH_BUFFER_SIZE)) != -1) {
      SHA1Update (&sha, HashBuffer, len);
      error = 0;
    }
  }
  else if (S_ISREG (sb->st_mode) || S_ISDIR (sb->st_mode)) {
    if ((f = open (path, O_RDONLY)) != -1) {
      while ((len = read (f, HashBuffer, HASH_BUFFER_SIZE)) > 0)
	SHA1Update (&sha, HashBuffer, len);
      close (f);
      error = len < 0;
    }
  }

  if (error)
    return 0;

  /* Compute final hash. */
  SHA1Final (&sha, hash);
  return 1;
}

const char *
ToHexStr (const uint8_t *data, int len)
{
  static const char hex[] = "0123456789abcdef";
  static char buf[128], *bufp;

  bufp = buf;
  while (len) {
    *(bufp++) = hex[*data >> 4];
    *(bufp++) = hex[*data & 0x0f];
    data++;
    len--;
  }
  *bufp = '\0';

  return buf;
}

static void
checkDeleted (const char *root,
	      DB *checkDB, DB *updateDB,
	      const char *checkDBName, const char *updateDBName)
{
  DBT cKey, cData, uKey, uData;
  int res;
  int flags;
  char *entry;
  uint8_t *bufp;
  struct stat oldsb;

  struct RuleEntry *re;
  rflag_t rflags;

  char fullpath[PATH_MAX];

  memset (&cKey, 0, sizeof (cKey));
  memset (&cData, 0, sizeof (cData));
  memset (&uKey, 0, sizeof (uKey));
  memset (&uData, 0, sizeof (uData));

  /* Iterate over each entry in the known database. */
  flags = R_FIRST;
  while (!checkDB->seq (checkDB, &cKey, &cData, flags)) {
    if (cData.size != DB_RECORD_SIZE) {
      fprintf (stderr, "%s: %s: %s\n", prog, checkDBName,
	       "bad database format");
      exit (1);
    }

    uKey.data = cKey.data;
    uKey.size = cKey.size;

    /* See if the entry still exists. */
    res = updateDB->get (updateDB, &uKey, &uData, 0);
    if (res == 1) { /* No longer exists. */
      entry = mymalloc (cKey.size + 1);
      strncpy (entry, cKey.data, cKey.size);
      entry[cKey.size] = '\0';

      if ((re = FindRuleEntry (entry)))
	rflags = re->entryFlags;
      else {
	re = FindClosestRuleEntry (entry);
	rflags = re->descFlags;
      }

      /* Skip if ignoring. */
      if (rflags & RFLAG_IGNORE) {
	free (entry);
	flags = R_NEXT;
	continue;
      }

      snprintf (fullpath, sizeof (fullpath), "%s%s", root, entry);

      free (entry);

      /* Unpack the record. */
      UnpackStat (cData.data, &oldsb);
      bufp = &((uint8_t *) cData.data)[DB_RECORD_SIZE - SHA1_HASH_SIZE];

      switch (SimpleOutput) {
      case 0:
	printf ("removed: %s"
		" p(" ST_MODE_FMT ")"
		" i(" ST_INO_FMT ")"
		" n(" ST_NLINK_FMT ")"
		" u(" ST_UID_FMT ")"
		" g(" ST_GID_FMT ")"
		" s(" ST_SIZE_FMT ")"
		" a(" ST_ATIME_FMT ")"
		" m(" ST_MTIME_FMT ")"
		" c(" ST_CTIME_FMT ")"
		" h(%s)\n",
		fullpath,
		ST_MODE_CAST oldsb.st_mode,
		ST_INO_CAST oldsb.st_ino,
		ST_NLINK_CAST oldsb.st_nlink,
		ST_UID_CAST oldsb.st_uid,
		ST_GID_CAST oldsb.st_gid,
		ST_SIZE_CAST oldsb.st_size,
		ST_ATIME_CAST oldsb.st_atime,
		ST_MTIME_CAST oldsb.st_mtime,
		ST_CTIME_CAST oldsb.st_ctime,
		ToHexStr (bufp, SHA1_HASH_SIZE));
	break;
      case 1:
	printf ("%s - REMOVED\n", fullpath);
	break;
      default:
	printf ("R %s\n", fullpath);
	break;
      }
    }
    else if (res)
      yaficError (updateDBName);

    flags = R_NEXT;
  }
}

static void
checkFile (DB *db, const char *dbName, const char *fullpath,
	   const char *name, struct stat *sb, rflag_t flags,
	   const uint8_t hash[SHA1_HASH_SIZE])
{
  DBT key, data;
  int res;
  uint8_t *bufp;
  struct stat oldsb;
  int changed;
  int gothash;

  static const uint8_t zerohash[SHA1_HASH_SIZE] = {
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
  };

  char buf[2 * PATH_MAX], buf2[256];

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

  if (Verbosity >= 4)
    printf ("Checking %s...\n", fullpath);

  key.data = (char *) name;
  key.size = strlen (name);
  if (!(res = db->get (db, &key, &data, 0))) {
    if (data.size != DB_RECORD_SIZE) {
      fprintf (stderr, "%s: %s: %s\n", prog, dbName,
	       "bad database format");
      exit (1);
    }

    /* First unpack the record. */
    UnpackStat (data.data, &oldsb);
    bufp = &((uint8_t *) data.data)[DB_RECORD_SIZE - SHA1_HASH_SIZE];

    changed = 0;
    snprintf (buf, sizeof (buf), "changed: %s", fullpath);
    if ((flags & RFLAG_PERM) && sb->st_mode != oldsb.st_mode) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " p(" ST_MODE_FMT ":" ST_MODE_FMT ")",
		ST_MODE_CAST oldsb.st_mode, ST_MODE_CAST sb->st_mode);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_INODE) && sb->st_ino != oldsb.st_ino) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " i(" ST_INO_FMT ":" ST_INO_FMT ")",
		ST_INO_CAST oldsb.st_ino, ST_INO_CAST sb->st_ino);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_NLINK) && sb->st_nlink != oldsb.st_nlink) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " n(" ST_NLINK_FMT ":" ST_NLINK_FMT ")",
		ST_NLINK_CAST oldsb.st_nlink, ST_NLINK_CAST sb->st_nlink);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_UID) && sb->st_uid != oldsb.st_uid) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " u(" ST_UID_FMT ":" ST_UID_FMT ")",
		ST_UID_CAST oldsb.st_uid, ST_UID_CAST sb->st_uid);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_GID) && sb->st_gid != oldsb.st_gid) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " g(" ST_GID_FMT ":" ST_GID_FMT ")",
		ST_GID_CAST oldsb.st_gid, ST_GID_CAST sb->st_gid);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_SIZE) && sb->st_size != oldsb.st_size) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " s(" ST_SIZE_FMT ":" ST_SIZE_FMT ")",
		ST_SIZE_CAST oldsb.st_size, ST_SIZE_CAST sb->st_size);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_ATIME) && sb->st_atime != oldsb.st_atime) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " a(" ST_ATIME_FMT ":" ST_ATIME_FMT ")",
		ST_ATIME_CAST oldsb.st_atime, ST_ATIME_CAST sb->st_atime);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_MTIME) && sb->st_mtime != oldsb.st_mtime) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " m(" ST_MTIME_FMT ":" ST_MTIME_FMT ")",
		ST_MTIME_CAST oldsb.st_mtime, ST_MTIME_CAST sb->st_mtime);
      strlcat (buf, buf2, sizeof (buf));
    }
    if ((flags & RFLAG_CTIME) && sb->st_ctime != oldsb.st_ctime) {
      changed = 1;
      snprintf (buf2, sizeof (buf2), " c(" ST_CTIME_FMT ":" ST_CTIME_FMT ")",
		ST_CTIME_CAST oldsb.st_ctime, ST_CTIME_CAST sb->st_ctime);
      strlcat (buf, buf2, sizeof (buf));
    }
    if (flags & RFLAG_HASH) {
      if (!hash)
	hash = zerohash;

      if (memcmp (hash, bufp, SHA1_HASH_SIZE)) {
	changed = 1;
	snprintf (buf2, sizeof (buf2), " h(%s:",
		  ToHexStr (bufp, SHA1_HASH_SIZE));
	strlcat (buf, buf2, sizeof (buf));
	snprintf (buf2, sizeof (buf2), "%s)",
		  ToHexStr (hash, SHA1_HASH_SIZE));
	strlcat (buf, buf2, sizeof (buf));
      }
    }

    if (changed) {
      switch (SimpleOutput) {
      case 0:
	printf ("%s\n", buf);
	break;
      case 1:
	printf ("%s - CHANGED\n", fullpath);
	break;
      default:
	printf ("M %s\n", fullpath);
	break;
      }
    }
  }
  else if (res == 1) {
    gothash = (flags & RFLAG_HASH) && hash;

    switch (SimpleOutput) {
    case 0:
      printf ("added: %s"
	      " p(" ST_MODE_FMT ")"
	      " i(" ST_INO_FMT ")"
	      " n(" ST_NLINK_FMT ")"
	      " u(" ST_UID_FMT ")"
	      " g(" ST_GID_FMT ")"
	      " s(" ST_SIZE_FMT ")"
	      " a(" ST_ATIME_FMT ")"
	      " m(" ST_MTIME_FMT ")"
	      " c(" ST_CTIME_FMT ")"
	      " h(%s)\n",
	      fullpath,
	      ST_MODE_CAST sb->st_mode,
	      ST_INO_CAST sb->st_ino,
	      ST_NLINK_CAST sb->st_nlink,
	      ST_UID_CAST sb->st_uid,
	      ST_GID_CAST sb->st_gid,
	      ST_SIZE_CAST sb->st_size,
	      ST_ATIME_CAST sb->st_atime,
	      ST_MTIME_CAST sb->st_mtime,
	      ST_CTIME_CAST sb->st_ctime,
	      gothash ? ToHexStr (hash, SHA1_HASH_SIZE) : "-");
      break;
    case 1:
      printf ("%s - ADDED\n", fullpath);
      break;
    default:
      printf ("A %s\n", fullpath);
      break;
    }
  }
  else
    yaficError (dbName);
}

static rflag_t
applyMasks (rflag_t flags, rflag_t *masks, struct stat *sb)
{
  int mtype;

  mtype = RMASK_SPECIAL;
  if (S_ISDIR (sb->st_mode))
    mtype = RMASK_DIR;
  else if (S_ISREG (sb->st_mode))
    mtype = RMASK_FILE;
  else if (S_ISLNK (sb->st_mode))
    mtype = RMASK_LINK;

  return flags & masks[mtype];
}

static void
updateFile (DB *db, const char *dbName, const char *fullpath,
	    const char *name, struct stat *sb, rflag_t flags)
{
  DBT key, data;
  uint8_t buf[128], *bufp;

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

  if (Verbosity >= 4)
    printf ("Updating %s...\n", fullpath);

  PackStat (sb, buf);

  bufp = &buf[DB_RECORD_SIZE - SHA1_HASH_SIZE];
  if (!(flags & RFLAG_HASH) || !hashFile (fullpath, sb, bufp))
    memset (bufp, 0, SHA1_HASH_SIZE);
  bufp += SHA1_HASH_SIZE;

  key.data = (char *) name;
  key.size = strlen (name);
  data.data = buf;
  data.size = DB_RECORD_SIZE;

  if (db->put (db, &key, &data, 0))
    yaficError (dbName);
}

static DB *scanQueue;

static void
dumpScanQueue (void)
{
  DBT key, data;
  int flags;
  int res;
  char *entry;

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

  printf ("Scan queue:");
  flags = R_FIRST;
  while (!(res = scanQueue->seq (scanQueue, &key, &data, flags))) {
    entry = mymalloc (key.size + 1);
    strncpy (entry, key.data, key.size);
    entry[key.size] = '\0';

    printf (" %s", entry);

    free (entry);
    flags = R_NEXT;
  }
  printf ("\n");

  if (res == -1)
    yaficError ("db->seq()");
}

static void
scanFilesHelper (struct RuleEntry *re)
{
  DBT key, data;

  if (re->entryFlags & RFLAG_IGNORE)
    return;

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

  key.data = re->path;
  key.size = strlen (re->path);
  data.data = "";
  data.size = 0;

  if (scanQueue->put (scanQueue, &key, &data, 0) == -1)
    yaficError ("db->put()");
}

static void
scanFiles (const char *root,
	   const char *checkDBName, const char *updateDBName,
	   int doCheck, int doUpdate)
{
  DBT key, data;
  char *entry;
  int res;

  struct RuleEntry *re;
  rflag_t flags, *masks;
  struct stat sb;
  int gothash;
  uint8_t hash[SHA1_HASH_SIZE];

  int i;
  dev_t parentdev;
  DIR *dirp;
  struct dirent *dp;

  char fullpath[PATH_MAX];
  char descentry[PATH_MAX];
  char descpath[PATH_MAX];

  int dbFD = -1, origFD = -1;

  unsigned int warningcount = 0;

  if (!doCheck && !doUpdate)
    return;

  if (doCheck &&
      !(CheckDB = dbopen (checkDBName, O_RDONLY, 0, DB_HASH, NULL)))
    yaficError (checkDBName);

  if (CheckDB && DisplayHashes > 1)
    displayDBHash (CheckDB, checkDBName);

#ifdef YAFIC_CRYPTO
  if (CheckDB && SignVerifyFiles)
    verifyDB (CheckDB, checkDBName);
#endif

  if (doUpdate &&
      !(UpdateDB = dbopen (updateDBName, O_CREAT|O_RDWR|O_TRUNC, 0600,
			   DB_HASH, NULL)))
    yaficError (updateDBName);

  /* Create a btree database for use as our priority queue. */
  if (!(scanQueue = dbopen (NULL, O_CREAT|O_RDWR|O_TRUNC, 0600, DB_BTREE,
			    NULL)))
    yaficError ("dbopen()");

  ApplyRuleSet (scanFilesHelper);

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

  /* The actual scanning loop. */
  for (;;) {
    if (Verbosity >= 3)
      dumpScanQueue ();

    if ((res = scanQueue->seq (scanQueue, &key, &data, R_FIRST)))
      break;
    entry = mymalloc (key.size + 1);
    strncpy (entry, key.data, key.size);
    entry[key.size] = '\0';
    if (scanQueue->del (scanQueue, &key, 0) == -1)
      yaficError ("db->del()");

    /* Process the entry itself. */
    if ((re = FindRuleEntry (entry)))
      flags = re->entryFlags;
    else {
      re = FindClosestRuleEntry (entry);
      flags = re->descFlags;
    }
    masks = re->masks;
    if (!(flags & RFLAG_IGNORE)) {
      snprintf (fullpath, sizeof (fullpath), "%s%s", root, entry);
      if (!lstat (fullpath, &sb)) {
	if (doCheck) {
	  flags = applyMasks (flags, masks, &sb);
	  gothash = (flags & RFLAG_HASH) && hashFile (fullpath, &sb, hash);
	  checkFile (CheckDB, checkDBName, fullpath, entry, &sb, flags,
		     gothash ? hash : NULL);
	}
	if (doUpdate)
	  updateFile (UpdateDB, updateDBName, fullpath, entry, &sb, flags);
      }
      else {
	yaficWarning (fullpath);
	free (entry);
	warningcount++;
	continue;
      }
    }
    else {
      free (entry);
      continue;
    }

    if (S_ISDIR (sb.st_mode)) {
      /* Now process its contents. */
      parentdev = sb.st_dev;
      re = FindClosestRuleEntry (entry);
      if (!(re->descFlags & RFLAG_IGNORE)) {
	if ((dirp = opendir (fullpath))) {
	  /* Kludge to keep paths clean. */
	  i = strlen (fullpath) - 1;
	  if (i >= 0 && fullpath[i] == '/')
	    fullpath[i] = '\0';
	  if (!strcmp (entry, "/"))
	    entry[0] = '\0';

	  while ((dp = readdir (dirp))) {
	    /* Skip "." and ".." */
	    if (!strcmp (dp->d_name, ".") || !strcmp (dp->d_name, ".."))
	      continue;

	    snprintf (descpath, sizeof (descpath), "%s/%s", fullpath,
		      dp->d_name);

	    if (!lstat (descpath, &sb)) {
	      snprintf (descentry, sizeof (descentry), "%s/%s", entry,
			dp->d_name);

	      if (!S_ISDIR (sb.st_mode)) {
		/* Optimize by copying above, skipping push/pop. */
		if ((re = FindRuleEntry (descentry)))
		  flags = re->entryFlags;
		else {
		  re = FindClosestRuleEntry (descentry);
		  flags = re->descFlags;
		}
		masks = re->masks;
		if (!(flags & RFLAG_IGNORE)) {
		  if (doCheck) {
		    flags = applyMasks (flags, masks, &sb);
		    gothash = (flags & RFLAG_HASH) && hashFile (descpath,
								&sb, hash);
		    checkFile (CheckDB, checkDBName, descpath, descentry,
			       &sb, flags, gothash ? hash : NULL);
		  }
		  if (doUpdate)
		    updateFile (UpdateDB, updateDBName, descpath, descentry,
				&sb, flags);
		}

		/* Remove this file from the scan queue, if present. */
		key.data = descentry;
		key.size = strlen (descentry);
		if (scanQueue->del (scanQueue, &key, 0) == -1)
		  yaficError ("db->del()");
	      }
	      else {
		if (sb.st_dev == parentdev) {
		  key.data = descentry;
		  key.size = strlen (descentry);
		  data.data = "";
		  data.size = 0;
		  if (scanQueue->put (scanQueue, &key, &data, R_NOOVERWRITE)
		      == -1)
		    yaficError ("db->put()");
		}
	      }
	    }
	    else {
	      yaficWarning (descpath);
	      warningcount++;
	    }
	  }

	  closedir (dirp);
	}
      }
    }

    free (entry);
  }

  scanQueue->close (scanQueue);
  scanQueue = NULL;

  if (warningcount) {
    fprintf (stderr,
	     "%s: WARNING: at least one file was skipped due to errors\n",
	     prog);
  }

  /* Check for deleted files, if applicable. */
  if (doCheck && doUpdate)
    checkDeleted (root, CheckDB, UpdateDB, checkDBName, updateDBName);
  fflush (stdout);

  if (UpdateDB) {
    if (DisplayHashes || SignVerifyFiles) {
      if ((dbFD = UpdateDB->fd (UpdateDB)) == -1)
	yaficError (updateDBName);
      if ((origFD = dup (dbFD)) == -1)
	yaficError ("dup()");
    }

    res = UpdateDB->close (UpdateDB);
    UpdateDB = NULL;
    if (res)
      yaficError (updateDBName);

    if (DisplayHashes || SignVerifyFiles) {
      if (DisplayHashes) {
	if (lseek (origFD, 0, SEEK_SET) == -1)
	  yaficError (updateDBName);
	DisplayFileHash (origFD, updateDBName);
      }
#ifdef YAFIC_CRYPTO
      if (SignVerifyFiles && !KeyIsPublic) {
	if (lseek (origFD, 0, SEEK_SET) == -1)
	  yaficError (updateDBName);
	SignFile (origFD, updateDBName, NULL);
      }
#endif
      close (origFD);
    }
  }

  if (CheckDB) {
    CheckDB->close (CheckDB);
    CheckDB = NULL;
  }
}

static void
compareDBs (const char *root,
	    const char *checkDBName, const char *compareDBName)
{
  DBT key, data;
  int seqflags;
  int res;

  char *entry;
  struct stat sb;
  uint8_t *hash;

  struct RuleEntry *re;
  rflag_t flags, *masks;

  char fullpath[PATH_MAX];

  if (!(CheckDB = dbopen (checkDBName, O_RDONLY, 0, DB_HASH, NULL)))
    yaficError (checkDBName);

  if (!(CompareDB = dbopen (compareDBName, O_RDONLY, 0, DB_HASH, NULL)))
    yaficError (compareDBName);

  if (DisplayHashes) {
    displayDBHash (CheckDB, checkDBName);
    displayDBHash (CompareDB, compareDBName);
  }

#ifdef YAFIC_CRYPTO
  if (SignVerifyFiles) {
    verifyDB (CheckDB, checkDBName);
    verifyDB (CompareDB, compareDBName);
  }
#endif

  memset (&key, 0, sizeof (key));
  memset (&data, 0, sizeof (data));
  memset (&sb, 0, sizeof (sb));

  seqflags = R_FIRST;
  while (!(res = CompareDB->seq (CompareDB, &key, &data, seqflags))) {
    if (data.size != DB_RECORD_SIZE) {
      fprintf (stderr, "%s: %s: %s\n", prog, compareDBName,
	       "bad database format");
      exit (1);
    }

    entry = mymalloc (key.size + 1);
    strncpy (entry, key.data, key.size);
    entry[key.size] = '\0';

    UnpackStat (data.data, &sb);
    hash = &((uint8_t *) data.data)[DB_RECORD_SIZE - SHA1_HASH_SIZE];

    if ((re = FindRuleEntry (entry)))
      flags = re->entryFlags;
    else {
      re = FindClosestRuleEntry (entry);
      flags = re->descFlags;
    }
    masks = re->masks;

    if (!(flags & RFLAG_IGNORE)) {
      snprintf (fullpath, sizeof (fullpath), "%s%s", root, entry);
      flags = applyMasks (flags, masks, &sb);
      checkFile (CheckDB, checkDBName, fullpath, entry, &sb, flags,
		 flags & RFLAG_HASH ? hash : NULL);
    }

    free (entry);
    seqflags = R_NEXT;
  }

  /* Display any deleted files. */
  checkDeleted (root, CheckDB, CompareDB, checkDBName, compareDBName);

  CompareDB->close (CompareDB);
  CompareDB = NULL;

  CheckDB->close (CheckDB);
  CheckDB = NULL;
}

static void
cleanUp (void)
{
  if (scanQueue)
    scanQueue->close (scanQueue);
  if (CompareDB)
    CompareDB->close (CompareDB);
  if (UpdateDB)
    UpdateDB->close (UpdateDB);
  if (CheckDB)
    CheckDB->close (CheckDB);
  CleanRuleSet ();

#if YAFIC_CRYPTO
  CleanCrypto ();
#endif

  free (HashBuffer);
}

static char *
filler (const char *template)
{
  int len;
  static char spaces[128];

  len = strlen (template);
  if (len > (sizeof (spaces) - 1))
    len = sizeof (spaces) - 1;
  memset (spaces, ' ', len);
  spaces[len] = '\0';

  return spaces;
}

static void
help (void)
{
  fprintf (stderr,
	   "\nOptions:\n"
	   "\t-H\t\tDisplay SHA-1 hash of updated database.\n"
	   "\t-V\t\tDisplay version information.\n"
	   "\t-h\t\tDisplay this summary.\n"
#ifdef YAFIC_CRYPTO
	   "\t-p\t\tExpect <keyfile> to be a public key instead of a\n"
	   "\t\t\tprivate key.\n"
#endif
	   "\t-v\t\tIncrease verbosity.\n"
	   "\t-s\t\tShow simpler output.\n"
	   "\t-C <config>\tSpecify configuration file. (Default: %s)\n"
#ifdef YAFIC_CRYPTO
	   "\t-k <keyfile>\tSpecify key for signing/verification operations.\n"
#endif
	   "\t-r <root>\tSpecify root. (Default: %s)\n"
	   "\t-c <database>\tCompare with known database. Specify - to use\n"
	   "\t\t\tdefault database. (Default: %s)\n"
	   "\t-u <database>\tPerform update. Specify - to use default\n"
	   "\t\t\tdatabase. (Default: %s)\n"
	   "\t-d <database>\tCompare known database with existing database.\n"
	   "\t-l <database>\tList entries in database.\n"
	   "\t-t <type>\tList only files of a specific type. <type> may be\n"
	   "\t\t\td, f, l, or s or any combination thereof.\n"
	   "\t\t\t(Directory, file, symlink, special, respectively.)\n"
	   "\n"
	   "  The -H option may be specified twice, which will additionally\n"
	   "  display the SHA-1 hashes of the configuration file and known\n"
	   "  database.\n"
	   "\n"
	   "  The -v option may be specified multiple times. More than twice\n"
	   "  will only slow things down! If used with -l, the actual\n"
	   "  file attributes will also be displayed.\n"
	   "\n"
	   "  The -s option may be specified twice, giving simpler output.\n"
	   "\n"
	   "  To detect removed files, the -c and -u options must be used\n"
	   "  together.\n"
	   , DEFAULT_CONF, /* DEFAULT_ROOT */ "/",
	   DEFAULT_CHECK_DB, DEFAULT_UPDATE_DB);
}

static void
usage (void)
{
#ifdef YAFIC_CRYPTO
#define CRYPTO_HELP "[-k <keyfile>] "
#else
#define CRYPTO_HELP
#endif

  fprintf (stderr,
	   "Usage: %s [-HVh%svs] [-C <config>] " CRYPTO_HELP "[-r <root>]\n"
	   "       %s [-c <known-database>] [-u <new-database> | -d <exist-database>]\n"
	   "       %s [-Vhv] [-r <root>] [-t <type>] -l <database>\n",
	   prog,
#ifdef YAFIC_CRYPTO
	   "p",
#else
	   "",
#endif
	   filler (prog), prog);
}

int
main (int argc, char *argv[])
{
  int ch;
  int doCheck, doUpdate, doCompare, doView;
  char *conf, *root, *checkDBName, *updateDBName, *compareDBName, *viewDBName;
  char *typeStr;
#ifdef YAFIC_CRYPTO
  char *keyName = NULL;
#endif
  int i;

  doCheck = doUpdate = doCompare = doView = 0;
  conf = DEFAULT_CONF;
  root = DEFAULT_ROOT;
  checkDBName = DEFAULT_CHECK_DB;
  updateDBName = DEFAULT_UPDATE_DB;
  compareDBName = NULL;
  viewDBName = NULL;
  typeStr = NULL;

  prog = argv[0];

#ifdef YAFIC_CRYPTO
#define CRYPTO_OPTS "pk:"
#else
#define CRYPTO_OPTS
#endif

  while ((ch = getopt (argc, argv, "C:HVc:d:hl:r:st:u:v" CRYPTO_OPTS)) != -1) {
    switch (ch) {
    case 'C':
      conf = optarg;
      break;
    case 'H':
      DisplayHashes++;
      break;
    case 'V':
      fprintf (stderr, "yafic version " VERSION_STRING
	       " (" VERSION_DATE ")\n");
      exit (1);
    case 'c':
      doCheck = 1;
      if (strcmp (optarg, "-"))
	checkDBName = optarg;
      break;
    case 'd':
      doCompare = 1;
      compareDBName = optarg;
      break;
    case 'h':
      usage ();
      help ();
      exit (2);
    case 'l':
      doView = 1;
      viewDBName = optarg;
      break;
    case 'r':
      if (!strcmp (optarg, "/"))
	root = "";
      else {
	root = optarg;
	/* Wipe out trailing slash, if any. */
	i = strlen (root) - 1;
	if (i > 1 && root[i] == '/')
	  root[i] = '\0';
      }
      break;
    case 's':
      SimpleOutput++;
      break;
    case 't':
      typeStr = optarg;
      break;
    case 'u':
      doUpdate = 1;
      if (strcmp (optarg, "-"))
	updateDBName = optarg;
      break;
#ifdef YAFIC_CRYPTO
    case 'p':
      KeyIsPublic = 1;
      break;
    case 'k':
      keyName = optarg;
      break;
#endif
    case 'v':
      Verbosity++;
      break;
    case '?':
    default:
      usage ();
      exit (2);
    }
  }
  argc -= optind;
  argv += optind;

  if (argc || (doView && (doCheck || doUpdate))) {
    usage ();
    exit (2);
  }

#ifdef SETVBUF_REVERSED
  setvbuf (stdout, _IOLBF, NULL, 0);
  setvbuf (stderr, _IOLBF, NULL, 0);
#else
  setvbuf (stdout, NULL, _IOLBF, 0);
  setvbuf (stderr, NULL, _IOLBF, 0);
#endif

  if (doView) {
    ViewDB (root, viewDBName, typeStr);
    exit (0);
  }

  if (doCompare) {
    if (!doCheck) {
      fprintf (stderr, "you must use -c with the -d option\n");
      exit (2);
    }
    if (doUpdate) {
      fprintf (stderr, "the -u and -d options are mutually exclusive\n");
      exit (2);
    }
  }

  HashBuffer = mymalloc (HASH_BUFFER_SIZE);

  atexit (cleanUp);

#ifdef YAFIC_CRYPTO
  InitCrypto ();
  if (keyName) {
    LoadKey (keyName, KeyIsPublic);
    SignVerifyFiles++;
  }
#endif

  if (Verbosity) {
    printf ("        config: %s\n", conf);
#ifdef YAFIC_CRYPTO
    if (SignVerifyFiles)
      printf ("   %s key: %s (%s)\n", KeyIsPublic ? " public" : "private",
	      keyName, KeyTypeStr ());
    else
      printf ("   %s key: none\n", KeyIsPublic ? " public" : "private");
#endif
    printf ("          root: %s\n", !strlen (root) ? "/" : root);
    printf ("known-database: %s\n", checkDBName);
    if (doCompare) {
      printf ("exist-database: %s\n", compareDBName);
      printf ("     operation: %s\n", "compare");
    }
    else {
      printf ("  new-database: %s\n", updateDBName);
      printf ("     operation: %s\n",
	      doCheck && doUpdate ? "check and update" :
	      (doCheck ? "check" :
	       (doUpdate ? "update" : "none")));
    }
    fflush (stdout);
  }

  InitRuleSet ();

  if (!ParseRuleSet (conf))
    exit (1);

  if (Verbosity >= 2) {
    printf ("\nParsed rule set:\n");
    ApplyRuleSet (DumpRuleEntry);

    if (Verbosity >= 3)
      printf ("\n");
  }

  if (doCompare) {
    compareDBs (root, checkDBName, compareDBName);
    exit (0);
  }

  /* Do the actual check/update. */
  scanFiles (root, checkDBName, updateDBName, doCheck, doUpdate);

  /* If neither checking nor updating and not verbose, say something to
     reassure the user. */
  if (!doCheck && !doUpdate && !Verbosity)
    printf ("nothing done; use -c, -u, -d, or -l options\n");

  exit (0);
}
