/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "mkfs.h"
#include "util.h"

static int write_block(file_info_t *fi, sqfs_info_t *info)
{
	size_t idx, bs;
	ssize_t ret;
	void *ptr;

	idx = info->file_block_count++;
	bs = info->super.block_size;

	ret = info->cmp->do_block(info->cmp, info->block, bs,
				  info->scratch, bs);
	if (ret < 0)
		return -1;

	if (ret > 0) {
		ptr = info->scratch;
		bs = ret;
		fi->blocksizes[idx] = bs;
	} else {
		ptr = info->block;
		fi->blocksizes[idx] = bs | (1 << 24);
	}

	ret = write_retry(info->outfd, ptr, bs);
	if (ret < 0) {
		perror("writing to output file");
		return -1;
	}

	if ((size_t)ret < bs) {
		fputs("write to output file truncated\n", stderr);
		return -1;
	}

	info->super.bytes_used += bs;
	return 0;
}

static int flush_fragments(sqfs_info_t *info)
{
	size_t newsz, size;
	file_info_t *fi;
	uint64_t offset;
	void *new, *ptr;
	ssize_t ret;

	if (info->num_fragments == info->max_fragments) {
		newsz = info->max_fragments ? info->max_fragments * 2 : 16;
		new = realloc(info->fragments,
			      sizeof(info->fragments[0]) * newsz);

		if (new == NULL) {
			perror("appending to fragment table");
			return -1;
		}

		info->max_fragments = newsz;
		info->fragments = new;
	}

	offset = info->super.bytes_used;
	size = info->frag_offset;

	for (fi = info->frag_list; fi != NULL; fi = fi->frag_next)
		fi->fragment = info->num_fragments;

	ret = info->cmp->do_block(info->cmp, info->fragment, size,
				  info->scratch, info->super.block_size);
	if (ret < 0)
		return -1;

	info->fragments[info->num_fragments].start_offset = htole64(offset);
	info->fragments[info->num_fragments].pad0 = 0;

	if (ret > 0) {
		ptr = info->scratch;
		size = ret;
		info->fragments[info->num_fragments].size = htole32(size);
	} else {
		ptr = info->fragment;
		info->fragments[info->num_fragments].size =
			htole32(size | (1 << 24));
	}

	info->num_fragments += 1;

	ret = write_retry(info->outfd, ptr, size);
	if (ret < 0) {
		perror("writing to output file");
		return -1;
	}

	if ((size_t)ret < size) {
		fputs("write to output file truncated\n", stderr);
		return -1;
	}

	memset(info->fragment, 0, info->super.block_size);

	info->super.bytes_used += size;
	info->frag_offset = 0;
	info->frag_list = NULL;

	info->super.flags &= ~SQFS_FLAG_NO_FRAGMENTS;
	info->super.flags |= SQFS_FLAG_ALWAYS_FRAGMENTS;
	return 0;
}

static int add_fragment(file_info_t *fi, sqfs_info_t *info, size_t size)
{
	if (info->frag_offset + size > info->super.block_size) {
		if (flush_fragments(info))
			return -1;
	}

	fi->fragment_offset = info->frag_offset;
	fi->frag_next = info->frag_list;
	info->frag_list = fi;

	memcpy((char *)info->fragment + info->frag_offset, info->block, size);
	info->frag_offset += size;
	return 0;
}

static int process_file(sqfs_info_t *info, file_info_t *fi)
{
	uint64_t count = fi->size;
	int infd, ret;
	size_t diff;

	infd = open(fi->input_file, O_RDONLY);
	if (infd < 0) {
		perror(fi->input_file);
		return -1;
	}

	fi->startblock = info->super.bytes_used;
	info->file_block_count = 0;

	while (count != 0) {
		diff = count > (uint64_t)info->super.block_size ?
			info->super.block_size : count;

		ret = read_retry(infd, info->block, diff);
		if (ret < 0)
			goto fail_read;
		if ((size_t)ret < diff)
			goto fail_trunc;

		if (diff < info->super.block_size) {
			if (add_fragment(fi, info, diff))
				goto fail;
		} else {
			if (write_block(fi, info))
				goto fail;
		}

		count -= diff;
	}

	close(infd);
	return 0;
fail:
	close(infd);
	return -1;
fail_read:
	fprintf(stderr, "read from %s: %s\n", fi->input_file, strerror(errno));
	goto fail;
fail_trunc:
	fprintf(stderr, "%s: truncated read\n", fi->input_file);
	goto fail;
}

static void print_name(tree_node_t *n)
{
	if (n->parent != NULL) {
		print_name(n->parent);
		fputc('/', stdout);
	}

	fputs(n->name, stdout);
}

static int find_and_process_files(sqfs_info_t *info, tree_node_t *n,
				  bool quiet)
{
	if (S_ISDIR(n->mode)) {
		for (n = n->data.dir->children; n != NULL; n = n->next) {
			if (find_and_process_files(info, n, quiet))
				return -1;
		}
		return 0;
	}

	if (S_ISREG(n->mode)) {
		if (!quiet) {
			fputs("packing ", stdout);
			print_name(n);
			fputc('\n', stdout);
		}

		return process_file(info, n->data.file);
	}

	return 0;
}

int write_data_to_image(sqfs_info_t *info)
{
	int ret;

	info->block = malloc(info->super.block_size);

	if (info->block == NULL) {
		perror("allocating data block buffer");
		return -1;
	}

	info->fragment = malloc(info->super.block_size);

	if (info->fragment == NULL) {
		perror("allocating fragment buffer");
		free(info->block);
		return -1;
	}

	info->scratch = malloc(info->super.block_size);
	if (info->scratch == NULL) {
		perror("allocating scratch buffer");
		free(info->block);
		free(info->fragment);
		return -1;
	}

	ret = find_and_process_files(info, info->fs.root, info->opt.quiet);

	free(info->block);
	free(info->fragment);
	free(info->scratch);

	info->block = NULL;
	info->fragment = NULL;
	info->scratch = NULL;
	return ret;
}