/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 1996, 2010 Oracle and/or its affiliates.  All rights reserved.
 *
 * $Id$
 */

#include "db_config.h"

#include "db_int.h"
#include "dbinc/db_page.h"
#include "dbinc/db_swap.h"
#include "dbinc/btree.h"
#include "dbinc/hash.h"
#include "dbinc/qam.h"

/*
 * __db_upgrade_pp --
 *	DB->upgrade pre/post processing.
 *
 * PUBLIC: int __db_upgrade_pp __P((DB *, const char *, u_int32_t));
 */
int
__db_upgrade_pp(dbp, fname, flags)
	DB *dbp;
	const char *fname;
	u_int32_t flags;
{
#ifdef HAVE_UPGRADE_SUPPORT
	DB_THREAD_INFO *ip;
	ENV *env;
	int ret;

	env = dbp->env;

	/*
	 * !!!
	 * The actual argument checking is simple, do it inline.
	 */
	if ((ret = __db_fchk(env, "DB->upgrade", flags, DB_DUPSORT)) != 0)
		return (ret);

	ENV_ENTER(env, ip);
	ret = __db_upgrade(dbp, fname, flags);
	ENV_LEAVE(env, ip);
	return (ret);
#else
	COMPQUIET(dbp, NULL);
	COMPQUIET(fname, NULL);
	COMPQUIET(flags, 0);

	__db_errx(dbp->env, "upgrade not supported");
	return (EINVAL);
#endif
}

#ifdef HAVE_UPGRADE_SUPPORT
static int (* const func_31_list[P_PAGETYPE_MAX])
    __P((DB *, char *, u_int32_t, DB_FH *, PAGE *, int *)) = {
	NULL,			/* P_INVALID */
	NULL,			/* __P_DUPLICATE */
	__ham_31_hash,		/* P_HASH_UNSORTED */
	NULL,			/* P_IBTREE */
	NULL,			/* P_IRECNO */
	__bam_31_lbtree,	/* P_LBTREE */
	NULL,			/* P_LRECNO */
	NULL,			/* P_OVERFLOW */
	__ham_31_hashmeta,	/* P_HASHMETA */
	__bam_31_btreemeta,	/* P_BTREEMETA */
	NULL,			/* P_QAMMETA */
	NULL,			/* P_QAMDATA */
	NULL,			/* P_LDUP */
	NULL,			/* P_HASH */
};

static int (* const func_46_list[P_PAGETYPE_MAX])
    __P((DB *, char *, u_int32_t, DB_FH *, PAGE *, int *)) = {
	NULL,			/* P_INVALID */
	NULL,			/* __P_DUPLICATE */
	__ham_46_hash,		/* P_HASH_UNSORTED */
	NULL,			/* P_IBTREE */
	NULL,			/* P_IRECNO */
	NULL,			/* P_LBTREE */
	NULL,			/* P_LRECNO */
	NULL,			/* P_OVERFLOW */
	__ham_46_hashmeta,	/* P_HASHMETA */
	NULL,			/* P_BTREEMETA */
	NULL,			/* P_QAMMETA */
	NULL,			/* P_QAMDATA */
	NULL,			/* P_LDUP */
	NULL,			/* P_HASH */
};

static int __db_page_pass __P((DB *, char *, u_int32_t, int (* const [])
	       (DB *, char *, u_int32_t, DB_FH *, PAGE *, int *), DB_FH *));
static int __db_set_lastpgno __P((DB *, char *, DB_FH *));

/*
 * __db_upgrade --
 *	Upgrade an existing database.
 *
 * PUBLIC: int __db_upgrade __P((DB *, const char *, u_int32_t));
 */
int
__db_upgrade(dbp, fname, flags)
	DB *dbp;
	const char *fname;
	u_int32_t flags;
{
	DBMETA *meta;
	DB_FH *fhp;
	ENV *env;
	size_t n;
	int ret, t_ret, use_mp_open;
	u_int8_t mbuf[256], tmpflags;
	char *real_name;

	use_mp_open = 0;
	env = dbp->env;
	fhp = NULL;

	/* Get the real backing file name. */
	if ((ret = __db_appname(env,
	    DB_APP_DATA, fname, NULL, &real_name)) != 0)
		return (ret);

	/* Open the file. */
	if ((ret = __os_open(env, real_name, 0, 0, 0, &fhp)) != 0) {
		__db_err(env, ret, "%s", real_name);
		return (ret);
	}

	/* Initialize the feedback. */
	if (dbp->db_feedback != NULL)
		dbp->db_feedback(dbp, DB_UPGRADE, 0);

	/*
	 * Read the metadata page.  We read 256 bytes, which is larger than
	 * any access method's metadata page and smaller than any disk sector.
	 */
	if ((ret = __os_read(env, fhp, mbuf, sizeof(mbuf), &n)) != 0)
		goto err;

	switch (((DBMETA *)mbuf)->magic) {
	case DB_BTREEMAGIC:
		switch (((DBMETA *)mbuf)->version) {
		case 6:
			/*
			 * Before V7 not all pages had page types, so we do the
			 * single meta-data page by hand.
			 */
			if ((ret =
			    __bam_30_btreemeta(dbp, real_name, mbuf)) != 0)
				goto err;
			if ((ret = __os_seek(env, fhp, 0, 0, 0)) != 0)
				goto err;
			if ((ret = __os_write(env, fhp, mbuf, 256, &n)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 7:
			/*
			 * We need the page size to do more.  Rip it out of
			 * the meta-data page.
			 */
			memcpy(&dbp->pgsize, mbuf + 20, sizeof(u_int32_t));

			if ((ret = __db_page_pass(
			    dbp, real_name, flags, func_31_list, fhp)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 8:
			if ((ret =
			     __db_set_lastpgno(dbp, real_name, fhp)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 9:
			break;
		default:
			__db_errx(env, "%s: unsupported btree version: %lu",
			    real_name, (u_long)((DBMETA *)mbuf)->version);
			ret = DB_OLD_VERSION;
			goto err;
		}
		break;
	case DB_HASHMAGIC:
		switch (((DBMETA *)mbuf)->version) {
		case 4:
		case 5:
			/*
			 * Before V6 not all pages had page types, so we do the
			 * single meta-data page by hand.
			 */
			if ((ret =
			    __ham_30_hashmeta(dbp, real_name, mbuf)) != 0)
				goto err;
			if ((ret = __os_seek(env, fhp, 0, 0, 0)) != 0)
				goto err;
			if ((ret = __os_write(env, fhp, mbuf, 256, &n)) != 0)
				goto err;

			/*
			 * Before V6, we created hash pages one by one as they
			 * were needed, using hashhdr.ovfl_point to reserve
			 * a block of page numbers for them.  A consequence
			 * of this was that, if no overflow pages had been
			 * created, the current doubling might extend past
			 * the end of the database file.
			 *
			 * In DB 3.X, we now create all the hash pages
			 * belonging to a doubling atomically; it's not
			 * safe to just save them for later, because when
			 * we create an overflow page we'll just create
			 * a new last page (whatever that may be).  Grow
			 * the database to the end of the current doubling.
			 */
			if ((ret =
			    __ham_30_sizefix(dbp, fhp, real_name, mbuf)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 6:
			/*
			 * We need the page size to do more.  Rip it out of
			 * the meta-data page.
			 */
			memcpy(&dbp->pgsize, mbuf + 20, sizeof(u_int32_t));

			if ((ret = __db_page_pass(
			    dbp, real_name, flags, func_31_list, fhp)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 7:
			if ((ret =
			     __db_set_lastpgno(dbp, real_name, fhp)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 8:
			/*
			 * Any upgrade that has proceeded this far has metadata
			 * pages compatible with hash version 8 metadata pages,
			 * so casting mbuf to a dbmeta is safe.
			 * If a newer revision moves the pagesize, checksum or
			 * encrypt_alg flags in the metadata, then the
			 * extraction of the fields will need to use hard coded
			 * offsets.
			 */
			meta = (DBMETA*)mbuf;
			/*
			 * We need the page size to do more.  Extract it from
			 * the meta-data page.
			 */
			memcpy(&dbp->pgsize, &meta->pagesize,
			    sizeof(u_int32_t));
			/*
			 * Rip out metadata and encrypt_alg fields from the
			 * metadata page. So the upgrade can know how big
			 * the page metadata pre-amble is. Any upgrade that has
			 * proceeded this far has metadata pages compatible
			 * with hash version 8 metadata pages, so extracting
			 * the fields is safe.
			 */
			memcpy(&tmpflags, &meta->metaflags, sizeof(u_int8_t));
			if (FLD_ISSET(tmpflags, DBMETA_CHKSUM))
				F_SET(dbp, DB_AM_CHKSUM);
			memcpy(&tmpflags, &meta->encrypt_alg, sizeof(u_int8_t));
			if (tmpflags != 0) {
				if (!CRYPTO_ON(dbp->env)) {
					__db_errx(env,
"Attempt to upgrade an encrypted database without providing a password.");
					ret = EINVAL;
					goto err;
				}
				F_SET(dbp, DB_AM_ENCRYPT);
			}

			/*
			 * This is ugly. It is necessary to have a usable
			 * mpool in the dbp to upgrade from an unsorted
			 * to a sorted hash database. The mpool file is used
			 * to resolve offpage key items, which are needed to
			 * determine sort order. Having mpool open and access
			 * the file does not affect the page pass, since the
			 * page pass only updates DB_HASH_UNSORTED pages
			 * in-place, and the mpool file is only used to read
			 * OFFPAGE items.
			 */
			use_mp_open = 1;
			if ((ret = __os_closehandle(env, fhp)) != 0)
				return (ret);
			dbp->type = DB_HASH;
			if ((ret = __env_mpool(dbp, fname,
			    DB_AM_NOT_DURABLE | DB_AM_VERIFYING)) != 0)
				return (ret);
			fhp = dbp->mpf->fhp;

			/* Do the actual conversion pass. */
			if ((ret = __db_page_pass(
			    dbp, real_name, flags, func_46_list, fhp)) != 0)
				goto err;

			/* FALLTHROUGH */
		case 9:
			break;
		default:
			__db_errx(env, "%s: unsupported hash version: %lu",
			    real_name, (u_long)((DBMETA *)mbuf)->version);
			ret = DB_OLD_VERSION;
			goto err;
		}
		break;
	case DB_QAMMAGIC:
		switch (((DBMETA *)mbuf)->version) {
		case 1:
			/*
			 * If we're in a Queue database, the only page that
			 * needs upgrading is the meta-database page, don't
			 * bother with a full pass.
			 */
			if ((ret = __qam_31_qammeta(dbp, real_name, mbuf)) != 0)
				return (ret);
			/* FALLTHROUGH */
		case 2:
			if ((ret = __qam_32_qammeta(dbp, real_name, mbuf)) != 0)
				return (ret);
			if ((ret = __os_seek(env, fhp, 0, 0, 0)) != 0)
				goto err;
			if ((ret = __os_write(env, fhp, mbuf, 256, &n)) != 0)
				goto err;
			/* FALLTHROUGH */
		case 3:
		case 4:
			break;
		default:
			__db_errx(env, "%s: unsupported queue version: %lu",
			    real_name, (u_long)((DBMETA *)mbuf)->version);
			ret = DB_OLD_VERSION;
			goto err;
		}
		break;
	default:
		M_32_SWAP(((DBMETA *)mbuf)->magic);
		switch (((DBMETA *)mbuf)->magic) {
		case DB_BTREEMAGIC:
		case DB_HASHMAGIC:
		case DB_QAMMAGIC:
			__db_errx(env,
		"%s: DB->upgrade only supported on native byte-order systems",
			    real_name);
			break;
		default:
			__db_errx(env,
			    "%s: unrecognized file type", real_name);
			break;
		}
		ret = EINVAL;
		goto err;
	}

	ret = __os_fsync(env, fhp);

	/*
	 * If mp_open was used, then rely on the database close to clean up
	 * any file handles.
	 */
err:	if (use_mp_open == 0 && fhp != NULL &&
	    (t_ret = __os_closehandle(env, fhp)) != 0 && ret == 0)
		ret = t_ret;
	__os_free(env, real_name);

	/* We're done. */
	if (dbp->db_feedback != NULL)
		dbp->db_feedback(dbp, DB_UPGRADE, 100);

	return (ret);
}

/*
 * __db_page_pass --
 *	Walk the pages of the database, upgrading whatever needs it.
 */
static int
__db_page_pass(dbp, real_name, flags, fl, fhp)
	DB *dbp;
	char *real_name;
	u_int32_t flags;
	int (* const fl[P_PAGETYPE_MAX])
	    __P((DB *, char *, u_int32_t, DB_FH *, PAGE *, int *));
	DB_FH *fhp;
{
	ENV *env;
	PAGE *page;
	db_pgno_t i, pgno_last;
	size_t n;
	int dirty, ret;

	env = dbp->env;

	/* Determine the last page of the file. */
	if ((ret = __db_lastpgno(dbp, real_name, fhp, &pgno_last)) != 0)
		return (ret);

	/* Allocate memory for a single page. */
	if ((ret = __os_malloc(env, dbp->pgsize, &page)) != 0)
		return (ret);

	/* Walk the file, calling the underlying conversion functions. */
	for (i = 0; i < pgno_last; ++i) {
		if (dbp->db_feedback != NULL)
			dbp->db_feedback(
			    dbp, DB_UPGRADE, (int)((i * 100)/pgno_last));
		if ((ret = __os_seek(env, fhp, i, dbp->pgsize, 0)) != 0)
			break;
		if ((ret = __os_read(env, fhp, page, dbp->pgsize, &n)) != 0)
			break;
		dirty = 0;
		/* Always decrypt the page. */
		if ((ret = __db_decrypt_pg(env, dbp, page)) != 0)
			break;
		if (fl[TYPE(page)] != NULL && (ret = fl[TYPE(page)]
		    (dbp, real_name, flags, fhp, page, &dirty)) != 0)
			break;
		if (dirty) {
			if ((ret = __db_encrypt_and_checksum_pg(
			    env, dbp, page)) != 0)
				break;
			if ((ret =
			    __os_seek(env, fhp, i, dbp->pgsize, 0)) != 0)
				break;
			if ((ret = __os_write(env,
			    fhp, page, dbp->pgsize, &n)) != 0)
				break;
		}
	}

	__os_free(dbp->env, page);
	return (ret);
}

/*
 * __db_lastpgno --
 *	Return the current last page number of the file.
 *
 * PUBLIC: int __db_lastpgno __P((DB *, char *, DB_FH *, db_pgno_t *));
 */
int
__db_lastpgno(dbp, real_name, fhp, pgno_lastp)
	DB *dbp;
	char *real_name;
	DB_FH *fhp;
	db_pgno_t *pgno_lastp;
{
	ENV *env;
	db_pgno_t pgno_last;
	u_int32_t mbytes, bytes;
	int ret;

	env = dbp->env;

	if ((ret = __os_ioinfo(env,
	    real_name, fhp, &mbytes, &bytes, NULL)) != 0) {
		__db_err(env, ret, "%s", real_name);
		return (ret);
	}

	/* Page sizes have to be a power-of-two. */
	if (bytes % dbp->pgsize != 0) {
		__db_errx(env,
		    "%s: file size not a multiple of the pagesize", real_name);
		return (EINVAL);
	}
	pgno_last = mbytes * (MEGABYTE / dbp->pgsize);
	pgno_last += bytes / dbp->pgsize;

	*pgno_lastp = pgno_last;
	return (0);
}

/*
 * __db_set_lastpgno --
 *	Update the meta->last_pgno field.
 *
 * Code assumes that we do not have checksums/crypto on the page.
 */
static int
__db_set_lastpgno(dbp, real_name, fhp)
	DB *dbp;
	char *real_name;
	DB_FH *fhp;
{
	DBMETA meta;
	ENV *env;
	int ret;
	size_t n;

	env = dbp->env;
	if ((ret = __os_seek(env, fhp, 0, 0, 0)) != 0)
		return (ret);
	if ((ret = __os_read(env, fhp, &meta, sizeof(meta), &n)) != 0)
		return (ret);
	dbp->pgsize = meta.pagesize;
	if ((ret = __db_lastpgno(dbp, real_name, fhp, &meta.last_pgno)) != 0)
		return (ret);
	if ((ret = __os_seek(env, fhp, 0, 0, 0)) != 0)
		return (ret);
	if ((ret = __os_write(env, fhp, &meta, sizeof(meta), &n)) != 0)
		return (ret);

	return (0);
}
#endif /* HAVE_UPGRADE_SUPPORT */
