/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 1996-2001
 *	Sleepycat Software.  All rights reserved.
 */
#include "db_config.h"

#ifndef lint
static const char revid[] = "$Id: dbreg.c,v 11.48 2001/12/10 16:00:23 bostic Exp $";
#endif /* not lint */

#ifndef NO_SYSTEM_INCLUDES
#include <sys/types.h>

#include <string.h>
#endif

#include "db_int.h"
#include "log.h"

#define	LOGREG_RECOVER(dblp)				\
    (F_ISSET((dblp), DBLOG_RECOVER) || LF_ISSET(DB_APPLY_DBREG))

/*
 * __dbreg_register --
 *	Register a file name.
 *
 * PUBLIC: int __dbreg_register __P((DB_ENV *, DB *, const char *));
 */
int
__dbreg_register(dbenv, dbp, name)
	DB_ENV *dbenv;
	DB *dbp;
	const char *name;
{

	return (__dbreg_register_int(dbenv, dbp, name, 0));
}

/*
 * __dbreg_register_int --
 *	Internal log_register function.
 *
 * PUBLIC: int __dbreg_register_int __P((DB_ENV *,
 * PUBLIC:     DB *, const char *, u_int32_t));
 */
int
__dbreg_register_int(dbenv, dbp, name, flags)
	DB_ENV *dbenv;
	DB *dbp;
	const char *name;
	u_int32_t flags;
{
	DBT fid_dbt, r_name;
	DB_LOG *dblp;
	DB_LSN r_unused;
	FNAME *alloc_fnp, *found_fnp, *fnp, *recover_fnp, *reuse_fnp;
	LOG *lp;
	size_t len;
	int32_t maxid;
	int inserted, ok, ret;
	void *namep;

	PANIC_CHECK(dbenv);
	ENV_REQUIRES_CONFIG(dbenv,
	    dbenv->lg_handle, "__dbreg_register", DB_INIT_LOG);

	dblp = dbenv->lg_handle;
	lp = dblp->reginfo.primary;
	alloc_fnp = fnp = reuse_fnp = NULL;
	inserted = ret = 0;
	namep = NULL;

	/* Check the arguments. */
	if (dbp->type != DB_BTREE && dbp->type != DB_QUEUE &&
	    dbp->type != DB_HASH && dbp->type != DB_RECNO) {
		__db_err(dbenv, "__dbreg_register: unknown DB file type");
		return (EINVAL);
	}

	MUTEX_LOCK(dbenv, &lp->fq_mutex);

	/*
	 * See if we've already got this file in the log, finding the
	 * (maximum+1) in-use file id and some available file id (if we
	 * find an available fid, we'll use it, else we'll have to allocate
	 * one after the maximum that we found).
	 */
	ok = 0;
	found_fnp = recover_fnp = NULL;
	for (maxid = 0, fnp = SH_TAILQ_FIRST(&lp->fq, __fname);
	    fnp != NULL; fnp = SH_TAILQ_NEXT(fnp, q, __fname)) {
		if (LOGREG_RECOVER(dblp) && fnp->id == dbp->log_fileid)
			recover_fnp = fnp;
		if (fnp->ref == 0) {		/* Entry is not in use. */
			if (reuse_fnp == NULL)
				reuse_fnp = fnp;
			continue;
		}
		if (memcmp(dbp->fileid, fnp->ufid, DB_FILE_ID_LEN) == 0) {
			if (fnp->meta_pgno == 0) {
				if (fnp->locked == 1) {
					__db_err(dbenv, "File is locked");
					ret = EINVAL;
					goto err;
				}
				if (found_fnp != NULL) {
					fnp = found_fnp;
					goto found;
				}
				ok = 1;
			}
			if (dbp->meta_pgno == fnp->meta_pgno) {
				if (LOGREG_RECOVER(dblp)) {
					if (fnp->id != dbp->log_fileid) {
						/*
						 * If we are in recovery, there
						 * is only one dbp on the list.
						 * If the refcount goes to 0,
						 * we will clear the list.  If
						 * it doesn't, we want to leave
						 * the dbp where it is, so
						 * passing a NULL to rem_logid
						 * is correct.
						 */
						__dbreg_rem_logid(dblp,
						    NULL, fnp->id);
						if (recover_fnp != NULL)
							break;
						continue;
					}
					fnp->ref = 1;
					goto found;
				}
				++fnp->ref;
				if (ok)
					goto found;
				found_fnp = fnp;
			}
		}
		if (maxid <= fnp->id)
			maxid = fnp->id + 1;
	}
	if ((fnp = found_fnp) != NULL)
		goto found;

	/* Fill in fnp structure. */
	if (recover_fnp != NULL)	/* This has the right number */
		fnp = recover_fnp;
	else if (reuse_fnp != NULL)	/* Reuse existing one. */
		fnp = reuse_fnp;
	else {				/* Allocate a new one. */
		R_LOCK(dbenv, &dblp->reginfo);
		ret = __db_shalloc(dblp->reginfo.addr,
		    sizeof(FNAME), 0, &alloc_fnp);
		R_UNLOCK(dbenv, &dblp->reginfo);
		if (ret != 0)
			goto mem_err;
		fnp = alloc_fnp;
		fnp->id = maxid;
	}

	if (LOGREG_RECOVER(dblp))
		fnp->id = dbp->log_fileid;

	fnp->ref = 1;
	fnp->locked = 0;
	fnp->s_type = dbp->type;
	memcpy(fnp->ufid, dbp->fileid, DB_FILE_ID_LEN);
	fnp->meta_pgno = dbp->meta_pgno;

	if (name != NULL) {
		len = strlen(name) + 1;
		R_LOCK(dbenv, &dblp->reginfo);
		ret = __db_shalloc(dblp->reginfo.addr, len, 0, &namep);
		R_UNLOCK(dbenv, &dblp->reginfo);
		if (ret != 0) {
mem_err:		__db_err(dbenv,
			    "Unable to allocate memory to register %s", name);
			goto err;
		}
		fnp->name_off = R_OFFSET(&dblp->reginfo, namep);
		memcpy(namep, name, len);
	} else
		fnp->name_off = INVALID_ROFF;

	/* Only do the insert if we allocated a new fnp. */
	if (reuse_fnp == NULL && recover_fnp == NULL)
		SH_TAILQ_INSERT_HEAD(&lp->fq, fnp, q, __fname);
	inserted = 1;

	/* Log the registry. */
	if (!LOGREG_RECOVER(dblp) && !F_ISSET(dbenv, DB_ENV_REP_CLIENT)) {
		/*
		 * We allow logging on in-memory databases, so the name here
		 * could be NULL.
		 */
		if (name != NULL) {
			r_name.data = (void *)name;
			r_name.size = strlen(name) + 1;
		}
		memset(&fid_dbt, 0, sizeof(fid_dbt));
		fid_dbt.data = dbp->fileid;
		fid_dbt.size = DB_FILE_ID_LEN;
		if ((ret = __dbreg_register_log(dbenv, NULL, &r_unused,
		    0, LOG_OPEN, name == NULL ? NULL : &r_name,
		    &fid_dbt, fnp->id, dbp->type, dbp->meta_pgno)) != 0)
			goto err;
	}

found:	/*
	 * If we found the entry in the shared area, then the file is
	 * already open, so there is no need to log the open.  We only
	 * log the open and closes on the first open and last close.
	 */
	if (!LOGREG_RECOVER(dblp) &&
	    (ret = __dbreg_add_logid(dbenv, dblp, dbp, fnp->id)) != 0)
			goto err;

	if (!LOGREG_RECOVER(dblp))
		dbp->log_fileid = fnp->id;

	if (0) {
err:		if (inserted)
			SH_TAILQ_REMOVE(&lp->fq, fnp, q, __fname);
		if (namep != NULL || alloc_fnp != NULL) {
			R_LOCK(dbenv, &dblp->reginfo);
			if (namep != NULL)
				__db_shalloc_free(dblp->reginfo.addr, namep);
			if (alloc_fnp != NULL)
				__db_shalloc_free(dblp->reginfo.addr,
				    alloc_fnp);
			R_UNLOCK(dbenv, &dblp->reginfo);
		}
	}

	MUTEX_UNLOCK(dbenv, &lp->fq_mutex);

	return (ret);
}

/*
 * __dbreg_unregister --
 *	Discard a registered file name.
 *
 * PUBLIC: int __dbreg_unregister __P((DB_ENV *, DB *));
 */
int
__dbreg_unregister(dbenv, dbp)
	DB_ENV *dbenv;
	DB *dbp;
{
	int ret;

	PANIC_CHECK(dbenv);
	ENV_REQUIRES_CONFIG(dbenv,
	    dbenv->lg_handle, "DB_ENV->log_unregister", DB_INIT_LOG);

	ret = __dbreg_filelist_update(dbenv, dbp, dbp->log_fileid, NULL, NULL);
	dbp->log_fileid = DB_LOGFILEID_INVALID;
	return (ret);
}

/*
 * PUBLIC: int __dbreg_filelist_update
 * PUBLIC:    __P((DB_ENV *, DB *, int32_t, const char *, int *));
 *
 *  Utility player for updating and logging the file list.  Called
 *  for 3 reasons:
 *	1) mark file closed: newname == NULL.
 *	2) change filename: newname != NULL.
 *	3) from recovery to verify & change filename if necessary, set != NULL.
 */
int
__dbreg_filelist_update(dbenv, dbp, fid, newname, set)
	DB_ENV *dbenv;
	DB *dbp;
	int32_t fid;
	const char *newname;
	int *set;
{
	DBT fid_dbt, r_name;
	DB_LOG *dblp;
	DB_LSN r_unused;
	FNAME *fnp;
	LOG *lp;
	u_int32_t len, newlen;
	int ret;
	void *namep;

	ret = 0;
	dblp = dbenv->lg_handle;
	lp = dblp->reginfo.primary;

	MUTEX_LOCK(dbenv, &lp->fq_mutex);

	/* Find the entry in the log. */
	for (fnp = SH_TAILQ_FIRST(&lp->fq, __fname);
	    fnp != NULL; fnp = SH_TAILQ_NEXT(fnp, q, __fname))
		if (fid == fnp->id)
			break;
	if (fnp == NULL) {
		__db_err(dbenv, "DB_ENV->log_unregister: non-existent file id");
		ret = EINVAL;
		goto ret1;
	}

	/*
	 * Log the unregistry only if this is the last one and we are
	 * really closing the file or if this is an abort of a created
	 * file and we need to make sure there is a record in the log.
	 */
	namep = NULL;
	len = 0;
	if (fnp->name_off != INVALID_ROFF) {
		namep = R_ADDR(&dblp->reginfo, fnp->name_off);
		len = strlen(namep) + 1;
	}
	if (!F_ISSET(dbenv, DB_ENV_REP_CLIENT) &&
	    !F_ISSET(dblp, DBLOG_RECOVER) && fnp->ref == 1) {
		if (namep != NULL) {
			memset(&r_name, 0, sizeof(r_name));
			r_name.data = namep;
			r_name.size = len;
		}
		memset(&fid_dbt, 0, sizeof(fid_dbt));
		fid_dbt.data = fnp->ufid;
		fid_dbt.size = DB_FILE_ID_LEN;
		if ((ret = __dbreg_register_log(dbenv, NULL, &r_unused,
		    0, LOG_CLOSE,
		    fnp->name_off == INVALID_ROFF ? NULL : &r_name,
		    &fid_dbt, fid, fnp->s_type, fnp->meta_pgno))
		    != 0)
			goto ret1;
	}

	/*
	 * If we are changing the name we must log this fact.
	 */
	if (newname != NULL && !F_ISSET(dbenv, DB_ENV_REP_CLIENT)) {
		DB_ASSERT(fnp->ref == 1);
		newlen = strlen(newname) + 1;
		if (!F_ISSET(dblp, DBLOG_RECOVER)) {
			r_name.data = (void *) newname;
			r_name.size = newlen;
			if ((ret = __dbreg_register_log(dbenv,
			    NULL, &r_unused, 0, LOG_OPEN, &r_name, &fid_dbt,
			    fnp->id, fnp->s_type, fnp->meta_pgno)) != 0)
				goto ret1;
		}

		/*
		 * Check to see if the name is already correct.
		 */
		if (set != NULL) {
			if (len != newlen || memcmp(namep, newname, len) != 0)
				*set = 1;
			else {
				*set = 0;
				goto ret1;
			}
		}

		/*
		 * Change the name, realloc memory if necessary
		 */
		if (len < newlen) {
			__db_shalloc_free(dblp->reginfo.addr,
			    R_ADDR(&dblp->reginfo, fnp->name_off));
			if ((ret = __db_shalloc(
			    dblp->reginfo.addr, newlen, 0, &namep)) != 0) {
				__db_err(dbenv,
				    "Unable to allocate memory to register %s",
				    newname);
				goto ret1;
			}
			fnp->name_off = R_OFFSET(&dblp->reginfo, namep);
		} else
			namep = R_ADDR(&dblp->reginfo, fnp->name_off);
		memcpy(namep, newname, newlen);
	} else {

		/*
		 * If more than 1 reference, just decrement the reference
		 * and return.  Otherwise, free the name if one exists.
		 */
		DB_ASSERT(fnp->ref >= 1);
		--fnp->ref;
		if (fnp->ref == 0) {
			if (fnp->name_off != INVALID_ROFF)
				__db_shalloc_free(dblp->reginfo.addr,
				    R_ADDR(&dblp->reginfo, fnp->name_off));
			fnp->name_off = INVALID_ROFF;
		}

		/*
		 * Remove from the process local table.  If this
		 * operation is taking place during recovery, then
		 * the logid was never added to the table, so do not remove it.
		 */
		if (!F_ISSET(dblp, DBLOG_RECOVER))
			__dbreg_rem_logid(dblp, dbp, fid);
	}

ret1:	MUTEX_UNLOCK(dbenv, &lp->fq_mutex);
	return (ret);
}

/*
 * __dbreg_file_lock -- lock a file for single access
 *	This only works if logging is on.
 *
 * PUBLIC: int __dbreg_file_lock __P((DB *));
 */
int
__dbreg_file_lock(dbp)
	DB *dbp;
{
	DB_ENV *dbenv;
	DB_LOG *dblp;
	FNAME *fnp;
	LOG *lp;
	int ret;

	dbenv = dbp->dbenv;
	dblp = dbenv->lg_handle;
	lp = dblp->reginfo.primary;

	ret = 0;
	MUTEX_LOCK(dbenv, &lp->fq_mutex);

	for (fnp = SH_TAILQ_FIRST(&lp->fq, __fname);
	    fnp != NULL; fnp = SH_TAILQ_NEXT(fnp, q, __fname)) {
		if (fnp->ref == 0)
			continue;

		if (!memcmp(dbp->fileid, fnp->ufid, DB_FILE_ID_LEN)) {
			/*
			 * Lock our file.
			 * If we are a master db no sub-db can be open either.
			 */
			if (fnp->meta_pgno == dbp->meta_pgno) {
				if (fnp->ref != 1)
					goto err;

				fnp->locked = 1;
			} else if (dbp->meta_pgno == 0) {
err:				__db_err(dbp->dbenv, "File is open");
				ret = EINVAL;
				goto done;
			}
		}
	}
done:	MUTEX_UNLOCK(dbenv, &lp->fq_mutex);
	return (ret);
}

/*
 * __dbreg_open_files --
 *	Put a LOG_CHECKPOINT log record for each open database.
 *
 * PUBLIC: int __dbreg_open_files __P((DB_ENV *));
 */
int
__dbreg_open_files(dbenv)
	DB_ENV *dbenv;
{
	DB_LOG *dblp;
	DB_LSN r_unused;
	DBT fid_dbt, t;
	FNAME *fnp;
	LOG *lp;
	int ret;

	dblp = dbenv->lg_handle;
	lp = dblp->reginfo.primary;

	ret = 0;

	MUTEX_LOCK(dbenv, &lp->fq_mutex);

	for (fnp = SH_TAILQ_FIRST(&lp->fq, __fname);
	    fnp != NULL; fnp = SH_TAILQ_NEXT(fnp, q, __fname)) {
		if (fnp->ref == 0)	/* Entry not in use. */
			continue;
		if (fnp->name_off != INVALID_ROFF) {
			memset(&t, 0, sizeof(t));
			t.data = R_ADDR(&dblp->reginfo, fnp->name_off);
			t.size = strlen(t.data) + 1;
		}
		memset(&fid_dbt, 0, sizeof(fid_dbt));
		fid_dbt.data = fnp->ufid;
		fid_dbt.size = DB_FILE_ID_LEN;
		/*
		 * Output LOG_CHECKPOINT records which will be
		 * processed during the OPENFILES pass of recovery.
		 * At the end of recovery we want to output the
		 * files that were open so that a future recovery
		 * run will have the correct files open during
		 * a backward pass.  For this we output LOG_RCLOSE
		 * records so that the files will be closed on
		 * the forward pass.
		 */
		if ((ret = __dbreg_register_log(dbenv,
		    NULL, &r_unused, 0,
		    F_ISSET(dblp, DBLOG_RECOVER) ? LOG_RCLOSE : LOG_CHECKPOINT,
		    fnp->name_off == INVALID_ROFF ? NULL : &t,
		    &fid_dbt, fnp->id, fnp->s_type, fnp->meta_pgno)) != 0)
			break;
	}

	MUTEX_UNLOCK(dbenv, &lp->fq_mutex);

	return (ret);
}
