/* 
 * CNID database support. 
 *
 * here's the deal:
 *  1) afpd already caches did's. 
 *  2) the database stores cnid's as both did/name and dev/ino pairs. 
 *  3) RootInfo holds the value of the NextID.
 *  4) the cnid database gets called in the following manner --
 *     start a database:
 *     cnid = cnid_open(root_dir);
 *
 *     allocate a new id: 
 *     newid = cnid_add(cnid, dev, ino, parent did,
 *     name, id); id is a hint for a specific id. pass 0 if you don't
 *     care. if the id is already assigned, you won't get what you
 *     requested.
 *
 *     given an id, get a did/name and dev/ino pair.
 *     name = cnid_get(cnid, &id); given an id, return the corresponding
 *     info.
 *     return code = cnid_delete(cnid, id); delete an entry. 
 *
 * with AFP, CNIDs 0-2 have special meanings. here they are:
 * 0 -- invalid cnid
 * 1 -- parent of root directory (handled by afpd) 
 * 2 -- root directory (handled by afpd)
 *
 * so, CNID_START begins at 3.
 */

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <syslog.h>

#include <db.h>

#include <atalk/adouble.h>
#include <atalk/cnid.h>

#include "cnid_private.h"

#define ROOTINFO     "RootInfo"
#define ROOTINFO_LEN 8

#define DBHOME       ".AppleDB"
#define DBCNID       "cnid.db"
#define DBDEVINO     "devino.db"
#define DBDIDNAME    "didname.db"
#define DBLOCKFILE   "/cnid.lock"

#define DBHOMELEN    8
#define DBLEN        10

#define DBOPTIONS    (DB_CREATE | DB_INIT_MPOOL | DB_INIT_LOCK | \
DB_INIT_LOG | DB_INIT_TXN | DB_TXN_NOSYNC | DB_RECOVER)

#define MAXITER     32767 /* maximum number of simultaneously open CNID 
			   * databases. */

void *cnid_open(const char *dir)
{
  struct stat st;
  struct flock lock;
  char path[MAXPATHLEN + 1];
  CNID_private *db;
  DB_INFO dbi;
  int len, open_flag;

  if (!dir)
    return NULL;

  /* this checks both RootInfo and .AppleDB */
  if ((len = strlen(dir)) > (MAXPATHLEN - DBLEN - 1)) {
    syslog(LOG_ERR, "cnid_open: path too large");
    return NULL;
  }
  
  if ((db = (CNID_private *) calloc(1, sizeof(CNID_private))) == NULL) {
    syslog(LOG_ERR, "cnid_open: unable to allocate memory");
    return NULL;
  }
  db->magic = CNID_DB_MAGIC;
    
  strcpy(path, dir);
  if (path[len - 1] != '/') {
    strcat(path, "/");
    len++;
  }

  lock.l_type = F_WRLCK;
  lock.l_whence = SEEK_SET;

  /* we create and initialize RootInfo if it doesn't exist. */
  strcat(path, ROOTINFO);
  if (ad_open(path, ADFLAGS_HF, O_RDWR, 0666, &db->rootinfo) < 0) {
    cnid_t id;

    /* see if we can open it read-only. if it's read-only, we can't
     * add CNIDs. */
    if (ad_open(path, ADFLAGS_HF, O_RDONLY, 0666, &db->rootinfo) == 0) {
      db->flags = CNIDFLAG_ROOTINFO_RO;
      syslog(LOG_INFO, "cnid_open: read-only RootInfo");
      goto mkdir_appledb;
    }

    /* create the file */
    if (ad_open(path, ADFLAGS_HF, O_CREAT | O_RDWR, 0666, 
		&db->rootinfo) < 0) {
      syslog(LOG_ERR, "cnid_open: ad_open(RootInfo)");
      goto fail_db;
    }

    /* lock the RootInfo file. this and cnid_add are the only places
     * that should fiddle with RootInfo. */
    lock.l_start = ad_getentryoff(&db->rootinfo, ADEID_DID);
    lock.l_len = ad_getentrylen(&db->rootinfo, ADEID_DID);
    if (fcntl(ad_hfileno(&db->rootinfo), F_SETLKW,  &lock) < 0) {
      syslog(LOG_ERR, "cnid_open: can't establish lock: %m");
      goto fail_adouble;
    }
    
    /* store the beginning CNID */
    id = htonl(CNID_START);
    memcpy(ad_entry(&db->rootinfo, ADEID_DID), &id, sizeof(id));
    ad_flush(&db->rootinfo, ADFLAGS_HF);

    /* unlock it */
    lock.l_type = F_UNLCK;
    fcntl(ad_hfileno(&db->rootinfo), F_SETLK, &lock);
    lock.l_type = F_WRLCK;
  }

mkdir_appledb:
  strcpy(path + len, DBHOME);
  if ((stat(path, &st) < 0) && (ad_mkdir(path, 0777) < 0)) {
    syslog(LOG_ERR, "cnid_open: mkdir failed");
    goto fail_adouble;
  }

  /* search for a byte lock. this allows us to cleanup the log files
   * at cnid_close() in a clean fashion.
   *
   * NOTE: this won't work if multiple volumes for the same user refer
   * to the same directory. */
  strcat(path, DBLOCKFILE);
  if ((db->lockfd = open(path, O_RDWR | O_CREAT, 0666)) > -1) {
    lock.l_start = 0;
    lock.l_len = 1;
    while (fcntl(db->lockfd, F_SETLK, &lock) < 0) {
      if (++lock.l_start > MAXITER) {
	syslog(LOG_INFO, "cnid_open: can't establish logfile cleanup lock.");
	close(db->lockfd);
	db->lockfd = -1;
	break;
      }      
    }
  } 

  path[len + DBHOMELEN] = '\0';
  open_flag = DB_CREATE;
  /* try a full-blown transactional environment */
  if (db_appinit(path, NULL, &db->dbenv, DBOPTIONS)) {

    /* try with a shared memory pool */
    memset(&db->dbenv, 0, sizeof(db->dbenv));
    if (db_appinit(path, NULL, &db->dbenv, DB_INIT_MPOOL)) {

      /* try without any options. */
      memset(&db->dbenv, 0, sizeof(db->dbenv));
      if (db_appinit(path, NULL, &db->dbenv, 0)) {
	syslog(LOG_ERR, "cnid_open: db_appinit failed");
	goto fail_lock;
      }
    }
    db->flags |= CNIDFLAG_DB_RO;
    open_flag = DB_RDONLY;
    syslog(LOG_INFO, "cnid_open: read-only CNID database");
  }

  memset(&dbi, 0, sizeof(dbi));

  /* main cnid database */
  if (db_open(DBCNID, DB_HASH, open_flag, 0666, &db->dbenv, &dbi,
	      &db->db_cnid)) {
    goto fail_appinit;
  }

  /* did/name reverse mapping */
  if (db_open(DBDIDNAME, DB_HASH, open_flag, 0666, &db->dbenv, &dbi,
	      &db->db_didname)) {
    db->db_cnid->close(db->db_cnid, 0);
    goto fail_appinit;
  }

  /* dev/ino reverse mapping */
  if (db_open(DBDEVINO, DB_HASH, open_flag, 0666, &db->dbenv, &dbi,
	      &db->db_devino)) {
    db->db_didname->close(db->db_didname, 0);
    db->db_cnid->close(db->db_cnid, 0);
    goto fail_appinit;
  }
  return db;
  
fail_appinit:
  syslog(LOG_ERR, "cnid_open: db_open failed");
  db_appexit(&db->dbenv);

fail_lock:
  if (db->lockfd > -1)
    close(db->lockfd);

fail_adouble:
  ad_close(&db->rootinfo, ADFLAGS_HF);

fail_db:
  free(db);
  return NULL;
}
