/*-
 * Copyright (c) 2001, 2003 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: ruleset.c 908 2003-12-06 01:01:16Z asaddi $
 */

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

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

#include "common.h"

#include "yafic.h"

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

#ifndef lint
static const char rcsid[] =
	"$Id: ruleset.c 908 2003-12-06 01:01:16Z asaddi $";
#endif /* !lint */

#define LINE_BUFFER_SIZE 4096

static const char flagChars[] = "pinugsamch";
static const struct {
  char c;
  rflag_t flags;
} flagTemplate[] = {
  { 'R', RFLAG_DEFAULT },
  { 'L', RFLAG_DEFAULT & ~(RFLAG_MTIME | RFLAG_CTIME | RFLAG_HASH) },
  { 'N', RFLAG_DEFAULT | RFLAG_SIZE | RFLAG_ATIME },
  { 'E', 0 }
};
#define TEMPLATE_COUNT (sizeof (flagTemplate) / sizeof (flagTemplate[0]))

static struct RuleEntry **ruleSet;
static rflag_t currentMasks[RMASK_MAX];

static uint32_t
myhash (const char *str)
{
  const uint8_t *s = str;
  uint32_t h = 0, g;

  while (*s) {
    h = (h << 4) + *(s++);
    if ((g = (h & 0xf0000000)))
      h ^= g >> 24;
    h &= ~g;
  }

  return h;
}

void
ApplyRuleSet (void (*func) (struct RuleEntry *re))
{
  int i;
  struct RuleEntry *re;

  for (i = RULESET_TABLE_SIZE - 1; i >= 0; i--) {
    re = ruleSet[i];
    while (re) {
      func (re);
      re = re->next;
    }
  }
}

static void
destroyRuleEntry (struct RuleEntry *re)
{
  free (re->path);
  free (re);
}

struct RuleEntry *
FindRuleEntry (const char *path)
{
  struct RuleEntry *re;
  uint32_t hash;

  hash = myhash (path) % RULESET_TABLE_SIZE;
  re = ruleSet[hash];
  while (re) {
    if (!strcmp (re->path, path))
      return re;
    re = re->next;
  }

  return NULL;
}

struct RuleEntry *
FindClosestRuleEntry (const char *path)
{
  char *newpath;
  struct RuleEntry *re;
  char *tmp;

  newpath = mystrdup (path);
  /* There better be a root entry or else we're going to loop forever! */
  for (;;) {
    if ((re = FindRuleEntry (newpath))) {
      free (newpath);
      return re;
    }
    tmp = mydirname (newpath);
    free (newpath);
    newpath = tmp;
  }
  /* NOTREACHED */
}

static struct RuleEntry *
addRuleEntry (const char *path, rflag_t entryFlags, rflag_t descFlags,
	      rflag_t masks[RMASK_MAX])
{
  struct RuleEntry *re;
  int i;
  uint32_t hash;

  re = mymalloc (sizeof (*re));
  re->path = mystrdup (path);
  re->entryFlags = entryFlags;
  re->descFlags = descFlags;
  for (i = 0; i < RMASK_MAX; i++)
    re->masks[i] = masks[i];

  hash = myhash (re->path) % RULESET_TABLE_SIZE;
  re->next = ruleSet[hash];
  ruleSet[hash] = re;

  return re;
}

void
InitRuleSet (void)
{
  int i;

  /* Initialize hash table. */
  ruleSet = mymalloc (sizeof (*ruleSet) * RULESET_TABLE_SIZE);
  memset (ruleSet, 0, sizeof (*ruleSet) * RULESET_TABLE_SIZE);

  /* Initialize masks. */
  for (i = 0; i < RMASK_MAX; i++)
    currentMasks[i] = RMASK_DEFAULT;

  /* Populate with root entry. */
  addRuleEntry ("/", RFLAG_DEFAULT, RFLAG_DEFAULT, currentMasks);
}

void
CleanRuleSet (void)
{
  int i;
  struct RuleEntry *re, *nextre;

  if (!ruleSet)
    return;

  for (i = RULESET_TABLE_SIZE - 1; i >= 0; i--) {
    re = ruleSet[i];
    while (re) {
      nextre = re->next;
      destroyRuleEntry (re);
      re = nextre;
    }
  }

  free (ruleSet);
}

static int
parseFlags (const char *flagsStr, rflag_t *flags)
{
  int i;
  int addMode = 1;

  if (flagsStr[0] == '+' || flagsStr[0] == '-') {
    *flags = RFLAG_DEFAULT;
  }
  else {
    *flags = 0;

    for (i = 0; i < TEMPLATE_COUNT; i++) {
      if (flagTemplate[i].c == *flagsStr) {
	*flags = flagTemplate[i].flags;
	flagsStr++;
	addMode = -1;
	break;
      }
    }
  }

  while (*flagsStr) {
    if (*flagsStr == '+') {
      addMode = 1;
      flagsStr++;
      continue;
    }
    else if (*flagsStr == '-') {
      addMode = 0;
      flagsStr++;
      continue;
    }

    for (i = 0; i < RFLAG_MAX; i++)
      if (*flagsStr == flagChars[i])
	break;

    if (i < RFLAG_MAX) {
      if (addMode == -1)
	return 0;
      else if (addMode)
	*flags |= 1 << i;
      else
	*flags &= ~(1 << i);
    }
    else
      return 0;

    flagsStr++;
  }

  return 1;
}

static int
checkPath (const char *path)
{
  /* Make sure there are no empty path components. */
  if (strstr (path, "//"))
    return 0;

  /* Make sure it's an absolute path. */
  if (path[0] != '/')
    return 0;

  /* Looks ok. */
  return 1;
}

static void
resolveRuleEntry (struct RuleEntry *re)
{
  struct RuleEntry *parentre;
  char *parent, *tmp;
  int i;

  if (re->descFlags & RFLAG_UPDATE) {
    parent = mydirname (re->path);

    for (;;) {
      if ((parentre = FindRuleEntry (parent)) &&
	  !(parentre->descFlags & RFLAG_IGNORE)) {
	re->descFlags = parentre->descFlags;
	for (i = 0; i < RMASK_MAX; i++)
	  re->masks[i] = parentre->masks[i];
	break;
      }
      if (!strcmp (parent, "/")) {
	/* End of the line, use default. */
	re->descFlags = RFLAG_DEFAULT;
	for (i = 0; i < RMASK_MAX; i++)
	  re->masks[i] = RMASK_DEFAULT;
	break;
      }
      /* Try next ancestor. */
      tmp = mydirname (parent);
      free (parent);
      parent = tmp;
    }

    free (parent);
  }
}

static int
parseSpecial (const char *entry, const char *flagsStr)
{
  int classType = -1;
  rflag_t flags;

  if (!strcmp (entry, CONFIG_DIRMASK))
    classType = RMASK_DIR;
  else if (!strcmp (entry, CONFIG_FILEMASK))
    classType = RMASK_FILE;
  else if (!strcmp (entry, CONFIG_LINKMASK))
    classType = RMASK_LINK;
  else if (!strcmp (entry, CONFIG_SPECIALMASK))
    classType = RMASK_SPECIAL;

  if (classType != -1) {
    if (!flagsStr)
      flags = RFLAG_DEFAULT | RFLAG_SIZE | RFLAG_ATIME;
    else if (!parseFlags (flagsStr, &flags))
      return -1;

    currentMasks[classType] = flags;
    return 1;
  }

  return 0;
}

#define ARGC_MAX 16

int
ParseRuleSet (const char *conf)
{
  FILE *f;
  int success = 1;

  int hardLineNo, lineNo, linePos;
  char lineBuf[LINE_BUFFER_SIZE], *line;
  int lineLen;
  int argc;
  char *argv[ARGC_MAX];
  int inQuote, inEsc, quoteErr;
  char *dst;

  int i, j, len;
  char *entry, *flagsStr;
  rflag_t flags;
  struct RuleEntry *re, *nextre, *lastre;
  int ret;

  if ((f = fopen (conf, "r"))) {
    if (DisplayHashes > 1) {
      DisplayFileHash (fileno (f), conf);
      if (fseek (f, 0, SEEK_SET) == -1)
	yaficError (conf);
    }

#ifdef YAFIC_CRYPTO
    if (SignVerifyFiles) {
      VerifyFile (fileno (f), conf, NULL);
      if (fseek (f, 0, SEEK_SET) == -1)
	yaficError (conf);
    }
#endif

    /* Read in the config file, line by line. */
    hardLineNo = lineNo = 0;
    linePos = 0;
    while (!ferror (f) &&
	   fgets (&lineBuf[linePos], LINE_BUFFER_SIZE - linePos, f)) {
      hardLineNo++;

      /* Eliminate trailing whitespace. */
      lineLen = strlen (lineBuf) - 1;
      while (lineLen >= 0 && isspace ((int) lineBuf[lineLen]))
	lineBuf[lineLen--] = '\0';

      /* Keep track of first line of any line continuations. */
      if (!linePos)
	lineNo = hardLineNo;

      /* See if line should be continued. */
      if (lineLen >= 0 && lineBuf[lineLen] == '\\') {
	lineBuf[lineLen] = '\0';
	linePos = lineLen;
	continue;
      }

      /* Reset line buffer position. */
      linePos = 0;

      /* Skip any leading whitespace. */
      line = lineBuf;
      while (*line && isspace ((int) *line))
	line++;

      /* Skip over blank lines and comments. */
      if (!*line || *line == '#')
	continue;

      /* Break apart the line into tokens. */

      argc = 0;
      quoteErr = 0;
      dst = line;
      while (*line && argc < ARGC_MAX) {
	/* Skip any leading whitespace. */
	while (*line && isspace ((int) *line))
	  line++;

	/* If that's it, break out of the loop. */
	if (!*line || *line == '#')
	  break;

	/* Got us a token. */
	argv[argc++] = dst;

	inQuote = inEsc = 0;
	while (*line && (inQuote || !isspace ((int) *line))) {
	  if (inEsc) {
	    /* Handle an escaped char. For now, whatever follows the
	       escape char is simply copied. */
	    *(dst++) = *(line++);
	    inEsc = 0;
	  }
	  else if (*line == '"') {
	    /* A beginning or ending quote. */
	    inQuote ^= 1;
	    line++;
	  }
	  else if (*line == '\\') {
	    /* The beginning of an escape sequence. */
	    inEsc = 1;
	    line++;
	  }
	  else if (!inQuote && *line == '#') {
	    /* Start of a comment. End the token and the line. */
	    *line = '\0';
	  }
	  else /* Everything else. */
	    *(dst++) = *(line++);
	}

	/* Skip over the whitespace, if necessary. */
	if (*line)
	  line++;

	/* End the token. */
	*(dst++) = '\0';

	if (inQuote) {
	  quoteErr = 1;
	  break;
	}
      }

      if (quoteErr) {
	fprintf (stderr, "%s: %s:%d: unmatched quote\n",
		 prog, conf, lineNo);
	success = 0;
	continue;
      }

      /* Grab entry and flags. */
      entry = argv[0];
      flagsStr = argc > 1 ? argv[1] : NULL;
      if (argc > 2)
	fprintf (stderr, "%s: %s:%d: ignoring junk after flags\n",
		 prog, conf, lineNo);

      /* Check for special entries. */
      if ((ret = parseSpecial(entry, flagsStr)) == 1)
	continue;
      else if (ret == -1) {
	fprintf (stderr, "%s: %s:%d: bad flags\n",
		 prog, conf, lineNo);
	success = 0;
	continue;
      }

      /* Wipe out trailing slash... except if it's just '/'. */
      i = strlen (entry) - 1;
      if (i > 1 && entry[i] == '/')
	entry[i] = '\0';

      /* Make sure entry is correctly formed. */
      if (entry[0] == '!' || entry[0] == '=' || entry[0] == '$') {
	if (!checkPath (&entry[1])) {
	  fprintf (stderr, "%s: %s:%d: bad path\n",
		   prog, conf, lineNo);
	  success = 0;
	  continue;
	}
      }
      else if (!checkPath (entry)) {
	fprintf (stderr, "%s: %s:%d: bad path\n",
		 prog, conf, lineNo);
	success = 0;
	continue;
      }

      if (!flagsStr)
	flags = RFLAG_DEFAULT;
      else if (!parseFlags (flagsStr, &flags)) {
	fprintf (stderr, "%s: %s:%d: bad flags\n",
		 prog, conf, lineNo);
	success = 0;
	continue;
      }

      /* Parse the entry. */
      if (*entry == '!') {
	/* Ignore entry. */
	entry++;
	/* Wipe out existing entries. */
	len = strlen (entry);
	for (i = 0; i < RULESET_TABLE_SIZE; i++) {
	  lastre = NULL;
	  re = ruleSet[i];
	  while (re) {
	    nextre = re->next;
	    if (!strncmp (entry, re->path, len)) {
	      if (!lastre)
		ruleSet[i] = nextre;
	      else
		lastre->next = nextre;
	      destroyRuleEntry (re);
	    }
	    else
	      lastre = re;
	    re = nextre;
	  }
	}
	addRuleEntry (entry, RFLAG_IGNORE, RFLAG_IGNORE, currentMasks);
      }
      else if (*entry == '=') {
	/* Directory only, no recursion. */
	entry++;
	if ((re = FindRuleEntry (entry))) {
	  re->entryFlags = flags;
	  re->descFlags = RFLAG_IGNORE;
	}
	else
	  addRuleEntry (entry, flags, RFLAG_IGNORE, currentMasks);
      }
      else if (*entry == '$') {
	/* Directory only, recurse. */
	entry++;
	if ((re = FindRuleEntry (entry)))
	  re->entryFlags = flags;
	else
	  addRuleEntry (entry, flags, RFLAG_UPDATE, currentMasks);
      }
      else {
	/* Normal entry. */
	if ((re = FindRuleEntry (entry))) {
	  re->entryFlags = flags;
	  re->descFlags = flags;
	  for (j = 0; j < RMASK_MAX; j++)
	    re->masks[j] = currentMasks[j];
	}
	else
	  addRuleEntry (entry, flags, flags, currentMasks);
      }
    }
    if (ferror (f))
      yaficError (conf);

    if (linePos) {
      fprintf (stderr, "%s: %s:%d: incomplete line continuation\n",
	       prog, conf, lineNo);
      success = 0;
    }
  }
  else
    yaficError (conf);

  if (success)
    ApplyRuleSet (resolveRuleEntry);

  return success;
}

static char *
dumpFlags (rflag_t flags)
{
  char *p;
  int i;
  rflag_t test;
  static char buf[16];

  if (flags & RFLAG_IGNORE) {
    buf[0] = '-';
    buf[1] = '\0';
    return buf;
  }

  p = buf;
  for (i = 0, test = 1; i < RFLAG_MAX; i++, test <<= 1)
    if (flags & test)
      *(p++) = flagChars[i];
  *(p++) = '\0';

  return buf;
}

void
DumpRuleEntry (struct RuleEntry *re)
{
  printf ("  %s (%s, ", re->path, dumpFlags (re->entryFlags));
  printf ("%s) (", dumpFlags (re->descFlags));
  printf ("d:%s ", dumpFlags (re->masks[RMASK_DIR]));
  printf ("f:%s ", dumpFlags (re->masks[RMASK_FILE]));
  printf ("l:%s ", dumpFlags (re->masks[RMASK_LINK]));
  printf ("s:%s)\n", dumpFlags (re->masks[RMASK_SPECIAL]));
}
