/* cbfstool, CLI utility for creating rmodules */
/* SPDX-License-Identifier: GPL-2.0-only */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>

#include "common.h"
#include "cbfs_image.h"
#include "partitioned_file.h"
#include "fit.h"

/* Global variables */
partitioned_file_t *image_file;

static const char *optstring  = "H:j:f:r:d:t:n:s:cAaDvh?";
static struct option long_options[] = {
	{"file",           required_argument, 0, 'f' },
	{"region",         required_argument, 0, 'r' },
	{"add-cbfs-entry", no_argument,       0, 'a' },
	{"add-region",     no_argument,       0, 'A' },
	{"del-entry",      required_argument, 0, 'd' },
	{"clear-table",    no_argument,       0, 'c' },
	{"fit-type",       required_argument, 0, 't' },
	{"cbfs-filename",  required_argument, 0, 'n' },
	{"max-table-size", required_argument, 0, 's' },
	{"topswap-size",   required_argument, 0, 'j' },
	{"dump",           no_argument,       0, 'D' },
	{"verbose",        no_argument,       0, 'v' },
	{"help",           no_argument,       0, 'h' },
	{"header-offset",  required_argument, 0, 'H' },
	{NULL,             0,                 0,  0  }
};

static void usage(const char *name)
{
	printf(
		"ifittool: utility for modifying Intel Firmware Interface Table\n\n"
		"USAGE: %s [-h] [-H] [-v] [-D] [-c] <-f|--file name> <-s|--max-table-size size> <-r|--region fmap region> OPERATION\n"
		"\tOPERATION:\n"
		"\t\t-a|--add-entry        :   Add a CBFS file as new entry to FIT\n"
		"\t\t-A|--add-region       :   Add region as new entry to FIT (for microcodes)\n"
		"\t\t-d|--del-entry number :   Delete existing <number> entry\n"
		"\t\t-t|--fit-type         :   Type of new entry\n"
		"\t\t-n|--name             :   The CBFS filename or region to add to table\n"
		"\tOPTIONAL ARGUMENTS:\n"
		"\t\t-h|--help             :   Display this text\n"
		"\t\t-H|--header-offset    :   Do not search for header, use this offset\n"
		"\t\t-v|--verbose          :   Be verbose\n"
		"\t\t-D|--dump             :   Dump FIT table (at end of operation)\n"
		"\t\t-c|--clear-table      :   Remove all existing entries (do not update)\n"
		"\t\t-j|--topswap-size     :   Use second FIT table if non zero\n"
		"\tREQUIRED ARGUMENTS:\n"
		"\t\t-f|--file name        :   The file containing the CBFS\n"
		"\t\t-s|--max-table-size   :   The number of possible FIT entries in table\n"
		"\t\t-r|--region           :   The FMAP region to operate on\n"
	, name);
}

static int is_valid_topswap(size_t topswap_size)
{
	switch (topswap_size) {
	case (64 * KiB):
	case (128 * KiB):
	case (256 * KiB):
	case (512 * KiB):
	case (1 * MiB):
		break;
	default:
		ERROR("Invalid topswap_size %zd\n", topswap_size);
		ERROR("topswap can be 64K|128K|256K|512K|1M\n");
		return 0;
	}
	return 1;
}

/*
 * Converts between offsets from the start of the specified image region and
 * "top-aligned" offsets from the top of the entire boot media. See comment
 * below for convert_to_from_top_aligned() about forming addresses.
 */
static unsigned int convert_to_from_absolute_top_aligned(
		const struct buffer *region, unsigned int offset)
{
	assert(region);

	size_t image_size = partitioned_file_total_size(image_file);

	return image_size - region->offset - offset;
}

/*
 * Converts between offsets from the start of the specified image region and
 * "top-aligned" offsets from the top of the image region. Works in either
 * direction: pass in one type of offset and receive the other type.
 * N.B. A top-aligned offset is always a positive number, and should not be
 * confused with a top-aligned *address*, which is its arithmetic inverse. */
static unsigned int convert_to_from_top_aligned(const struct buffer *region,
						unsigned int offset)
{
	assert(region);

	/* Cover the situation where a negative base address is given by the
	 * user. Callers of this function negate it, so it'll be a positive
	 * number smaller than the region.
	 */
	if ((offset > 0) && (offset < region->size))
		return region->size - offset;

	return convert_to_from_absolute_top_aligned(region, offset);
}

/*
 * Get a pointer from an offset. This function assumes the ROM is located
 * in the host address space at [4G - romsize -> 4G). It also assume all
 * pointers have values within this address range.
 */
static inline uint32_t offset_to_ptr(fit_offset_converter_t helper,
				     const struct buffer *region, int offset)
{
	return -helper(region, offset);
}

enum fit_operation {
	NO_OP = 0,
	ADD_CBFS_OP,
	ADD_REGI_OP,
	ADD_ADDR_OP,
	DEL_OP
};

int main(int argc, char *argv[])
{
	int c;
	const char *input_file = NULL;
	const char *name = NULL;
	const char *region_name = NULL;
	enum fit_operation op = NO_OP;
	bool dump = false, clear_table = false;
	size_t max_table_size = 0;
	size_t table_entry = 0;
	uint32_t addr = 0;
	size_t topswap_size = 0;
	enum fit_type fit_type = 0;
	uint32_t headeroffset = ~0u;

	verbose = 0;

	if (argc < 4) {
		usage(argv[0]);
		return 1;
	}

	while (1) {
		int optindex = 0;
		char *suffix = NULL;

		c = getopt_long(argc, argv, optstring, long_options, &optindex);

		if (c == -1)
			break;

		switch (c) {
		case 'h':
			usage(argv[0]);
			return 1;
		case 'a':
			if (op != NO_OP) {
				ERROR("specified multiple actions at once\n");
				usage(argv[0]);
				return 1;
			}
			op = ADD_CBFS_OP;
			break;
		case 'A':
			if (op != NO_OP) {
				ERROR("specified multiple actions at once\n");
				usage(argv[0]);
				return 1;
			}
			op = ADD_REGI_OP;
			break;
		case 'x':
			if (op != NO_OP) {
				ERROR("specified multiple actions at once\n");
				usage(argv[0]);
				return 1;
			}
			op = ADD_ADDR_OP;
			addr = atoll(optarg);
			break;
		case 'c':
			clear_table = true;
			break;
		case 'd':
			if (op != NO_OP) {
				ERROR("specified multiple actions at once\n");
				usage(argv[0]);
				return 1;
			}
			op = DEL_OP;
			table_entry = atoi(optarg);
			break;
		case 'D':
			dump = true;
			break;
		case 'f':
			input_file = optarg;
			break;
		case 'H':
			headeroffset = strtoul(optarg, &suffix, 0);
			if (!*optarg || (suffix && *suffix)) {
				ERROR("Invalid header offset '%s'.\n", optarg);
				return 1;
			}
			break;
		case 'j':
			topswap_size = strtol(optarg, NULL, 0);
			if (!is_valid_topswap(topswap_size))
				return 1;
			break;
		case 'n':
			name = optarg;
			break;
		case 'r':
			region_name = optarg;
			break;
		case 's':
			max_table_size = atoi(optarg);
			break;
		case 't':
			fit_type = atoi(optarg);
			break;
		case 'v':
			verbose++;
			break;
		default:
			break;
		}
	}

	if (input_file == NULL) {
		ERROR("No input file given\n");
		usage(argv[0]);
		return 1;
	}

	if (op == ADD_CBFS_OP || op == ADD_REGI_OP) {
		if (fit_type == 0) {
			ERROR("Adding FIT entry, but no type given\n");
			usage(argv[0]);
			return 1;
		} else if (name == NULL) {
			ERROR("Adding FIT entry, but no name set\n");
			usage(argv[0]);
			return 1;
		} else if (max_table_size == 0) {
			ERROR("Maximum table size not given\n");
			usage(argv[0]);
			return 1;
		}
	}
	if (op == ADD_ADDR_OP) {
		if (fit_type == 0) {
			ERROR("Adding FIT entry, but no type given\n");
			usage(argv[0]);
			return 1;
		} else if (max_table_size == 0) {
			ERROR("Maximum table size not given\n");
			usage(argv[0]);
			return 1;
		}
	}

	if (!region_name) {
		ERROR("Region not given\n");
		usage(argv[0]);
		return 1;
	}

	image_file = partitioned_file_reopen(input_file,
					     op != NO_OP || clear_table);

	struct buffer image_region;

	if (!partitioned_file_read_region(&image_region, image_file,
					  region_name)) {
		partitioned_file_close(image_file);
		ERROR("The image will be left unmodified.\n");
		return 1;
	}

	struct buffer bootblock;
	// The bootblock is part of the CBFS on x86
	buffer_clone(&bootblock, &image_region);

	struct cbfs_image image;
	if (cbfs_image_from_buffer(&image, &image_region, headeroffset)) {
		partitioned_file_close(image_file);
		return 1;
	}

	struct fit_table *fit = fit_get_table(&bootblock,
					      convert_to_from_top_aligned,
					      topswap_size);
	if (!fit) {
		partitioned_file_close(image_file);
		ERROR("FIT not found.\n");
		return 1;
	}

	if (clear_table) {
		if (fit_clear_table(fit)) {
			partitioned_file_close(image_file);
			ERROR("Failed to clear table.\n");
			return 1;
		}
	}

	switch (op) {
	case ADD_REGI_OP:
	{
		struct buffer region;

		if (partitioned_file_read_region(&region, image_file, name)) {
			addr = -convert_to_from_top_aligned(&region, 0);
		} else {
			partitioned_file_close(image_file);
			return 1;
		}

		if (fit_add_entry(fit, addr, 0, fit_type, max_table_size)) {
			partitioned_file_close(image_file);
			ERROR("Adding type %u FIT entry\n", fit_type);
			return 1;
		}
		break;
	}
	case ADD_CBFS_OP:
	{
		if (fit_type == FIT_TYPE_MICROCODE) {
			if (fit_add_microcode_file(fit, &image, name,
						   convert_to_from_top_aligned,
						   max_table_size)) {
				return 1;
			}
		} else {
			uint32_t offset, len;
			struct cbfs_file *cbfs_file;

			cbfs_file = cbfs_get_entry(&image, name);
			if (!cbfs_file) {
				partitioned_file_close(image_file);
				ERROR("%s not found in CBFS.\n", name);
				return 1;
			}

			len = ntohl(cbfs_file->len);
			offset = offset_to_ptr(convert_to_from_top_aligned,
					&image.buffer,
					cbfs_get_entry_addr(&image, cbfs_file) +
					ntohl(cbfs_file->offset));


			if (fit_add_entry(fit, offset, len, fit_type,
					  max_table_size)) {
				partitioned_file_close(image_file);
				ERROR("Adding type %u FIT entry\n", fit_type);
				return 1;
			}
		}
		break;
	}
	case ADD_ADDR_OP:
	{
		if (fit_add_entry(fit, addr, 0, fit_type, max_table_size)) {
			partitioned_file_close(image_file);
			ERROR("Adding type %u FIT entry\n", fit_type);
			return 1;
		}
	}
	break;
	case DEL_OP:
	{
		if (fit_delete_entry(fit, table_entry)) {
			partitioned_file_close(image_file);
			ERROR("Deleting FIT entry %zu failed\n", table_entry);
			return 1;
		}
		break;
	}
	case NO_OP:
	default:
		break;
	}

	if (op != NO_OP || clear_table) {
		if (!partitioned_file_write_region(image_file, &bootblock)) {
			ERROR("Failed to write changes to disk.\n");
			partitioned_file_close(image_file);
			return 1;
		}
	}

	if (dump) {
		if (fit_dump(fit)) {
			partitioned_file_close(image_file);
			return 1;
		}
	}

	partitioned_file_close(image_file);

	return 0;
}