/* $Cambridge: exim/exim-src/src/exim_dbmbuild.c,v 1.2 2005/01/04 10:00:42 ph10 Exp $ */

/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 2005 */
/* See the file NOTICE for conditions of use and distribution. */


/* A small freestanding program to build dbm databases from serial input. For
alias files, this program fulfils the function of the newaliases program used
by other mailers, but it can be used for other dbm data files too. It operates
by writing a new file or files, and then renaming; otherwise old entries can
never get flushed out.

This program is clever enough to cope with ndbm, which creates two files called
<name>.dir and <name>.pag, or with db, which creates a single file called
<name>.db. If native db is in use (USE_DB defined) or tdb is in use (USE_TDB
defined) there is no extension to the output filename. This is also handled. If
there are any other variants, the program won't cope.

The first argument to the program is the name of the serial file; the second
is the base name for the DBM file(s). When native db is in use, these must be
different.

Input lines beginning with # are ignored, as are blank lines. Entries begin
with a key terminated by a colon or end of line or whitespace and continue with
indented lines. Keys may be quoted if they contain colons or whitespace or #
characters. */


#include "exim.h"


#define max_insize   20000
#define max_outsize 100000

/* This is global because it's defined in the headers and compilers grumble
if it is made static. */

uschar *hex_digits = US"0123456789abcdef";


#ifdef STRERROR_FROM_ERRLIST
/* Some old-fashioned systems still around (e.g. SunOS4) don't have strerror()
in their libraries, but can provide the same facility by this simple
alternative function. */

char *
strerror(int n)
{
if (n < 0 || n >= sys_nerr) return "unknown error number";
return sys_errlist[n];
}
#endif /* STRERROR_FROM_ERRLIST */


/* For Berkeley DB >= 2, we can define a function to be called in case of DB
errors. This should help with debugging strange DB problems, e.g. getting "File
exists" when you try to open a db file. */

#if defined(USE_DB) && defined(DB_VERSION_STRING)
void
dbfn_bdb_error_callback(const char *pfx, char *msg)
{
pfx = pfx;
printf("Berkeley DB error: %s\n", msg);
}
#endif



/*************************************************
*          Interpret escape sequence             *
*************************************************/

/* This function is copied from the main Exim code.

Arguments:
  pp       points a pointer to the initiating "\" in the string;
           the pointer gets updated to point to the final character
Returns:   the value of the character escape
*/

int
string_interpret_escape(uschar **pp)
{
int ch;
uschar *p = *pp;
ch = *(++p);
if (isdigit(ch) && ch != '8' && ch != '9')
  {
  ch -= '0';
  if (isdigit(p[1]) && p[1] != '8' && p[1] != '9')
    {
    ch = ch * 8 + *(++p) - '0';
    if (isdigit(p[1]) && p[1] != '8' && p[1] != '9')
      ch = ch * 8 + *(++p) - '0';
    }
  }
else switch(ch)
  {
  case 'n':  ch = '\n'; break;
  case 'r':  ch = '\r'; break;
  case 't':  ch = '\t'; break;
  case 'x':
  ch = 0;
  if (isxdigit(p[1]))
    {
    ch = ch * 16 +
      Ustrchr(hex_digits, tolower(*(++p))) - hex_digits;
    if (isxdigit(p[1])) ch = ch * 16 +
      Ustrchr(hex_digits, tolower(*(++p))) - hex_digits;
    }
  break;
  }
*pp = p;
return ch;
}


/*************************************************
*               Main Program                     *
*************************************************/

int main(int argc, char **argv)
{
int started;
int count = 0;
int dupcount = 0;
int yield = 0;
int arg = 1;
int add_zero = 1;
BOOL lowercase = TRUE;
BOOL warn = TRUE;
BOOL duperr = TRUE;
BOOL lastdup = FALSE;
#if !defined (USE_DB) && !defined(USE_TDB) && !defined(USE_GDBM)
int is_db = 0;
struct stat statbuf;
#endif
FILE *f;
EXIM_DB *d;
EXIM_DATUM key, content;
uschar *bptr;
uschar  keybuffer[256];
uschar  temp_dbmname[256];
uschar  real_dbmname[256];
uschar *buffer = malloc(max_outsize);
uschar *line = malloc(max_insize);

while (argc > 1)
  {
  if      (Ustrcmp(argv[arg], "-nolc") == 0)     lowercase = FALSE;
  else if (Ustrcmp(argv[arg], "-nowarn") == 0)   warn = FALSE;
  else if (Ustrcmp(argv[arg], "-lastdup") == 0)  lastdup = TRUE;
  else if (Ustrcmp(argv[arg], "-noduperr") == 0) duperr = FALSE;
  else if (Ustrcmp(argv[arg], "-nozero") == 0)   add_zero = 0;
  else break;
  arg++;
  argc--;
  }

if (argc != 3)
  {
  printf("usage: exim_dbmbuild [-nolc] <source file> <dbm base name>\n");
  exit(1);
  }

if (Ustrcmp(argv[arg], "-") == 0) f = stdin; else
  {
  f = fopen(argv[arg], "rb");
  if (f == NULL)
    {
    printf("exim_dbmbuild: unable to open %s: %s\n", argv[arg], strerror(errno));
    exit(1);
    }
  }

/* By default Berkeley db does not put extensions on... which
can be painful! */

#if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM)
if (Ustrcmp(argv[arg], argv[arg+1]) == 0)
  {
  printf("exim_dbmbuild: input and output filenames are the same\n");
  exit(1);
  }
#endif

Ustrcpy(temp_dbmname, argv[arg+1]);
Ustrcat(temp_dbmname, ".dbmbuild_temp");

/* It is apparently necessary to open with O_RDWR for this to work
with gdbm-1.7.3, though no reading is actually going to be done. */

EXIM_DBOPEN(temp_dbmname, O_RDWR|O_CREAT|O_EXCL, 0644, &d);

if (d == NULL)
  {
  printf("exim_dbmbuild: unable to create %s: %s\n", temp_dbmname,
    strerror(errno));
  fclose(f);
  exit(1);
  }

/* Unless using native db calls, see if we have created <name>.db; if not,
assume .dir & .pag */

#if !defined(USE_DB) && !defined(USE_TDB) && !defined(USE_GDBM)
sprintf(CS real_dbmname, "%s.db", temp_dbmname);
is_db = Ustat(real_dbmname, &statbuf) == 0;
#endif

/* Now do the business */

bptr = buffer;
started = 0;

while (Ufgets(line, max_insize, f) != NULL)
  {
  uschar *p;
  int len = Ustrlen(line);

  p = line + len;

  if (len >= max_insize - 1 && p[-1] != '\n')
    {
    printf("Overlong line read: max permitted length is %d\n", max_insize - 1);
    return 1;
    }

  if (line[0] == '#') continue;
  while (p > line && isspace(p[-1])) p--;
  *p = 0;
  if (line[0] == 0) continue;

  /* A continuation line is valid only if there was a previous first
  line. */

  if (isspace(line[0]))
    {
    uschar *s = line;
    if (!started)
      {
      printf("Unexpected continuation line ignored\n%s\n\n", line);
      continue;
      }
    while (isspace(*s)) s++;
    *(--s) = ' ';

    if (bptr - buffer + p - s >= max_outsize - 1)
      {
      printf("Continued set of lines is too long: max permitted length is %d\n",
        max_outsize -1);
      return 1;
      }

    Ustrcpy(bptr, s);
    bptr += p - s;
    }

  /* A first line must have a name followed by a colon or whitespace or
  end of line, but first finish with a previous line. The key is lower
  cased by default - this is what the newaliases program for sendmail does.
  However, there's an option not to do this. */

  else
    {
    int i, rc;
    uschar *s = line;
    uschar *keystart;

    if (started)
      {
      EXIM_DATUM_INIT(content);
      EXIM_DATUM_DATA(content) = CS buffer;
      EXIM_DATUM_SIZE(content) = bptr - buffer + add_zero;

      switch(rc = EXIM_DBPUTB(d, key, content))
        {
        case EXIM_DBPUTB_OK:
        count++;
        break;

        case EXIM_DBPUTB_DUP:
        if (warn) fprintf(stderr, "** Duplicate key \"%s\"\n",
          keybuffer);
        dupcount++;
        if(duperr) yield = 1;
        if (lastdup) EXIM_DBPUT(d, key, content);
        break;

        default:
        fprintf(stderr, "Error %d while writing key %s: errno=%d\n", rc,
          keybuffer, errno);
        yield = 2;
        goto TIDYUP;
        }

      bptr = buffer;
      }

    EXIM_DATUM_INIT(key);
    EXIM_DATUM_DATA(key) = CS keybuffer;

    /* Deal with quoted keys. Escape sequences always make one character
    out of several, so we can re-build in place. */

    if (*s == '\"')
      {
      uschar *t = s++;
      keystart = t;
      while (*s != 0 && *s != '\"')
        {
        if (*s == '\\') *t++ = string_interpret_escape(&s);
          else *t++ = *s;
        s++;
        }
      if (*s != 0) s++;               /* Past terminating " */
      EXIM_DATUM_SIZE(key) = t - keystart + add_zero;
      }
    else
      {
      keystart = s;
      while (*s != 0 && *s != ':' && !isspace(*s)) s++;
      EXIM_DATUM_SIZE(key) = s - keystart + add_zero;
      }

    if (EXIM_DATUM_SIZE(key) > 256)
      {
      printf("Keys longer than 255 characters cannot be handled\n");
      started = 0;
      yield = 2;
      goto TIDYUP;
      }

    if (lowercase)
      {
      for (i = 0; i < EXIM_DATUM_SIZE(key) - add_zero; i++)
        keybuffer[i] = tolower(keystart[i]);
      }
    else
      {
      for (i = 0; i < EXIM_DATUM_SIZE(key) - add_zero; i++)
        keybuffer[i] = keystart[i];
      }

    keybuffer[i] = 0;
    started = 1;

    while (isspace(*s))s++;
    if (*s == ':')
      {
      s++;
      while (isspace(*s))s++;
      }
    if (*s != 0)
      {
      Ustrcpy(bptr, s);
      bptr += p - s;
      }
    else buffer[0] = 0;
    }
  }

if (started)
  {
  int rc;
  EXIM_DATUM_INIT(content);
  EXIM_DATUM_DATA(content) = CS buffer;
  EXIM_DATUM_SIZE(content) = bptr - buffer + add_zero;

  switch(rc = EXIM_DBPUTB(d, key, content))
    {
    case EXIM_DBPUTB_OK:
    count++;
    break;

    case EXIM_DBPUTB_DUP:
    if (warn) fprintf(stderr, "** Duplicate key \"%s\"\n", keybuffer);
    dupcount++;
    if (duperr) yield = 1;
    if (lastdup) EXIM_DBPUT(d, key, content);
    break;

    default:
    fprintf(stderr, "Error %d while writing key %s: errno=%d\n", rc,
      keybuffer, errno);
    yield = 2;
    break;
    }
  }

/* Close files, rename or abandon the temporary files, and exit */

TIDYUP:

EXIM_DBCLOSE(d);
fclose(f);

/* If successful, output the number of entries and rename the temporary
files. */

if (yield == 0 || yield == 1)
  {
  printf("%d entr%s written\n", count, (count == 1)? "y" : "ies");
  if (dupcount > 0)
    {
    printf("%d duplicate key%s \n", dupcount, (dupcount > 1)? "s" : "");
    }

  #if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM)
  Ustrcpy(real_dbmname, temp_dbmname);
  Ustrcpy(buffer, argv[arg+1]);
  if (Urename(real_dbmname, buffer) != 0)
    {
    printf("Unable to rename %s as %s\n", real_dbmname, buffer);
    return 1;
    }
  #else

  /* Rename a single .db file */

  if (is_db)
    {
    sprintf(CS real_dbmname, "%s.db", temp_dbmname);
    sprintf(CS buffer, "%s.db", argv[arg+1]);
    if (Urename(real_dbmname, buffer) != 0)
      {
      printf("Unable to rename %s as %s\n", real_dbmname, buffer);
      return 1;
      }
    }

  /* Rename .dir and .pag files */

  else
    {
    sprintf(CS real_dbmname, "%s.dir", temp_dbmname);
    sprintf(CS buffer, "%s.dir", argv[arg+1]);
    if (Urename(real_dbmname, buffer) != 0)
      {
      printf("Unable to rename %s as %s\n", real_dbmname, buffer);
      return 1;
      }

    sprintf(CS real_dbmname, "%s.pag", temp_dbmname);
    sprintf(CS buffer, "%s.pag", argv[arg+1]);
    if (Urename(real_dbmname, buffer) != 0)
      {
      printf("Unable to rename %s as %s\n", real_dbmname, buffer);
      return 1;
      }
    }

  #endif /* USE_DB || USE_TDB || USE_GDBM */
  }

/* Otherwise unlink the temporary files. */

else
  {
  printf("dbmbuild abandoned\n");
  #if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM)
  Uunlink(temp_dbmname);
  #else
  if (is_db)
    {
    sprintf(CS real_dbmname, "%s.db", temp_dbmname);
    Uunlink(real_dbmname);
    }
  else
    {
    sprintf(CS real_dbmname, "%s.dir", temp_dbmname);
    Uunlink(real_dbmname);
    sprintf(CS real_dbmname, "%s.pag", temp_dbmname);
    Uunlink(real_dbmname);
    }
  #endif /* USE_DB || USE_TDB */
  }

return yield;
}

/* End of exim_dbmbuild.c */
