// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright 2018 Google LLC
 */
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/types.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/falloc.h>
#include <linux/slab.h>
#include <linux/crc32.h>
#include <linux/kernel.h>

#include "format.h"
#include "data_mgmt.h"

struct backing_file_context *incfs_alloc_bfc(struct mount_info *mi,
					     struct file *backing_file)
{
	struct backing_file_context *result = NULL;

	result = kzalloc(sizeof(*result), GFP_NOFS);
	if (!result)
		return ERR_PTR(-ENOMEM);

	result->bc_file = get_file(backing_file);
	result->bc_cred = mi->mi_owner;
	mutex_init(&result->bc_mutex);
	return result;
}

void incfs_free_bfc(struct backing_file_context *bfc)
{
	if (!bfc)
		return;

	if (bfc->bc_file)
		fput(bfc->bc_file);

	mutex_destroy(&bfc->bc_mutex);
	kfree(bfc);
}

static loff_t incfs_get_end_offset(struct file *f)
{
	/*
	 * This function assumes that file size and the end-offset
	 * are the same. This is not always true.
	 */
	return i_size_read(file_inode(f));
}

/*
 * Truncate the tail of the file to the given length.
 * Used to rollback partially successful multistep writes.
 */
static int truncate_backing_file(struct backing_file_context *bfc,
				loff_t new_end)
{
	struct inode *inode = NULL;
	struct dentry *dentry = NULL;
	loff_t old_end = 0;
	struct iattr attr;
	int result = 0;

	if (!bfc)
		return -EFAULT;

	LOCK_REQUIRED(bfc->bc_mutex);

	if (!bfc->bc_file)
		return -EFAULT;

	old_end = incfs_get_end_offset(bfc->bc_file);
	if (old_end == new_end)
		return 0;
	if (old_end < new_end)
		return -EINVAL;

	inode = bfc->bc_file->f_inode;
	dentry = bfc->bc_file->f_path.dentry;

	attr.ia_size = new_end;
	attr.ia_valid = ATTR_SIZE;

	inode_lock(inode);
	result = notify_change(dentry, &attr, NULL);
	inode_unlock(inode);

	return result;
}

static int write_to_bf(struct backing_file_context *bfc, const void *buf,
			size_t count, loff_t pos)
{
	ssize_t res = incfs_kwrite(bfc, buf, count, pos);

	if (res < 0)
		return res;
	if (res != count)
		return -EIO;
	return 0;
}

static int append_zeros_no_fallocate(struct backing_file_context *bfc,
				     size_t file_size, size_t len)
{
	u8 buffer[256] = {};
	size_t i;

	for (i = 0; i < len; i += sizeof(buffer)) {
		int to_write = len - i > sizeof(buffer)
			? sizeof(buffer) : len - i;
		int err = write_to_bf(bfc, buffer, to_write, file_size + i);

		if (err)
			return err;
	}

	return 0;
}

/* Append a given number of zero bytes to the end of the backing file. */
static int append_zeros(struct backing_file_context *bfc, size_t len)
{
	loff_t file_size = 0;
	loff_t new_last_byte_offset = 0;
	int result;

	if (!bfc)
		return -EFAULT;

	if (len == 0)
		return 0;

	LOCK_REQUIRED(bfc->bc_mutex);

	/*
	 * Allocate only one byte at the new desired end of the file.
	 * It will increase file size and create a zeroed area of
	 * a given size.
	 */
	file_size = incfs_get_end_offset(bfc->bc_file);
	new_last_byte_offset = file_size + len - 1;
	result = vfs_fallocate(bfc->bc_file, 0, new_last_byte_offset, 1);
	if (result != -EOPNOTSUPP)
		return result;

	return append_zeros_no_fallocate(bfc, file_size, len);
}

/*
 * Append a given metadata record to the backing file and update a previous
 * record to add the new record the the metadata list.
 */
static int append_md_to_backing_file(struct backing_file_context *bfc,
			      struct incfs_md_header *record)
{
	int result = 0;
	loff_t record_offset;
	loff_t file_pos;
	__le64 new_md_offset;
	size_t record_size;

	if (!bfc || !record)
		return -EFAULT;

	if (bfc->bc_last_md_record_offset < 0)
		return -EINVAL;

	LOCK_REQUIRED(bfc->bc_mutex);

	record_size = le16_to_cpu(record->h_record_size);
	file_pos = incfs_get_end_offset(bfc->bc_file);
	record->h_next_md_offset = 0;

	/* Write the metadata record to the end of the backing file */
	record_offset = file_pos;
	new_md_offset = cpu_to_le64(record_offset);
	result = write_to_bf(bfc, record, record_size, file_pos);
	if (result)
		return result;

	/* Update next metadata offset in a previous record or a superblock. */
	if (bfc->bc_last_md_record_offset) {
		/*
		 * Find a place in the previous md record where new record's
		 * offset needs to be saved.
		 */
		file_pos = bfc->bc_last_md_record_offset +
			offsetof(struct incfs_md_header, h_next_md_offset);
	} else {
		/*
		 * No metadata yet, file a place to update in the
		 * file_header.
		 */
		file_pos = offsetof(struct incfs_file_header,
				    fh_first_md_offset);
	}
	result = write_to_bf(bfc, &new_md_offset, sizeof(new_md_offset),
			     file_pos);
	if (result)
		return result;

	bfc->bc_last_md_record_offset = record_offset;
	return result;
}

/*
 * Reserve 0-filled space for the blockmap body, and append
 * incfs_blockmap metadata record pointing to it.
 */
int incfs_write_blockmap_to_backing_file(struct backing_file_context *bfc,
					 u32 block_count)
{
	struct incfs_blockmap blockmap = {};
	int result = 0;
	loff_t file_end = 0;
	size_t map_size = block_count * sizeof(struct incfs_blockmap_entry);

	if (!bfc)
		return -EFAULT;

	blockmap.m_header.h_md_entry_type = INCFS_MD_BLOCK_MAP;
	blockmap.m_header.h_record_size = cpu_to_le16(sizeof(blockmap));
	blockmap.m_header.h_next_md_offset = cpu_to_le64(0);
	blockmap.m_block_count = cpu_to_le32(block_count);

	LOCK_REQUIRED(bfc->bc_mutex);

	/* Reserve 0-filled space for the blockmap body in the backing file. */
	file_end = incfs_get_end_offset(bfc->bc_file);
	result = append_zeros(bfc, map_size);
	if (result)
		return result;

	/* Write blockmap metadata record pointing to the body written above. */
	blockmap.m_base_offset = cpu_to_le64(file_end);
	result = append_md_to_backing_file(bfc, &blockmap.m_header);
	if (result)
		/* Error, rollback file changes */
		truncate_backing_file(bfc, file_end);

	return result;
}

int incfs_write_signature_to_backing_file(struct backing_file_context *bfc,
					struct mem_range sig, u32 tree_size,
					loff_t *tree_offset, loff_t *sig_offset)
{
	struct incfs_file_signature sg = {};
	int result = 0;
	loff_t rollback_pos = 0;
	loff_t tree_area_pos = 0;
	size_t alignment = 0;

	if (!bfc)
		return -EFAULT;

	LOCK_REQUIRED(bfc->bc_mutex);

	rollback_pos = incfs_get_end_offset(bfc->bc_file);

	sg.sg_header.h_md_entry_type = INCFS_MD_SIGNATURE;
	sg.sg_header.h_record_size = cpu_to_le16(sizeof(sg));
	sg.sg_header.h_next_md_offset = cpu_to_le64(0);
	if (sig.data != NULL && sig.len > 0) {
		sg.sg_sig_size = cpu_to_le32(sig.len);
		sg.sg_sig_offset = cpu_to_le64(rollback_pos);

		result = write_to_bf(bfc, sig.data, sig.len, rollback_pos);
		if (result)
			goto err;
	}

	tree_area_pos = incfs_get_end_offset(bfc->bc_file);
	if (tree_size > 0) {
		if (tree_size > 5 * INCFS_DATA_FILE_BLOCK_SIZE) {
			/*
			 * If hash tree is big enough, it makes sense to
			 * align in the backing file for faster access.
			 */
			loff_t offset = round_up(tree_area_pos, PAGE_SIZE);

			alignment = offset - tree_area_pos;
			tree_area_pos = offset;
		}

		/*
		 * If root hash is not the only hash in the tree.
		 * reserve 0-filled space for the tree.
		 */
		result = append_zeros(bfc, tree_size + alignment);
		if (result)
			goto err;

		sg.sg_hash_tree_size = cpu_to_le32(tree_size);
		sg.sg_hash_tree_offset = cpu_to_le64(tree_area_pos);
	}

	/* Write a hash tree metadata record pointing to the hash tree above. */
	result = append_md_to_backing_file(bfc, &sg.sg_header);
err:
	if (result)
		/* Error, rollback file changes */
		truncate_backing_file(bfc, rollback_pos);
	else {
		if (tree_offset)
			*tree_offset = tree_area_pos;
		if (sig_offset)
			*sig_offset = rollback_pos;
	}

	return result;
}

static int write_new_status_to_backing_file(struct backing_file_context *bfc,
				       u32 data_blocks_written,
				       u32 hash_blocks_written)
{
	int result;
	loff_t rollback_pos;
	struct incfs_status is = {
		.is_header = {
			.h_md_entry_type = INCFS_MD_STATUS,
			.h_record_size = cpu_to_le16(sizeof(is)),
		},
		.is_data_blocks_written = cpu_to_le32(data_blocks_written),
		.is_hash_blocks_written = cpu_to_le32(hash_blocks_written),
	};

	LOCK_REQUIRED(bfc->bc_mutex);
	rollback_pos = incfs_get_end_offset(bfc->bc_file);
	result = append_md_to_backing_file(bfc, &is.is_header);
	if (result)
		truncate_backing_file(bfc, rollback_pos);

	return result;
}

int incfs_write_status_to_backing_file(struct backing_file_context *bfc,
				       loff_t status_offset,
				       u32 data_blocks_written,
				       u32 hash_blocks_written)
{
	struct incfs_status is;
	int result;

	if (!bfc)
		return -EFAULT;

	if (status_offset == 0)
		return write_new_status_to_backing_file(bfc,
				data_blocks_written, hash_blocks_written);

	result = incfs_kread(bfc, &is, sizeof(is), status_offset);
	if (result != sizeof(is))
		return -EIO;

	is.is_data_blocks_written = cpu_to_le32(data_blocks_written);
	is.is_hash_blocks_written = cpu_to_le32(hash_blocks_written);
	result = incfs_kwrite(bfc, &is, sizeof(is), status_offset);
	if (result != sizeof(is))
		return -EIO;

	return 0;
}

int incfs_write_verity_signature_to_backing_file(
		struct backing_file_context *bfc, struct mem_range signature,
		loff_t *offset)
{
	struct incfs_file_verity_signature vs = {};
	int result;
	loff_t pos;

	/* No verity signature section is equivalent to an empty section */
	if (signature.data == NULL || signature.len == 0)
		return 0;

	pos = incfs_get_end_offset(bfc->bc_file);

	vs = (struct incfs_file_verity_signature) {
		.vs_header = (struct incfs_md_header) {
			.h_md_entry_type = INCFS_MD_VERITY_SIGNATURE,
			.h_record_size = cpu_to_le16(sizeof(vs)),
			.h_next_md_offset = cpu_to_le64(0),
		},
		.vs_size = cpu_to_le32(signature.len),
		.vs_offset = cpu_to_le64(pos),
	};

	result = write_to_bf(bfc, signature.data, signature.len, pos);
	if (result)
		goto err;

	result = append_md_to_backing_file(bfc, &vs.vs_header);
	if (result)
		goto err;

	*offset = pos;
err:
	if (result)
		/* Error, rollback file changes */
		truncate_backing_file(bfc, pos);
	return result;
}

/*
 * Write a backing file header
 * It should always be called only on empty file.
 * fh.fh_first_md_offset is 0 for now, but will be updated
 * once first metadata record is added.
 */
int incfs_write_fh_to_backing_file(struct backing_file_context *bfc,
				   incfs_uuid_t *uuid, u64 file_size)
{
	struct incfs_file_header fh = {};
	loff_t file_pos = 0;

	if (!bfc)
		return -EFAULT;

	fh.fh_magic = cpu_to_le64(INCFS_MAGIC_NUMBER);
	fh.fh_version = cpu_to_le64(INCFS_FORMAT_CURRENT_VER);
	fh.fh_header_size = cpu_to_le16(sizeof(fh));
	fh.fh_first_md_offset = cpu_to_le64(0);
	fh.fh_data_block_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE);

	fh.fh_file_size = cpu_to_le64(file_size);
	fh.fh_uuid = *uuid;

	LOCK_REQUIRED(bfc->bc_mutex);

	file_pos = incfs_get_end_offset(bfc->bc_file);
	if (file_pos != 0)
		return -EEXIST;

	return write_to_bf(bfc, &fh, sizeof(fh), file_pos);
}

/*
 * Write a backing file header for a mapping file
 * It should always be called only on empty file.
 */
int incfs_write_mapping_fh_to_backing_file(struct backing_file_context *bfc,
				incfs_uuid_t *uuid, u64 file_size, u64 offset)
{
	struct incfs_file_header fh = {};
	loff_t file_pos = 0;

	if (!bfc)
		return -EFAULT;

	fh.fh_magic = cpu_to_le64(INCFS_MAGIC_NUMBER);
	fh.fh_version = cpu_to_le64(INCFS_FORMAT_CURRENT_VER);
	fh.fh_header_size = cpu_to_le16(sizeof(fh));
	fh.fh_original_offset = cpu_to_le64(offset);
	fh.fh_data_block_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE);

	fh.fh_mapped_file_size = cpu_to_le64(file_size);
	fh.fh_original_uuid = *uuid;
	fh.fh_flags = cpu_to_le32(INCFS_FILE_MAPPED);

	LOCK_REQUIRED(bfc->bc_mutex);

	file_pos = incfs_get_end_offset(bfc->bc_file);
	if (file_pos != 0)
		return -EEXIST;

	return write_to_bf(bfc, &fh, sizeof(fh), file_pos);
}

/* Write a given data block and update file's blockmap to point it. */
int incfs_write_data_block_to_backing_file(struct backing_file_context *bfc,
				     struct mem_range block, int block_index,
				     loff_t bm_base_off, u16 flags)
{
	struct incfs_blockmap_entry bm_entry = {};
	int result = 0;
	loff_t data_offset = 0;
	loff_t bm_entry_off =
		bm_base_off + sizeof(struct incfs_blockmap_entry) * block_index;

	if (!bfc)
		return -EFAULT;

	if (block.len >= (1 << 16) || block_index < 0)
		return -EINVAL;

	LOCK_REQUIRED(bfc->bc_mutex);

	data_offset = incfs_get_end_offset(bfc->bc_file);
	if (data_offset <= bm_entry_off) {
		/* Blockmap entry is beyond the file's end. It is not normal. */
		return -EINVAL;
	}

	/* Write the block data at the end of the backing file. */
	result = write_to_bf(bfc, block.data, block.len, data_offset);
	if (result)
		return result;

	/* Update the blockmap to point to the newly written data. */
	bm_entry.me_data_offset_lo = cpu_to_le32((u32)data_offset);
	bm_entry.me_data_offset_hi = cpu_to_le16((u16)(data_offset >> 32));
	bm_entry.me_data_size = cpu_to_le16((u16)block.len);
	bm_entry.me_flags = cpu_to_le16(flags);

	return write_to_bf(bfc, &bm_entry, sizeof(bm_entry),
				bm_entry_off);
}

int incfs_write_hash_block_to_backing_file(struct backing_file_context *bfc,
					   struct mem_range block,
					   int block_index,
					   loff_t hash_area_off,
					   loff_t bm_base_off,
					   loff_t file_size)
{
	struct incfs_blockmap_entry bm_entry = {};
	int result;
	loff_t data_offset = 0;
	loff_t file_end = 0;
	loff_t bm_entry_off =
		bm_base_off +
		sizeof(struct incfs_blockmap_entry) *
			(block_index + get_blocks_count_for_size(file_size));

	if (!bfc)
		return -EFAULT;

	LOCK_REQUIRED(bfc->bc_mutex);

	data_offset = hash_area_off + block_index * INCFS_DATA_FILE_BLOCK_SIZE;
	file_end = incfs_get_end_offset(bfc->bc_file);
	if (data_offset + block.len > file_end) {
		/* Block is located beyond the file's end. It is not normal. */
		return -EINVAL;
	}

	result = write_to_bf(bfc, block.data, block.len, data_offset);
	if (result)
		return result;

	bm_entry.me_data_offset_lo = cpu_to_le32((u32)data_offset);
	bm_entry.me_data_offset_hi = cpu_to_le16((u16)(data_offset >> 32));
	bm_entry.me_data_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE);

	return write_to_bf(bfc, &bm_entry, sizeof(bm_entry), bm_entry_off);
}

int incfs_read_blockmap_entry(struct backing_file_context *bfc, int block_index,
			loff_t bm_base_off,
			struct incfs_blockmap_entry *bm_entry)
{
	int error = incfs_read_blockmap_entries(bfc, bm_entry, block_index, 1,
						bm_base_off);

	if (error < 0)
		return error;

	if (error == 0)
		return -EIO;

	if (error != 1)
		return -EFAULT;

	return 0;
}

int incfs_read_blockmap_entries(struct backing_file_context *bfc,
		struct incfs_blockmap_entry *entries,
		int start_index, int blocks_number,
		loff_t bm_base_off)
{
	loff_t bm_entry_off =
		bm_base_off + sizeof(struct incfs_blockmap_entry) * start_index;
	const size_t bytes_to_read = sizeof(struct incfs_blockmap_entry)
					* blocks_number;
	int result = 0;

	if (!bfc || !entries)
		return -EFAULT;

	if (start_index < 0 || bm_base_off <= 0)
		return -ENODATA;

	result = incfs_kread(bfc, entries, bytes_to_read, bm_entry_off);
	if (result < 0)
		return result;
	return result / sizeof(*entries);
}

int incfs_read_file_header(struct backing_file_context *bfc,
			   loff_t *first_md_off, incfs_uuid_t *uuid,
			   u64 *file_size, u32 *flags)
{
	ssize_t bytes_read = 0;
	struct incfs_file_header fh = {};

	if (!bfc || !first_md_off)
		return -EFAULT;

	bytes_read = incfs_kread(bfc, &fh, sizeof(fh), 0);
	if (bytes_read < 0)
		return bytes_read;

	if (bytes_read < sizeof(fh))
		return -EBADMSG;

	if (le64_to_cpu(fh.fh_magic) != INCFS_MAGIC_NUMBER)
		return -EILSEQ;

	if (le64_to_cpu(fh.fh_version) > INCFS_FORMAT_CURRENT_VER)
		return -EILSEQ;

	if (le16_to_cpu(fh.fh_data_block_size) != INCFS_DATA_FILE_BLOCK_SIZE)
		return -EILSEQ;

	if (le16_to_cpu(fh.fh_header_size) != sizeof(fh))
		return -EILSEQ;

	if (first_md_off)
		*first_md_off = le64_to_cpu(fh.fh_first_md_offset);
	if (uuid)
		*uuid = fh.fh_uuid;
	if (file_size)
		*file_size = le64_to_cpu(fh.fh_file_size);
	if (flags)
		*flags = le32_to_cpu(fh.fh_flags);
	return 0;
}

/*
 * Read through metadata records from the backing file one by one
 * and call provided metadata handlers.
 */
int incfs_read_next_metadata_record(struct backing_file_context *bfc,
			      struct metadata_handler *handler)
{
	const ssize_t max_md_size = INCFS_MAX_METADATA_RECORD_SIZE;
	ssize_t bytes_read = 0;
	size_t md_record_size = 0;
	loff_t next_record = 0;
	int res = 0;
	struct incfs_md_header *md_hdr = NULL;

	if (!bfc || !handler)
		return -EFAULT;

	if (handler->md_record_offset == 0)
		return -EPERM;

	memset(&handler->md_buffer, 0, max_md_size);
	bytes_read = incfs_kread(bfc, &handler->md_buffer, max_md_size,
				 handler->md_record_offset);
	if (bytes_read < 0)
		return bytes_read;
	if (bytes_read < sizeof(*md_hdr))
		return -EBADMSG;

	md_hdr = &handler->md_buffer.md_header;
	next_record = le64_to_cpu(md_hdr->h_next_md_offset);
	md_record_size = le16_to_cpu(md_hdr->h_record_size);

	if (md_record_size > max_md_size) {
		pr_warn("incfs: The record is too large. Size: %zu",
				md_record_size);
		return -EBADMSG;
	}

	if (bytes_read < md_record_size) {
		pr_warn("incfs: The record hasn't been fully read.");
		return -EBADMSG;
	}

	if (next_record <= handler->md_record_offset && next_record != 0) {
		pr_warn("incfs: Next record (%lld) points back in file.",
			next_record);
		return -EBADMSG;
	}

	switch (md_hdr->h_md_entry_type) {
	case INCFS_MD_NONE:
		break;
	case INCFS_MD_BLOCK_MAP:
		if (handler->handle_blockmap)
			res = handler->handle_blockmap(
				&handler->md_buffer.blockmap, handler);
		break;
	case INCFS_MD_FILE_ATTR:
		/*
		 * File attrs no longer supported, ignore section for
		 * compatibility
		 */
		break;
	case INCFS_MD_SIGNATURE:
		if (handler->handle_signature)
			res = handler->handle_signature(
				&handler->md_buffer.signature, handler);
		break;
	case INCFS_MD_STATUS:
		if (handler->handle_status)
			res = handler->handle_status(
				&handler->md_buffer.status, handler);
		break;
	case INCFS_MD_VERITY_SIGNATURE:
		if (handler->handle_verity_signature)
			res = handler->handle_verity_signature(
				&handler->md_buffer.verity_signature, handler);
		break;
	default:
		res = -ENOTSUPP;
		break;
	}

	if (!res) {
		if (next_record == 0) {
			/*
			 * Zero offset for the next record means that the last
			 * metadata record has just been processed.
			 */
			bfc->bc_last_md_record_offset =
				handler->md_record_offset;
		}
		handler->md_prev_record_offset = handler->md_record_offset;
		handler->md_record_offset = next_record;
	}
	return res;
}

ssize_t incfs_kread(struct backing_file_context *bfc, void *buf, size_t size,
		    loff_t pos)
{
	const struct cred *old_cred = override_creds(bfc->bc_cred);
	int ret = kernel_read(bfc->bc_file, buf, size, &pos);

	revert_creds(old_cred);
	return ret;
}

ssize_t incfs_kwrite(struct backing_file_context *bfc, const void *buf,
		     size_t size, loff_t pos)
{
	const struct cred *old_cred = override_creds(bfc->bc_cred);
	int ret = kernel_write(bfc->bc_file, buf, size, &pos);

	revert_creds(old_cred);
	return ret;
}