/* AMD Family 17h and later BIOS compressor */
/* SPDX-License-Identifier: GPL-2.0-only */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <getopt.h>
#include <elfparsing.h>
#include "zlib.h"

#define DEBUG_FILE 0

#define HDR_SIZE 256
#define UNCOMP_MAX 0x300000

#define DIR_UNDEF 0
#define DIR_COMP 1
#define DIR_UNCOMP 2

typedef struct _header {
	uint32_t rsvd1[5];
	uint32_t size;
	uint32_t rsvd2[58];
}  __attribute__((packed)) header;

static const char *optstring  = "i:o:cm:uh";

static struct option long_options[] = {
	{"infile",           required_argument, 0, 'i' },
	{"outfile",          required_argument, 0, 'o' },
	{"compress",         no_argument,       0, 'c' },
	{"maxsize",          required_argument, 0, 'm' },
	{"uncompress",       no_argument,       0, 'u' },
	{"help",             no_argument,       0, 'h' },
};

static void usage(void)
{
	printf("<name>: Extract or create a zlib compressed BIOS binary\n");
	printf("        image.  A compressed image contains a 256 byte\n");
	printf("        header with a 32-bit size at 0x14.\n");
	printf("Usage: <name> -i in_file -o out_file -[c|u]\n");
	printf("-i | --infile <FILE>         Input file\n");
	printf("-o | --outfile <FILE>        Output file\n");
	printf("-c | --compress              Compress\n");
	printf("-m | --maxsize <HEX_VAL>     Maximum uncompressed size (optional)\n");
	printf("                              * On compress: verify uncompressed size\n");
	printf("                                will be less than or equal maxsize\n");
	printf("                              * On uncompress: override default buffer size\n");
	printf("                                allocation of 0x%x bytes\n", UNCOMP_MAX);
	printf("-u | --uncompress            Uncompress\n");
	printf("-h | --help                  Display this message\n");

	exit(1);
}

static int do_file(char *name, size_t *size, int oflag)
{
	struct stat fd_stat;
	int fd;

	fd = open(name, oflag, 0666);
	if (fd < 0)
		return -1;

	if (fstat(fd, &fd_stat)) {
		close(fd);
		return -1;
	}

	if (size)
		*size = fd_stat.st_size;
	return fd;
}

static int parse_elf_to_xip_ram(const struct buffer *input,
					struct buffer *output)
{
	struct parsed_elf pelf;

	if (parse_elf(input, &pelf, ELF_PARSE_ALL))
		return 1;
	if (buffer_create(output, pelf.phdr->p_filesz, "") != 0)
		return 1;

	memcpy(output->data, input->data + pelf.phdr->p_offset, output->size);

	return 0;
}

static int convert_elf(struct buffer *buf)
{
	struct buffer out;

	if (parse_elf_to_xip_ram(buf, &out)) {
		printf("\tError parsing ELF file\n");
		return -1;
	}

	/* Discard the elf file in buf and replace with the progbits */
	free(buf->data);
	buf->data = out.data;
	buf->size = out.size;

	return 0;
}

static int iself(const void *input)
{
	const Elf32_Ehdr *ehdr = input;
	return !memcmp(ehdr->e_ident, ELFMAG, 4);
}

/* todo: Consider using deflate() and inflate() instead of compress() and
 * decompress(), especially if memory allocation somehow becomes a problem.
 * Those two functions can operate on streams and process chunks of data.
 */

/* Build the required header and follow it with the compressed image.  Detect
 * whether the input is an elf image, and if so, compress only the progbits.
 *
 *     header
 *   0 +------+-------+-------+-------+
 *     |      |       |       |       |
 *     +----------------------+-------+
 *     |      | size  |       |       |
 *     +----------------------+-------+
 *     |      |       |       |       |
 *     |      |       |          ...  |
 * 256 +------------------------------+
 *     |compressed image              |
 *     |   ...                        |
 *     |   ...                        |
 *     |   ...                        |
 *   n +------------------------------+
 */
static void do_compress(char *outf, char *inf, size_t max_size)
{
	int out_fd, in_fd;
	struct buffer inbf, outbf;
	int err;

	in_fd = do_file(inf, &inbf.size, O_RDONLY);
	if (in_fd < 0) {
		printf("\tError opening input file %s\n", inf);
		err = 1;
		goto out;
	}

	out_fd = do_file(outf, 0, O_CREAT | O_WRONLY);
	if (out_fd < 0) {
		printf("\tError opening output file %s\n", outf);
		err = 1;
		goto out_close_in;
	}

	inbf.data = calloc(inbf.size, 1);
	if (!inbf.data) {
		printf("\tError allocating 0x%zx bytes for input buffer\n", inbf.size);
		err = 1;
		goto out_close_out;
	}

	if (read(in_fd, inbf.data, inbf.size) != (ssize_t)inbf.size) {
		printf("\tError reading input file %s\n", inf);
		err = 1;
		goto out_free_in;
	}

	if (iself(inbf.data)) {
		if (convert_elf(&inbf)) {
			err = 1;
			goto out_free_in;
		}
	}

	if (max_size && inbf.size > max_size) {
		printf("\tError - size (%zx) exceeds specified max_size (%zx)\n",
				inbf.size, max_size);
		err = 1;
		goto out_free_in;
	}

	outbf.size = inbf.size; /* todo: tbd worst case? */
	outbf.size += sizeof(header);
	outbf.data = calloc(outbf.size, 1);
	if (!outbf.size) {
		printf("\tError allocating 0x%zx bytes for output buffer\n", outbf.size);
		err = 1;
		goto out_free_in;
	}

	err = compress((Bytef *)(outbf.data + sizeof(header)), &outbf.size,
				(Bytef *)inbf.data, inbf.size);
	if (err != Z_OK) {
		printf("\tzlib compression error %d\n", err);
		err = 1;
		goto out_free_out;
	}

	if (DEBUG_FILE)
		printf("\tCompressed 0x%zx bytes into 0x%zx\n", inbf.size,
				outbf.size - sizeof(header));

	((header *)outbf.data)->size = outbf.size;

	if (write(out_fd, outbf.data, outbf.size + sizeof(header))
				!= (ssize_t)(outbf.size + sizeof(header))) {
		printf("\tError writing to %s\n", outf);
		err = 1;
		/* fall through to out_free_out */
	}

out_free_out:
	free(outbf.data);
out_free_in:
	free(inbf.data);
out_close_out:
	close(out_fd);
out_close_in:
	close(in_fd);
out:
	if (err)
		exit(err);
}

static void do_uncompress(char *outf, char *inf, size_t max_size)
{
	int out_fd, in_fd;
	char *in_buf, *out_buf;
	size_t size_unc, size_comp;
	size_t bytes;
	int err;

	in_fd = do_file(inf, &size_comp, O_RDONLY);
	if (in_fd < 0) {
		printf("\tError opening input file %s\n", inf);
		err = 1;
		goto out;
	}

	out_fd = do_file(outf, 0, O_CREAT | O_WRONLY);
	if (out_fd < 0) {
		printf("\tError opening output file %s\n", outf);
		err = 1;
		goto out_close_in;
	}

	in_buf = calloc(size_comp, 1);
	if (!in_buf) {
		printf("\tError allocating 0x%zx bytes for input buffer\n", size_comp);
		err = 1;
		goto out_close_out;
	}

	bytes = read(in_fd, in_buf, size_comp);
	if (bytes != size_comp) {
		printf("\tError reading input file %s\n", inf);
		err = 1;
		goto out_free_in;
	}

	size_comp = ((header *)in_buf)->size;

	size_unc = max_size ? max_size : UNCOMP_MAX;
	out_buf = calloc(size_unc, 1);
	if (!out_buf) {
		printf("\tError allocating 0x%zx bytes for output buffer\n", size_unc);
		err = 1;
		goto out_free_in;
	}

	err = uncompress((Bytef *)out_buf, &size_unc,
				(Bytef *)in_buf + sizeof(header), size_comp);
	if (err != Z_OK) {
		printf("\tzlib uncompression error %d\n", err);
		err = 1;
		goto out_free_out;
	}

	if (DEBUG_FILE)
		printf("Uncompressed 0x%zx bytes into 0x%zx\n", size_comp, size_unc);

	bytes = write(out_fd, out_buf, size_unc);
	if (bytes != size_unc) {
		printf("\tError writing to %s\n", outf);
		err = 1;
		/* fall through to out_free_out */
	}

out_free_out:
	free(out_buf);
out_free_in:
	free(in_buf);
out_close_out:
	close(out_fd);
out_close_in:
	close(in_fd);
out:
	if (err)
		exit(err);
}

int main(int argc, char *argv[])
{
	int c;
	char *inf = 0, *outf = 0, *scratch;
	int direction = DIR_UNDEF;
	size_t max_size = 0;

	while (1) {
		int optindex = 0;

		c = getopt_long(argc, argv, optstring, long_options, &optindex);
		if (c == -1)
			break;

		switch (c) {
		case 'i':
			inf = optarg;
			break;
		case 'o':
			outf = optarg;
			break;
		case 'c':
			if (direction != DIR_UNDEF)
				usage();
			direction = DIR_COMP;
			break;
		case 'u':
			if (direction != DIR_UNDEF)
				usage();
			direction = DIR_UNCOMP;
			break;
		case 'm':
			max_size = strtoull(optarg, &scratch, 16);
			break;
		case 'h':
			usage();
		}
	}
	if (!inf || !outf || direction == DIR_UNDEF)
		usage();

	if (direction == DIR_COMP)
		do_compress(outf, inf, max_size);
	else
		do_uncompress(outf, inf, max_size);

	return 0;
}