/* SPDX-License-Identifier: LGPL-3.0-or-later */
/*
 * common.c
 *
 * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
 */
#define SQFS_BUILDING_DLL
#include "internal.h"

static int set_block_size(sqfs_inode_generic_t **inode,
			  sqfs_u32 index, sqfs_u32 size)
{
	size_t min_size = (index + 1) * sizeof(sqfs_u32);
	size_t avail = (*inode)->payload_bytes_available;
	size_t newsz;
	void *new;

	if (avail < min_size) {
		newsz = avail ? avail : (sizeof(sqfs_u32) * 4);
		while (newsz < min_size)
			newsz *= 2;

		if (SZ_ADD_OV(newsz, sizeof(**inode), &newsz))
			return SQFS_ERROR_OVERFLOW;

		if (sizeof(size_t) > sizeof(sqfs_u32)) {
			if ((newsz - sizeof(**inode)) > 0x0FFFFFFFFUL)
				return SQFS_ERROR_OVERFLOW;
		}

		new = realloc((*inode), newsz);
		if (new == NULL)
			return SQFS_ERROR_ALLOC;

		(*inode) = new;
		(*inode)->payload_bytes_available = newsz - sizeof(**inode);
	}

	(*inode)->extra[index] = size;

	if (min_size >= (*inode)->payload_bytes_used)
		(*inode)->payload_bytes_used = min_size;

	return 0;
}

static void release_old_block(sqfs_block_processor_t *proc, sqfs_block_t *blk)
{
	sqfs_block_t *it;

	if (blk->flags & SQFS_BLK_FRAGMENT_BLOCK && blk->frag_list != NULL) {
		it = blk->frag_list;

		while (it->next != NULL)
			it = it->next;

		it->next = proc->free_list;
		proc->free_list = blk->frag_list;
	}

	blk->next = proc->free_list;
	proc->free_list = blk;
}

static int process_completed_block(sqfs_block_processor_t *proc,
				   sqfs_block_t *blk)
{
	sqfs_u64 location;
	sqfs_u32 size;
	int err;

	err = proc->wr->write_data_block(proc->wr, blk->user, blk->size,
					 blk->checksum,
					 blk->flags & ~BLK_FLAG_INTERNAL,
					 blk->data, &location);
	if (err)
		goto out;

	proc->stats.output_bytes_generated += blk->size;

	if (blk->flags & SQFS_BLK_IS_SPARSE) {
		if (blk->inode != NULL) {
			sqfs_inode_make_extended(*(blk->inode));
			(*(blk->inode))->data.file_ext.sparse += blk->size;

			err = set_block_size(blk->inode, blk->index, 0);
			if (err)
				goto out;
		}
		proc->stats.sparse_block_count += 1;
	} else if (blk->size != 0) {
		size = blk->size;
		if (!(blk->flags & SQFS_BLK_IS_COMPRESSED))
			size |= 1 << 24;

		if (blk->flags & SQFS_BLK_FRAGMENT_BLOCK) {
			if (proc->frag_tbl != NULL) {
				err = sqfs_frag_table_set(proc->frag_tbl,
							  blk->index, location,
							  size);
				if (err)
					goto out;
			}
			proc->stats.frag_block_count += 1;
		} else {
			if (blk->inode != NULL) {
				err = set_block_size(blk->inode, blk->index,
						     size);
				if (err)
					goto out;
			}
			proc->stats.data_block_count += 1;
		}
	}

	if (blk->flags & SQFS_BLK_LAST_BLOCK && blk->inode != NULL)
		sqfs_inode_set_file_block_start(*(blk->inode), location);
out:
	release_old_block(proc, blk);
	return err;
}

static bool is_zero_block(unsigned char *ptr, size_t size)
{
	return ptr[0] == 0 && memcmp(ptr, ptr + 1, size - 1) == 0;
}

static int process_block(sqfs_block_t *block, sqfs_compressor_t *cmp,
			 sqfs_u8 *scratch, size_t scratch_size)
{
	sqfs_block_t *it;
	size_t offset;
	sqfs_s32 ret;

	if (block->size == 0)
		return 0;

	if (block->flags & SQFS_BLK_FRAGMENT_BLOCK) {
		offset = block->size;

		for (it = block->frag_list; it != NULL; it = it->next) {
			assert(offset >= it->size);
			offset -= it->size;

			memcpy(block->data + offset, it->data, it->size);

			block->flags |= (it->flags & SQFS_BLK_DONT_COMPRESS);
		}
	}

	if (!(block->flags & SQFS_BLK_IGNORE_SPARSE) &&
	    is_zero_block(block->data, block->size)) {
		block->flags |= SQFS_BLK_IS_SPARSE;
		return 0;
	}

	if (block->flags & SQFS_BLK_DONT_HASH) {
		block->checksum = 0;
	} else {
		block->checksum = xxh32(block->data, block->size);
	}

	if (block->flags & (SQFS_BLK_IS_FRAGMENT | SQFS_BLK_DONT_COMPRESS))
		return 0;

	ret = cmp->do_block(cmp, block->data, block->size,
			    scratch, scratch_size);
	if (ret < 0)
		return ret;

	if (ret > 0) {
		memcpy(block->data, scratch, ret);
		block->size = ret;
		block->flags |= SQFS_BLK_IS_COMPRESSED;
	}
	return 0;
}

static int process_completed_fragment(sqfs_block_processor_t *proc,
				      sqfs_block_t *frag,
				      sqfs_block_t **blk_out)
{
	sqfs_u32 index, offset;
	size_t size;
	int err;

	if (frag->flags & SQFS_BLK_IS_SPARSE) {
		if (frag->inode != NULL) {
			sqfs_inode_make_extended(*(frag->inode));
			set_block_size(frag->inode, frag->index, 0);
			(*(frag->inode))->data.file_ext.sparse += frag->size;
		}
		proc->stats.sparse_block_count += 1;
		release_old_block(proc, frag);
		return 0;
	}

	proc->stats.total_frag_count += 1;

	if (!(frag->flags & SQFS_BLK_DONT_DEDUPLICATE) &&
	    proc->frag_tbl != NULL) {
		err = sqfs_frag_table_find_tail_end(proc->frag_tbl,
						    frag->checksum, frag->size,
						    &index, &offset);
		if (err == 0) {
			if (frag->inode != NULL) {
				sqfs_inode_set_frag_location(*(frag->inode),
							     index, offset);
			}
			release_old_block(proc, frag);
			return 0;
		}
	}

	if (proc->frag_block != NULL) {
		size = proc->frag_block->size + frag->size;

		if (size > proc->max_block_size) {
			*blk_out = proc->frag_block;
			proc->frag_block = NULL;
		}
	}

	if (proc->frag_block == NULL) {
		if (proc->frag_tbl == NULL) {
			index = 0;
		} else {
			err = sqfs_frag_table_append(proc->frag_tbl,
						     0, 0, &index);
			if (err)
				goto fail;
		}

		offset = 0;
		proc->frag_block = frag;
		proc->frag_block->index = index;
		proc->frag_block->flags &= SQFS_BLK_DONT_COMPRESS;
		proc->frag_block->flags |= SQFS_BLK_FRAGMENT_BLOCK;
		proc->frag_block->frag_list = NULL;
	} else {
		index = proc->frag_block->index;
		offset = proc->frag_block->size;

		frag->next = proc->frag_block->frag_list;
		proc->frag_block->frag_list = frag;

		proc->frag_block->size += frag->size;
	}

	if (proc->frag_tbl != NULL) {
		err = sqfs_frag_table_add_tail_end(proc->frag_tbl,
						   index, offset,
						   frag->size, frag->checksum);
		if (err)
			goto fail_outblk;
	}

	if (frag->inode != NULL)
		sqfs_inode_set_frag_location(*(frag->inode), index, offset);

	proc->stats.actual_frag_count += 1;
	return 0;
fail:
	release_old_block(proc, frag);
fail_outblk:
	if (*blk_out != NULL) {
		release_old_block(proc, *blk_out);
		*blk_out = NULL;
	}
	return err;
}

int block_processor_init(sqfs_block_processor_t *base, size_t max_block_size,
			 sqfs_compressor_t *cmp, sqfs_block_writer_t *wr,
			 sqfs_frag_table_t *tbl)
{
	base->process_completed_block = process_completed_block;
	base->process_completed_fragment = process_completed_fragment;
	base->process_block = process_block;
	base->max_block_size = max_block_size;
	base->cmp = cmp;
	base->frag_tbl = tbl;
	base->wr = wr;
	base->stats.size = sizeof(base->stats);
	return 0;
}