/*
 * undo_io.c --- This is the undo io manager that copies the old data that
 * copies the old data being overwritten into a tdb database
 *
 * Copyright IBM Corporation, 2007
 * Author Aneesh Kumar K.V <aneesh.kumar@linux.vnet.ibm.com>
 *
 * %Begin-Header%
 * This file may be redistributed under the terms of the GNU Public
 * License.
 * %End-Header%
 */

#define _LARGEFILE_SOURCE
#define _LARGEFILE64_SOURCE

#include <stdio.h>
#include <string.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#if HAVE_ERRNO_H
#include <errno.h>
#endif
#include <fcntl.h>
#include <time.h>
#ifdef __linux__
#include <sys/utsname.h>
#endif
#if HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#if HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#if HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif

#include "tdb.h"

#include "ext2_fs.h"
#include "ext2fs.h"

#ifdef __GNUC__
#define ATTR(x) __attribute__(x)
#else
#define ATTR(x)
#endif

/*
 * For checking structure magic numbers...
 */

#define EXT2_CHECK_MAGIC(struct, code) \
	  if ((struct)->magic != (code)) return (code)

struct undo_private_data {
	int	magic;
	TDB_CONTEXT *tdb;
	char *tdb_file;

	/* The backing io channel */
	io_channel real;

	int tdb_data_size;
	int tdb_written;

	/* to support offset in unix I/O manager */
	ext2_loff_t offset;
};

static errcode_t undo_open(const char *name, int flags, io_channel *channel);
static errcode_t undo_close(io_channel channel);
static errcode_t undo_set_blksize(io_channel channel, int blksize);
static errcode_t undo_read_blk(io_channel channel, unsigned long block,
			       int count, void *data);
static errcode_t undo_write_blk(io_channel channel, unsigned long block,
				int count, const void *data);
static errcode_t undo_flush(io_channel channel);
static errcode_t undo_write_byte(io_channel channel, unsigned long offset,
				int size, const void *data);
static errcode_t undo_set_option(io_channel channel, const char *option,
				 const char *arg);

static struct struct_io_manager struct_undo_manager = {
	EXT2_ET_MAGIC_IO_MANAGER,
	"Undo I/O Manager",
	undo_open,
	undo_close,
	undo_set_blksize,
	undo_read_blk,
	undo_write_blk,
	undo_flush,
	undo_write_byte,
	undo_set_option
};

io_manager undo_io_manager = &struct_undo_manager;
static io_manager undo_io_backing_manager ;
static char *tdb_file;
static int actual_size;

static unsigned char mtime_key[] = "filesystem MTIME";
static unsigned char blksize_key[] = "filesystem BLKSIZE";
static unsigned char uuid_key[] = "filesystem UUID";

errcode_t set_undo_io_backing_manager(io_manager manager)
{
	/*
	 * We may want to do some validation later
	 */
	undo_io_backing_manager = manager;
	return 0;
}

errcode_t set_undo_io_backup_file(char *file_name)
{
	tdb_file = strdup(file_name);

	if (tdb_file == NULL) {
		return EXT2_ET_NO_MEMORY;
	}

	return 0;
}

static errcode_t write_file_system_identity(io_channel undo_channel,
							TDB_CONTEXT *tdb)
{
	errcode_t retval;
	struct ext2_super_block super;
	TDB_DATA tdb_key, tdb_data;
	struct undo_private_data *data;
	io_channel channel;
	int block_size ;

	data = (struct undo_private_data *) undo_channel->private_data;
	channel = data->real;
	block_size = channel->block_size;

	io_channel_set_blksize(channel, SUPERBLOCK_OFFSET);
	retval = io_channel_read_blk(channel, 1, -SUPERBLOCK_SIZE, &super);
	if (retval)
		goto err_out;

	/* Write to tdb file in the file system byte order */
	tdb_key.dptr = mtime_key;
	tdb_key.dsize = sizeof(mtime_key);
	tdb_data.dptr = (unsigned char *) &(super.s_mtime);
	tdb_data.dsize = sizeof(super.s_mtime);

	retval = tdb_store(tdb, tdb_key, tdb_data, TDB_INSERT);
	if (retval == -1) {
		retval = EXT2_ET_TDB_SUCCESS + tdb_error(tdb);
		goto err_out;
	}

	tdb_key.dptr = uuid_key;
	tdb_key.dsize = sizeof(uuid_key);
	tdb_data.dptr = (unsigned char *)&(super.s_uuid);
	tdb_data.dsize = sizeof(super.s_uuid);

	retval = tdb_store(tdb, tdb_key, tdb_data, TDB_INSERT);
	if (retval == -1) {
		retval = EXT2_ET_TDB_SUCCESS + tdb_error(tdb);
	}

err_out:
	io_channel_set_blksize(channel, block_size);
	return retval;
}

static errcode_t write_block_size(TDB_CONTEXT *tdb, int block_size)
{
	errcode_t retval;
	TDB_DATA tdb_key, tdb_data;

	tdb_key.dptr = blksize_key;
	tdb_key.dsize = sizeof(blksize_key);
	tdb_data.dptr = (unsigned char *)&(block_size);
	tdb_data.dsize = sizeof(block_size);

	retval = tdb_store(tdb, tdb_key, tdb_data, TDB_INSERT);
	if (retval == -1) {
		retval = EXT2_ET_TDB_SUCCESS + tdb_error(tdb);
	}

	return retval;
}

static errcode_t undo_write_tdb(io_channel channel,
				unsigned long block, int count)

{
	int size, sz;
	unsigned long block_num, backing_blk_num;
	errcode_t retval = 0;
	ext2_loff_t offset;
	struct undo_private_data *data;
	TDB_DATA tdb_key, tdb_data;
	unsigned char *read_ptr;
	unsigned long end_block;

	data = (struct undo_private_data *) channel->private_data;

	if (data->tdb == NULL) {
		/*
		 * Transaction database not initialized
		 */
		return 0;
	}

	if (count == 1)
		size = channel->block_size;
	else {
		if (count < 0)
			size = -count;
		else
			size = count * channel->block_size;
	}
	/*
	 * Data is stored in tdb database as blocks of tdb_data_size size
	 * This helps in efficient lookup further.
	 *
	 * We divide the disk to blocks of tdb_data_size.
	 */
	offset = (block * channel->block_size) + data->offset ;
	block_num = offset / data->tdb_data_size;
	end_block = (offset + size) / data->tdb_data_size;

	tdb_transaction_start(data->tdb);
	while (block_num <= end_block ) {

		tdb_key.dptr = (unsigned char *)&block_num;
		tdb_key.dsize = sizeof(block_num);
		/*
		 * Check if we have the record already
		 */
		if (tdb_exists(data->tdb, tdb_key)) {
			/* Try the next block */
			block_num++;
			continue;
		}
		/*
		 * Read one block using the backing I/O manager
		 * The backing I/O manager block size may be
		 * different from the tdb_data_size.
		 * Also we need to recalcuate the block number with respect
		 * to the backing I/O manager.
		 */
		offset = block_num * data->tdb_data_size;
		backing_blk_num = (offset - data->offset) / channel->block_size;

		count = data->tdb_data_size +
				((offset - data->offset) % channel->block_size);
		retval = ext2fs_get_mem(count, &read_ptr);
		if (retval) {
			tdb_transaction_cancel(data->tdb);
			return retval;
		}

		memset(read_ptr, 0, count);
		actual_size = 0;
		if ((count % channel->block_size) == 0)
			sz = count / channel->block_size;
		else
			sz = -count;
		retval = io_channel_read_blk(data->real, backing_blk_num,
					     sz, read_ptr);
		if (retval) {
			if (retval != EXT2_ET_SHORT_READ) {
				free(read_ptr);
				tdb_transaction_cancel(data->tdb);
				return retval;
			}
			/*
			 * short read so update the record size
			 * accordingly
			 */
			tdb_data.dsize = actual_size;
		} else {
			tdb_data.dsize = data->tdb_data_size;
		}
		tdb_data.dptr = read_ptr +
				((offset - data->offset) % channel->block_size);
#ifdef DEBUG
		printf("Printing with key %ld data %x and size %d\n",
		       block_num,
		       tdb_data.dptr,
		       tdb_data.dsize);
#endif
		if (!data->tdb_written) {
			data->tdb_written = 1;
			/* Write the blocksize to tdb file */
			retval = write_block_size(data->tdb,
						  data->tdb_data_size);
			if (retval) {
				tdb_transaction_cancel(data->tdb);
				retval = EXT2_ET_TDB_ERR_IO;
				free(read_ptr);
				return retval;
			}
		}
		retval = tdb_store(data->tdb, tdb_key, tdb_data, TDB_INSERT);
		if (retval == -1) {
			/*
			 * TDB_ERR_EXISTS cannot happen because we
			 * have already verified it doesn't exist
			 */
			tdb_transaction_cancel(data->tdb);
			retval = EXT2_ET_TDB_ERR_IO;
			free(read_ptr);
			return retval;
		}
		free(read_ptr);
		/* Next block */
		block_num++;
	}
	tdb_transaction_commit(data->tdb);

	return retval;
}

static errcode_t undo_io_read_error(io_channel channel ATTR((unused)),
				    unsigned long block ATTR((unused)),
				    int count ATTR((unused)),
				    void *data ATTR((unused)),
				    size_t size ATTR((unused)),
				    int actual,
				    errcode_t error ATTR((unused)))
{
	actual_size = actual;
	return error;
}

static void undo_err_handler_init(io_channel channel)
{
	channel->read_error = undo_io_read_error;
}

static errcode_t undo_open(const char *name, int flags, io_channel *channel)
{
	io_channel	io = NULL;
	struct undo_private_data *data = NULL;
	errcode_t	retval;

	if (name == 0)
		return EXT2_ET_BAD_DEVICE_NAME;
	retval = ext2fs_get_mem(sizeof(struct struct_io_channel), &io);
	if (retval)
		return retval;
	memset(io, 0, sizeof(struct struct_io_channel));
	io->magic = EXT2_ET_MAGIC_IO_CHANNEL;
	retval = ext2fs_get_mem(sizeof(struct undo_private_data), &data);
	if (retval)
		goto cleanup;

	io->manager = undo_io_manager;
	retval = ext2fs_get_mem(strlen(name)+1, &io->name);
	if (retval)
		goto cleanup;

	strcpy(io->name, name);
	io->private_data = data;
	io->block_size = 1024;
	io->read_error = 0;
	io->write_error = 0;
	io->refcount = 1;

	memset(data, 0, sizeof(struct undo_private_data));
	data->magic = EXT2_ET_MAGIC_UNIX_IO_CHANNEL;

	if (undo_io_backing_manager) {
		retval = undo_io_backing_manager->open(name, flags,
						       &data->real);
		if (retval)
			goto cleanup;
	} else {
		data->real = 0;
	}

	/* setup the tdb file */
	data->tdb = tdb_open(tdb_file, 0, TDB_CLEAR_IF_FIRST,
			     O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 0600);
	if (!data->tdb) {
		retval = errno;
		goto cleanup;
	}

	/*
	 * setup err handler for read so that we know
	 * when the backing manager fails do short read
	 */
	undo_err_handler_init(data->real);

	*channel = io;
	return 0;

cleanup:
	if (data->real)
		io_channel_close(data->real);
	if (data)
		ext2fs_free_mem(&data);
	if (io)
		ext2fs_free_mem(&io);
	return retval;
}

static errcode_t undo_close(io_channel channel)
{
	struct undo_private_data *data;
	errcode_t	retval = 0;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	if (--channel->refcount > 0)
		return 0;
	/* Before closing write the file system identity */
	retval = write_file_system_identity(channel, data->tdb);
	if (retval)
		return retval;
	if (data->real)
		retval = io_channel_close(data->real);
	if (data->tdb)
		tdb_close(data->tdb);
	ext2fs_free_mem(&channel->private_data);
	if (channel->name)
		ext2fs_free_mem(&channel->name);
	ext2fs_free_mem(&channel);

	return retval;
}

static errcode_t undo_set_blksize(io_channel channel, int blksize)
{
	struct undo_private_data *data;
	errcode_t		retval = 0;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	if (data->real)
		retval = io_channel_set_blksize(data->real, blksize);
	/*
	 * Set the block size used for tdb
	 */
	if (!data->tdb_data_size) {
		data->tdb_data_size = blksize;
	}
	channel->block_size = blksize;
	return retval;
}

static errcode_t undo_read_blk(io_channel channel, unsigned long block,
			       int count, void *buf)
{
	errcode_t	retval = 0;
	struct undo_private_data *data;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	if (data->real)
		retval = io_channel_read_blk(data->real, block, count, buf);

	return retval;
}

static errcode_t undo_write_blk(io_channel channel, unsigned long block,
				int count, const void *buf)
{
	struct undo_private_data *data;
	errcode_t	retval = 0;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);
	/*
	 * First write the existing content into database
	 */
	retval = undo_write_tdb(channel, block, count);
	if (retval)
		 return retval;
	if (data->real)
		retval = io_channel_write_blk(data->real, block, count, buf);

	return retval;
}

static errcode_t undo_write_byte(io_channel channel, unsigned long offset,
				 int size, const void *buf)
{
	struct undo_private_data *data;
	errcode_t	retval = 0;
	ext2_loff_t	location;
	unsigned long blk_num, count;;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	location = offset + data->offset;
	blk_num = location/channel->block_size;
	/*
	 * the size specified may spread across multiple blocks
	 * also make sure we account for the fact that block start
	 * offset for tdb is different from the backing I/O manager
	 * due to possible different block size
	 */
	count = (size + (location % channel->block_size) +
			channel->block_size  -1)/channel->block_size;
	retval = undo_write_tdb(channel, blk_num, count);
	if (retval)
		return retval;
	if (data->real && data->real->manager->write_byte)
		retval = io_channel_write_byte(data->real, offset, size, buf);

	return retval;
}

/*
 * Flush data buffers to disk.
 */
static errcode_t undo_flush(io_channel channel)
{
	errcode_t	retval = 0;
	struct undo_private_data *data;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	if (data->real)
		retval = io_channel_flush(data->real);

	return retval;
}

static errcode_t undo_set_option(io_channel channel, const char *option,
				 const char *arg)
{
	errcode_t	retval = 0;
	struct undo_private_data *data;
	unsigned long tmp;
	char *end;

	EXT2_CHECK_MAGIC(channel, EXT2_ET_MAGIC_IO_CHANNEL);
	data = (struct undo_private_data *) channel->private_data;
	EXT2_CHECK_MAGIC(data, EXT2_ET_MAGIC_UNIX_IO_CHANNEL);

	if (!strcmp(option, "tdb_data_size")) {
		if (!arg)
			return EXT2_ET_INVALID_ARGUMENT;

		tmp = strtoul(arg, &end, 0);
		if (*end)
			return EXT2_ET_INVALID_ARGUMENT;
		if (!data->tdb_data_size || !data->tdb_written) {
			data->tdb_data_size = tmp;
		}
		return 0;
	}
	/*
	 * Need to support offset option to work with
	 * Unix I/O manager
	 */
	if (data->real && data->real->manager->set_option) {
		retval = data->real->manager->set_option(data->real,
							option, arg);
	}
	if (!retval && !strcmp(option, "offset")) {
		if (!arg)
			return EXT2_ET_INVALID_ARGUMENT;

		tmp = strtoul(arg, &end, 0);
		if (*end)
			return EXT2_ET_INVALID_ARGUMENT;
		data->offset = tmp;
	}
	return retval;
}