/*
 * sysdb.c  -  Manage the systems database
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  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
 *  any later version.
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: sysdb.c,v 1.17 2007/04/22 18:52:34 gkminix Exp $
 */

#include <common.h>
#include <nblib.h>
#include "privlib.h"
#include "privdb.h"



/*
 * Global handle for systems database
 */
static DBHDL dbhandle = DBHDL_NULL;




/*
 * Error handler for database routines
 */
static void syserr __F((dbname, msg, class),
				const char *dbname AND
				const char *msg AND
				dberrclass class)
{
  char *classmsg = NULL;
  int errnum = 0;

  /* Check for error number */
  if (msg != NULL && sscanf(msg, "<%d>", &errnum) == 1) {
	msg = strchr(msg, '>');
	if (msg[1] == '\0')
		msg = NULL;
	else
		msg++;
  }

  /* Print error message */
  switch (class) {
	case DBERR_CLASS_WARNING:
		classmsg = "Warning";
		break;
	case DBERR_CLASS_ERROR:
		classmsg = "Error";
		break;
	case DBERR_CLASS_FATAL:
		classmsg = "Fatal";
		break;
	case DBERR_CLASS_INFO:
		prnlog(LOGLEVEL_INFO, "Info: [%s] %s\n", dbname, msg);
		/* Fall through */
	default:
		classmsg = NULL;
		break;
  }
  if (classmsg != NULL && msg != NULL)
	prnerr("%s: [%s] %s", classmsg, dbname, msg);

  /* Terminate in case of fatal error */
  if (class == DBERR_CLASS_FATAL) {
	if (errnum > 0)
		nbexit(-1);
	nbexit(EXIT_DB);
  }
}



/*
 * Check for a record name without wildcards
 */
static int check_recname __F((recname), const char *recname)
{
  const char *cp, *colonp;

  for (cp = recname, colonp = NULL; *cp; cp++) {
	if (*cp == DBMATCH_SINGLE || *cp == DBMATCH_MULTI)
		break;
	if (*cp == ':') {
		if (colonp != NULL)
			break;
		colonp = cp;
	}
  }
  if (*cp != '\0' || colonp == NULL || colonp == recname || colonp[1] == '\0') {
	nberror(EXIT_DB, "invalid record name \"%s\" for system database", recname);
	return(FALSE);
  }
  return(TRUE);
}



/*
 * Open the systems database
 */
int opensysdb __F((readonly), int readonly)
{
  /* Check if handle is already open */
  if (dbhandle != DBHDL_NULL)
	return(TRUE);

  /* Generate a system database handle */
  dbhandle = createdb(NULL, &syserr);
  if (dbhandle == DBHDL_NULL)
	return(FALSE);

  /* Open systems database */
  if (opendb(dbhandle, readonly) == 0 || dberrors > 0) {
	if (dberrors == 0)
		nberror(EXIT_DB, "unable to open systems database");
	else
		nberror(EXIT_DB, NULL);
	closedb(dbhandle);
	freedb(dbhandle);
	dbhandle = DBHDL_NULL;
	return(FALSE);
  }
  return(TRUE);
}



/*
 * Close the systems database
 */
void closesysdb __F_NOARGS
{
  if (dbhandle != DBHDL_NULL) {
	closedb(dbhandle);
	freedb(dbhandle);
	dbhandle = DBHDL_NULL;
  }
}



/*
 * Read a system definition out of the systems database
 */
int readsysdb __F((recname, params), char *recname AND struct paramdef *params)
{
  struct sectdef sects[2];

  /* Check if we have a database handle */
  if (dbhandle == DBHDL_NULL) {
	nberror(EXIT_DB, "systems database not open");
	return(FALSE);
  }

  /* Check that the record name is correct */
  if (!check_recname(recname))
	return(FALSE);

  /* Create a section definition */
  memzero(sects, sizeof(sects));
  sects[0].name = recname;
  sects[0].params = params;
  sects[0].startsect = NULL;
  sects[0].endsect = NULL;

  /* Search for record name */
  if (!findfirst(dbhandle, recname)) {
	if (dberrors == 0)
		nberror(EXIT_DB,
			"unable to find record \"%s\" in database",
			recname);
	else
		nberror(EXIT_DB, NULL);
	return(FALSE);
  }

  /* Read all records for this system */
  do {
	if (readrec(dbhandle, sects) != 1) {
		if (dberrors == 0)
			nberror(EXIT_DB,
				"invalid number of records for \"%s\"",
				recname);
		else
			nberror(EXIT_DB, NULL);
		return(FALSE);
	}
  } while (findnext(dbhandle));
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	return(FALSE);
  }
  releasedb(dbhandle);
  return(TRUE);
}



/*
 * Write a system definition into the systems database
 *
 * Writing is a bit more complicated than reading. This is because
 * a database definition may contain multiple databases. We search
 * for the last database which is available for writing and which
 * has the "add" flag.
 */
int writesysdb __F((recname, params), char *recname AND struct paramdef *params)
{
  struct sectdef sects[2];
  int dbstate, update;

  /* Check if we have a database handle */
  if (dbhandle == DBHDL_NULL) {
	nberror(EXIT_DB, "systems database not open");
	return(FALSE);
  }

  /* Check that the record name is correct */
  if (!check_recname(recname))
	return(FALSE);

  /* Create a section definition */
  memzero(sects, sizeof(sects));
  sects[0].name = recname;
  sects[0].params = params;
  sects[0].startsect = NULL;
  sects[0].endsect = NULL;

  /*
   * Delete all records with this name. If we can't delete a record
   * because it's in a read-only database that's not a problem. This
   * delete loop just avoids that a database can grow endlessly.
   * If we found a record in a writable database, mark it's position,
   * and delete all further records. However, if we get to a non-
   * writable entry again, we have to remove the old bookmark.
   * If we have a valid bookmark at the end, we can just update the
   * record with the new data. Otherwise we have to add a new record
   * to the database.
   */
  update = FALSE;
  if (findfirst(dbhandle, recname)) {
	do {
		dbstate = getstatedb(dbhandle, NULL) &
						(DBSTATUS_RO |
						 DBSTATUS_VALID |
						 DBSTATUS_POS);
		if ((dbstate & (DBSTATUS_POS | DBSTATUS_VALID)) ==
					(DBSTATUS_POS | DBSTATUS_VALID)) {
			/* We found a valid database entry */
			if ((dbstate & DBSTATUS_RO) == DBSTATUS_RO) {
				/* The entry is read-only */
				update = FALSE;
			} else {
				/* The entry is writable */
				if (!update)
					update = markrec(dbhandle, FALSE);
				else
					delrec(dbhandle);
				if (dberrors > 0)
					break;
			}
		}
	} while (findnext(dbhandle));
  }
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	return(FALSE);
  }

  /* Update the record if possible */
  if (update) {
	if (markrec(dbhandle, TRUE))
		writerec(dbhandle, sects);
	if (dberrors > 0) {
		nberror(EXIT_DB, NULL);
		return(FALSE);
	}
	goto writeend;
  }

  /* Now find a database to which we can safely add the new record */
  dbstate = 0;
  if (lastrec(dbhandle)) {
	do {
		dbstate = getstatedb(dbhandle, NULL) &
				(DBSTATUS_RO | DBSTATUS_VALID | DBSTATUS_ADD);
		if (dbstate == (DBSTATUS_VALID | DBSTATUS_ADD))
			break;
	} while (prevdb(dbhandle));
  } else
	dbstate = getstatedb(dbhandle, NULL) &
				(DBSTATUS_RO | DBSTATUS_VALID | DBSTATUS_ADD);
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	return(FALSE);
  }

  /* Check that we can add a new record to the current database */
  if (dbstate != (DBSTATUS_VALID | DBSTATUS_ADD)) {
	nberror(EXIT_DB, "unable to find a systems database which allows adding");
	return(FALSE);
  }

  /* Now finally add the new record at the current position */
  addrec(dbhandle, sects, 0);
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	return(FALSE);
  }

  /* Return without error */
writeend:
  releasedb(dbhandle);
  return(TRUE);
}



/*
 * Delete a system definition from the systems database
 *
 */
int delsysdb __F((recname), const char *recname)
{
  int dbstate;

  /* Check if we have a database handle */
  if (dbhandle == DBHDL_NULL) {
	nberror(EXIT_DB, "systems database not open");
	return(FALSE);
  }

  /* Check that the record name is correct */
  if (!check_recname(recname))
	return(FALSE);

  /*
   * Delete all records with this name. If we can't delete a record
   * because it's in a read-only database that's not a problem.
   */
  if (findfirst(dbhandle, recname)) {
	do {
		dbstate = getstatedb(dbhandle, NULL) &
				(DBSTATUS_RO | DBSTATUS_VALID | DBSTATUS_POS);
		if (dbstate == (DBSTATUS_VALID | DBSTATUS_POS)) {
			delrec(dbhandle);
			if (dberrors > 0) {
				nberror(EXIT_DB, NULL);
				return(FALSE);
			}
		}
	} while (findnext(dbhandle));
  }
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	return(FALSE);
  }
  releasedb(dbhandle);
  return(TRUE);
}



/*
 * Compare a record name
 */
static int listcmp __F((handle, data, state),
					LISTHDL handle AND
					voidstar *data AND
					const voidstar state)
{
  return(!strcmp(*((char **)data), (char *)state));
}



/*
 * Get a list of record names from systems database
 */
LISTHDL listsysdb __F((filter), const char *filter)
{
  LISTHDL syslist;
  int dbstate;
  char *recname;

  /* Check if we have a database handle */
  if (dbhandle == DBHDL_NULL) {
	nberror(EXIT_DB, "systems database not open");
	return(FALSE);
  }

  /* Create new linked list */
  syslist = createlist(NULL);

  /* Scan through all database records */
  if (findfirst(dbhandle, filter)) {
	do {
		recname = NULL;
		dbstate = getstatedb(dbhandle, &recname) &
				(DBSTATUS_VALID | DBSTATUS_POS);
		if (recname != NULL) {
			if (dbstate != (DBSTATUS_VALID | DBSTATUS_POS))
				free(recname);
			else if (walklist(syslist, &listcmp, (voidstar)recname) < 0)
				(void)appendlist(syslist, recname);
		}
	} while (findnext(dbhandle));
  }
  if (dberrors > 0) {
	nberror(EXIT_DB, NULL);
	releaselist(syslist);
	return(LISTHDL_NULL);
  }
  releasedb(dbhandle);
  return(syslist);
}

