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

#include "db_config.h"

#include "db_int.h"
#include "dbinc/db_page.h"
#include "dbinc/db_am.h"
#include "dbinc/fop.h"

static int __db_stream_close __P((DB_STREAM *, u_int32_t));
static int __db_stream_read
    __P((DB_STREAM *, DBT *, db_off_t, u_int32_t, u_int32_t));
static int __db_stream_size __P((DB_STREAM *, db_off_t *, u_int32_t));
static int __db_stream_write __P((DB_STREAM *, DBT *, db_off_t, u_int32_t));

/*
 * __db_stream_init
 *	DB_STREAM initializer.
 *
 * PUBLIC: int __db_stream_init __P((DBC *, DB_STREAM **, u_int32_t));
 */
int
__db_stream_init(dbc, dbsp, flags)
	DBC *dbc;
	DB_STREAM **dbsp;
	u_int32_t flags;
{
	DB_STREAM *dbs;
	DB_THREAD_INFO *ip;
	ENV *env;
	int ret;
	off_t size;

	dbs = NULL;
	env = dbc->env;

	if ((ret = __os_malloc(env, sizeof(DB_STREAM), &dbs)) != 0)
		return (ret);
	memset(dbs, 0, sizeof(DB_STREAM));

	ENV_ENTER(env, ip);
	/* Should the copy be transient? */
	if ((ret = __dbc_idup(dbc, &dbs->dbc, DB_POSITION)) != 0)
		goto err;
	dbs->flags = flags;

	/*
	 * Make sure we have a write lock on the db record if writing
	 * to the blob.
	 */
	if (F_ISSET(dbs, DB_FOP_WRITE))
		F_SET(dbc, DBC_RMW);

	if ((ret = __dbc_get_blob_id(dbs->dbc, &dbs->blob_id)) != 0) {
		if (ret == EINVAL)
			__db_errx(env, DB_STR("0211",
		    "Error, cursor does not point to an external file."));
		goto err;
	}

	if ((ret = __dbc_get_blob_size(dbs->dbc, &size)) != 0)
		goto err;
	dbs->file_size = size;

	if ((ret = __blob_file_open(
	    dbs->dbc->dbp, &dbs->fhp, dbs->blob_id, flags, 1)) != 0)
		goto err;
	ENV_LEAVE(env, ip);

	dbs->close = __db_stream_close;
	dbs->read = __db_stream_read;
	dbs->size = __db_stream_size;
	dbs->write = __db_stream_write;

	*dbsp = dbs;
	return (0);

err:	if (dbs != NULL && dbs->dbc != NULL)
		(void)__dbc_close(dbs->dbc);
	ENV_LEAVE(env, ip);
	if (dbs != NULL)
		__os_free(env, dbs);
	return (ret);
}

/*
 * __db_stream_close --
 *
 * DB_STREAM->close
 */
static int
__db_stream_close(dbs, flags)
	DB_STREAM *dbs;
	u_int32_t flags;
{
	DB_THREAD_INFO *ip;
	ENV *env;
	int ret;

	env = dbs->dbc->env;

	if ((ret = __db_fchk(env, "DB_STREAM->close", flags, 0)) != 0)
		return (ret);

	ENV_ENTER(env, ip);

	ret = __db_stream_close_int(dbs);

	ENV_LEAVE(env, ip);

	return (ret);
}

/*
 * __db_stream_close_int --
 *	Close a DB_STREAM object.
 *
 * PUBLIC: int __db_stream_close_int __P ((DB_STREAM *));
 */
int
__db_stream_close_int(dbs)
	DB_STREAM *dbs;
{
	DBC *dbc;
	ENV *env;
	int ret, t_ret;

	dbc = dbs->dbc;
	env = dbc->env;

	ret = __blob_file_close(dbc, dbs->fhp, dbs->flags);

	if ((t_ret = __dbc_close(dbs->dbc)) != 0 && ret == 0)
		ret = t_ret;

	__os_free(env, dbs);

	return (ret);
}

/*
 * __db_stream_read --
 *
 * DB_STREAM->read
 */
static int
__db_stream_read(dbs, data, offset, size, flags)
	DB_STREAM *dbs;
	DBT *data;
	db_off_t offset;
	u_int32_t size;
	u_int32_t flags;
{
	DBC *dbc;
	ENV *env;
	int ret;
	u_int32_t needed, start;

	dbc = dbs->dbc;
	env = dbc->dbp->env;
	ret = 0;

	if ((ret = __db_fchk(env, "DB_STREAM->read", flags, 0)) != 0)
		return (ret);

	if (F_ISSET(data, DB_DBT_PARTIAL)) {
		ret = USR_ERR(env, EINVAL);
		__db_errx(env, DB_STR("0212",
		    "Error, do not use DB_DBT_PARTIAL with DB_STREAM."));
		goto err;
	}

	if (offset > dbs->file_size) {
		data->size = 0;
		goto err;
	}

	if ((ret = __db_alloc_dbt(
	    env, data, size, &needed, &start, NULL, NULL)) != 0)
		goto err;
	data->size = needed;

	if (needed == 0)
		goto err;

	ret = __blob_file_read(env, dbs->fhp, data, offset, size);

err:	return (ret);
}

/*
 * __db_stream_size --
 *
 * DB_STREAM->size
 */
static int
__db_stream_size(dbs, size, flags)
	DB_STREAM *dbs;
	db_off_t *size;
	u_int32_t flags;
{
	int ret;

	if ((ret = __db_fchk(dbs->dbc->env, "DB_STREAM->size", flags, 0)) != 0)
		return (ret);

	*size = dbs->file_size;

	return (0);
}

/*
 * __db_stream_write --
 *
 * DB_STREAM->write
 */
static int
__db_stream_write(dbs, data, offset, flags)
	DB_STREAM *dbs;
	DBT *data;
	db_off_t offset;
	u_int32_t flags;
{
	DB_THREAD_INFO *ip;
	ENV *env;
	int ret;
	off_t file_size;
	u_int32_t wflags;

	env = dbs->dbc->env;

	if ((ret = __db_fchk(
	    env, "DB_STREAM->write", flags, DB_STREAM_SYNC_WRITE)) != 0)
		return (ret);

	if (F_ISSET(dbs, DB_FOP_READONLY)) {
		ret = USR_ERR(env, EINVAL);
		__db_errx(env, DB_STR("0213",
		    "Error, external file is read only."));
		return (ret);
	}
	if (F_ISSET(data, DB_DBT_PARTIAL)) {
		ret = USR_ERR(env, EINVAL);
		__db_errx(env, DB_STR("0214",
		    "Error, do not use DB_DBT_PARTIAL with DB_STREAM."));
		return (ret);
	}
	if (offset < 0 ) {
		ret = USR_ERR(env, EINVAL);
		__db_errx(env, DB_STR_A("0215",
		    "Error, invalid offset value: %lld", "%lld"),
		    (long long)offset);
		return (ret);
	}
	/*
	 * Catch offset overflow. After the above test, offset >= 0 is true, and
	 * DB_OFF_T_MAX is defined to be the maximum signed integer of the same
	 * size as (db_off_t), therefore (DB_OFF_T_MAX - offset) >= 0 must also
	 * be true and it cannot cause an overflow. data->size is an unsigned
	 * 32-bit integer, while offset is a signed 32-bit or 64-bit integer, so
	 * the only safe way to compare the two is to promote both to
	 * unsigned 64-bit integers.
	 */
	if ((u_int64_t)data->size > (u_int64_t)(DB_OFF_T_MAX - offset)) {
		ret = USR_ERR(env, EINVAL);
		__db_errx(env, DB_STR_A("0216",
    "Error, this write would exceed the maximum external file size: %lu %lld",
		"%lu %lld"), (u_long)data->size, (long long)offset);
		return (ret);
	}

	ENV_ENTER(env, ip);
	wflags = 0;
	if (LF_ISSET(DB_STREAM_SYNC_WRITE) || F_ISSET(dbs, DB_FOP_SYNC_WRITE))
		wflags |= DB_FOP_SYNC_WRITE;
	file_size = dbs->file_size;
	if ((ret = __blob_file_write(dbs->dbc, dbs->fhp,
	    data, offset, dbs->blob_id, &file_size, wflags)) != 0)
		goto err;
	if (file_size != dbs->file_size) {
		dbs->file_size = file_size;
		if ((ret = __dbc_set_blob_size(dbs->dbc, dbs->file_size)) != 0)
			goto err;
	}
err:	ENV_LEAVE(env, ip);

	return (ret);
}
