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

/*
 * Exim - CDB database lookup module
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * --------------------------------------------------------------
 * Modified by PH for Exim 4:
 *   Changed over to using unsigned chars
 *   Makes use of lf_check_file() for file checking
 * --------------------------------------------------------------
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 *
 * This code implements Dan Bernstein's Constant DataBase (cdb) spec.
 * Information, the spec and sample code for cdb can be obtained from
 *      http://www.pobox.com/~djb/cdb.html
 *
 * This implementation borrows some code from Dan Bernstein's
 * implementation (which has no license restrictions applied to it).
 * This (read-only) implementation is completely contained within
 * cdb.[ch] it does *not* link against an external cdb library.
 *
 *
 * There are 2 varients included within this code.  One uses MMAP and
 * should give better performance especially for multiple lookups on a
 * modern machine.  The other is the default implementation which is
 * used in the case where the MMAP fails or if MMAP was not compiled
 * in.  this implementation is the same as the original reference cdb
 * implementation.  The MMAP version is compiled in if the HAVE_MMAP
 * preprocessor define is defined - this should be set in the system
 * specific os.h file.
 *
 */


#include "../exim.h"
#include "lf_functions.h"
#include "cdb.h"

#ifdef HAVE_MMAP
#  include <sys/mman.h>
/* Not all implementations declare MAP_FAILED */
#  ifndef MAP_FAILED
#    define MAP_FAILED ((void *) -1)
#  endif /* MAP_FAILED */
#endif /* HAVE_MMAP */


#define CDB_HASH_SPLIT 256     /* num pieces the hash table is split into */
#define CDB_HASH_MASK  255     /* mask to and off split value */
#define CDB_HASH_ENTRY 8       /* how big each offset it */
#define CDB_HASH_TABLE (CDB_HASH_SPLIT * CDB_HASH_ENTRY)

/* State information for cdb databases that are open NB while the db
 * is open its contents will not change (cdb dbs are normally updated
 * atomically by renaming).  However the lifetime of one of these
 * state structures should be limited - ie a long running daemon
 * that opens one may hit problems....
 */

struct cdb_state {
  int     fileno;
  off_t   filelen;
  uschar *cdb_map;
  uschar *cdb_offsets;
};

/* 32 bit unsigned type - this is an int on all modern machines */
typedef unsigned int uint32;

/*
 * cdb_hash()
 * Internal function to make hash value */

static uint32
cdb_hash(uschar *buf, unsigned int len)
{
  uint32 h;

  h = 5381;
  while (len) {
    --len;
    h += (h << 5);
    h ^= (uint32) *buf++;
  }
  return h;
}

/*
 * cdb_bread()
 * Internal function to read len bytes from disk, coping with oddities */

static int
cdb_bread(int fd,
	 uschar *buf,
	 int len)
{
  int r;
  while (len > 0) {
    do
      r = Uread(fd,buf,len);
    while ((r == -1) && (errno == EINTR));
    if (r == -1) return -1;
    if (r == 0) { errno = EIO; return -1; }
    buf += r;
    len -= r;
  }
  return 0;
}

/*
 * cdb_bread()
 * Internal function to parse 4 byte number (endian independant) */

static uint32
cdb_unpack(uschar *buf)
{
  uint32 num;
  num =  buf[3]; num <<= 8;
  num += buf[2]; num <<= 8;
  num += buf[1]; num <<= 8;
  num += buf[0];
  return num;
}

void *
cdb_open(uschar *filename,
	 uschar **errmsg)
{
  int fileno;
  struct cdb_state *cdbp;
  struct stat statbuf;
  void * mapbuf;

  fileno = Uopen(filename, O_RDONLY, 0);
  if (fileno == -1) {
    int save_errno = errno;
    *errmsg = string_open_failed(errno, "%s for cdb lookup", filename);
    errno = save_errno;
    return NULL;
  }

  if (fstat(fileno, &statbuf) == 0) {
    /* If this is a valid file, then it *must* be at least
     * CDB_HASH_TABLE bytes long */
    if (statbuf.st_size < CDB_HASH_TABLE) {
      int save_errno = errno;
      *errmsg = string_open_failed(errno,
				  "%s to short for cdb lookup",
				  filename);
      errno = save_errno;
      return NULL;
    }
  } else {
    int save_errno = errno;
    *errmsg = string_open_failed(errno,
				"fstat(%s) failed - cannot do cdb lookup",
				filename);
    errno = save_errno;
    return NULL;
  }

  /* Having got a file open we need the structure to put things in */
  cdbp = store_malloc(sizeof(struct cdb_state));
  /* store_malloc() does not return if memory was not available... */
  /* preload the structure.... */
  cdbp->fileno = fileno;
  cdbp->filelen = statbuf.st_size;
  cdbp->cdb_map = NULL;
  cdbp->cdb_offsets = NULL;

  /* if we are allowed to we use mmap here.... */
#ifdef HAVE_MMAP
  mapbuf = mmap(NULL,
	       statbuf.st_size,
	       PROT_READ,
	       MAP_SHARED,
	       fileno,
	       0);
  if (mapbuf != MAP_FAILED) {
    /* We have an mmap-ed section.  Now we can just use it */
    cdbp->cdb_map = mapbuf;
    /* The offsets can be set to the same value since they should
     * effectively be cached as well
     */
    cdbp->cdb_offsets = mapbuf;

    /* Now return the state struct */
    return(cdbp);
  } else {
    /* If we got here the map failed.  Basically we can ignore
     * this since we fall back to slower methods....
     * However lets debug log it...
     */
    DEBUG(D_lookup) debug_printf("cdb mmap failed - %d\n", errno);
  }
#endif /* HAVE_MMAP */

  /* In this case we have either not got MMAP allowed, or it failed */

  /* get a buffer to stash the basic offsets in - this should speed
   * things up a lot - especially on multiple lookups */
  cdbp->cdb_offsets = store_malloc(CDB_HASH_TABLE);

  /* now fill the buffer up... */
  if (cdb_bread(fileno, cdbp->cdb_offsets, CDB_HASH_TABLE) == -1) {
    /* read of hash table failed, oh dear, oh.....
     * time to give up I think....
     * call the close routine (deallocs the memory), and return NULL */
    *errmsg = string_open_failed(errno,
				"cannot read header from %s for cdb lookup",
				filename);
    cdb_close(cdbp);
    return NULL;
  }

  /* Everything else done - return the cache structure */
  return cdbp;
}



/*************************************************
*             Check entry point                  *
*************************************************/

BOOL
cdb_check(void *handle,
	 uschar *filename,
	 int modemask,
	 uid_t *owners,
	 gid_t *owngroups,
	 uschar **errmsg)
{
  struct cdb_state * cdbp = handle;
  return lf_check_file(cdbp->fileno,
		       filename,
                       S_IFREG,
		       modemask,
		       owners,
		       owngroups,
		       "cdb",
                       errmsg) == 0;
}



/*************************************************
*              Find entry point                  *
*************************************************/

int
cdb_find(void *handle,
	uschar *filename,
	uschar *keystring,
	int  key_len,
	uschar **result,
	uschar **errmsg)
{
  struct cdb_state * cdbp = handle;
  uint32 item_key_len,
    item_dat_len,
    key_hash,
    item_hash,
    item_posn,
    cur_offset,
    end_offset,
    hash_offset_entry,
    hash_offset,
    hash_offlen,
    hash_slotnm;
  int loop;

  key_hash = cdb_hash((uschar *)keystring, key_len);

  hash_offset_entry = CDB_HASH_ENTRY * (key_hash & CDB_HASH_MASK);
  hash_offset = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry);
  hash_offlen = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry + 4);

  /* If the offset length is zero this key cannot be in the file */
  if (hash_offlen == 0) {
    return FAIL;
  }
  hash_slotnm = (key_hash >> 8) % hash_offlen;

  /* check to ensure that the file is not corrupt
   * if the hash_offset + (hash_offlen * CDB_HASH_ENTRY) is longer
   * than the file, then we have problems.... */
  if ((hash_offset + (hash_offlen * CDB_HASH_ENTRY)) > cdbp->filelen) {
    *errmsg = string_sprintf("cdb: corrupt cdb file %s (too short)",
			    filename);
    DEBUG(D_lookup) debug_printf("%s\n", *errmsg);
    return DEFER;
  }

  cur_offset = hash_offset + (hash_slotnm * CDB_HASH_ENTRY);
  end_offset = hash_offset + (hash_offlen * CDB_HASH_ENTRY);
  /* if we are allowed to we use mmap here.... */
#ifdef HAVE_MMAP
  /* make sure the mmap was OK */
  if (cdbp->cdb_map != NULL) {
    uschar * cur_pos = cur_offset + cdbp->cdb_map;
    uschar * end_pos = end_offset + cdbp->cdb_map;
    for (loop = 0; (loop < hash_offlen); ++loop) {
      item_hash = cdb_unpack(cur_pos);
      cur_pos += 4;
      item_posn = cdb_unpack(cur_pos);
      cur_pos += 4;
      /* if the position is zero then we have a definite miss */
      if (item_posn == 0)
       return FAIL;

      if (item_hash == key_hash) {
       /* matching hash value */
       uschar * item_ptr = cdbp->cdb_map + item_posn;
       item_key_len = cdb_unpack(item_ptr);
       item_ptr += 4;
       item_dat_len = cdb_unpack(item_ptr);
       item_ptr += 4;
       /* check key length matches */
       if (item_key_len == key_len) {
	 /* finally check if key matches */
	 if (Ustrncmp(keystring, item_ptr, key_len) == 0) {
	   /* we have a match....
	    * make item_ptr point to data */
	   item_ptr += item_key_len;
	   /* ... and the returned result */
	   *result = store_malloc(item_dat_len + 1);
	   memcpy(*result, item_ptr, item_dat_len);
	   (*result)[item_dat_len] = 0;
	   return OK;
	 }
       }
      }
      /* handle warp round of table */
      if (cur_pos == end_pos)
       cur_pos = cdbp->cdb_map + hash_offset;
    }
    /* looks like we failed... */
    return FAIL;
  }
#endif /* HAVE_MMAP */
  for (loop = 0; (loop < hash_offlen); ++loop) {
    uschar packbuf[8];
    if (lseek(cdbp->fileno, (off_t) cur_offset,SEEK_SET) == -1) return DEFER;
    if (cdb_bread(cdbp->fileno, packbuf,8) == -1) return DEFER;
    item_hash = cdb_unpack(packbuf);
    item_posn = cdb_unpack(packbuf + 4);
    /* if the position is zero then we have a definite miss */
    if (item_posn == 0)
      return FAIL;

    if (item_hash == key_hash) {
      /* matching hash value */
      if (lseek(cdbp->fileno, (off_t) item_posn, SEEK_SET) == -1) return DEFER;
      if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER;
      item_key_len = cdb_unpack(packbuf);
      /* check key length matches */
      if (item_key_len == key_len) {
       /* finally check if key matches */
       uschar * item_key = store_malloc(key_len);
       if (cdb_bread(cdbp->fileno, item_key, key_len) == -1) return DEFER;
       if (Ustrncmp(keystring, item_key, key_len) == 0) {
	 /* matches - get data length */
	 item_dat_len = cdb_unpack(packbuf + 4);
	 /* then we build a new result string */
	 *result = store_malloc(item_dat_len + 1);
	 if (cdb_bread(cdbp->fileno, *result, item_dat_len) == -1)
	   return DEFER;
	 (*result)[item_dat_len] = 0;
	 return OK;
       }
      }
    }
    cur_offset += 8;

    /* handle warp round of table */
    if (cur_offset == end_offset)
      cur_offset = hash_offset;
  }
  return FAIL;
}



/*************************************************
*              Close entry point                 *
*************************************************/

/* See local README for interface description */

void
cdb_close(void *handle)
{
struct cdb_state * cdbp = handle;

#ifdef HAVE_MMAP
 if (cdbp->cdb_map) {
   munmap(CS cdbp->cdb_map, cdbp->filelen);
   if (cdbp->cdb_map == cdbp->cdb_offsets)
     cdbp->cdb_offsets = NULL;
 }
#endif /* HAVE_MMAP */
 if (cdbp->cdb_offsets)
   store_free(cdbp->cdb_offsets);

 close(cdbp->fileno);
 store_free(cdbp);
}

/* End of lookups/cdb.c */
