/* Copyright 2018 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 * Cros Board Info utility
 */

#include <compile_time_macros.h>
#include <errno.h>
#include <dirent.h>
#include <getopt.h>
#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "cros_board_info.h"
#include "crc8.h"

#define ARGS_MASK_BOARD_VERSION		BIT(0)
#define ARGS_MASK_FILENAME		BIT(1)
#define ARGS_MASK_SIZE			BIT(2)
#define ARGS_MASK_SKU_ID		BIT(3)

/* TODO: Set it by macro */
const char cmd_name[] = "cbi-util";

/* Command line options */
enum {
	OPT_FILENAME,
	OPT_BOARD_VERSION,
	OPT_OEM_ID,
	OPT_SKU_ID,
	OPT_DRAM_PART_NUM,
	OPT_OEM_NAME,
	OPT_MODEL_ID,
	OPT_FW_CONFIG,
	OPT_PCB_SUPPLIER,
	OPT_SSFC,
	OPT_REWORK_ID,
	OPT_SIZE,
	OPT_ERASE_BYTE,
	OPT_SHOW_ALL,
	OPT_HELP,
};

static const struct option opts_create[] = {
	{"file", 1, 0, OPT_FILENAME},
	{"board_version", 1, 0, OPT_BOARD_VERSION},
	{"oem_id", 1, 0, OPT_OEM_ID},
	{"sku_id", 1, 0, OPT_SKU_ID},
	{"dram_part_num", 1, 0, OPT_DRAM_PART_NUM},
	{"oem_name", 1, 0, OPT_OEM_NAME},
	{"model_id", 1, 0, OPT_MODEL_ID},
	{"fw_config", 1, 0, OPT_FW_CONFIG},
	{"pcb_supplier", 1, 0, OPT_PCB_SUPPLIER},
	{"ssfc", 1, 0, OPT_SSFC},
	{"rework_id", 1, 0, OPT_REWORK_ID},
	{"size", 1, 0, OPT_SIZE},
	{"erase_byte", 1, 0, OPT_ERASE_BYTE},
	{NULL, 0, 0, 0}
};

static const struct option opts_show[] = {
	{"file", 1, 0, OPT_FILENAME},
	{"all", 0, 0, OPT_SHOW_ALL},
	{NULL, 0, 0, 0}
};

static const char *field_name[] = {
	/* Same order as enum cbi_data_tag */
	"BOARD_VERSION",
	"OEM_ID",
	"SKU_ID",
	"DRAM_PART_NUM",
	"OEM_NAME",
	"MODEL_ID",
	"FW_CONFIG",
	"PCB_SUPPLIER",
	"SSFC",
	"REWORK_ID",
};
BUILD_ASSERT(ARRAY_SIZE(field_name) == CBI_TAG_COUNT);

const char help_create[] =
	"\n"
	"'%s create [ARGS]' creates an EEPROM image file.\n"
	"Required ARGS are:\n"
	"  --file <file>              Path to output file\n"
	"  --board_version <value>    Board version\n"
	"  --sku_id <value>           SKU ID\n"
	"  --size <size>              Size of output file in bytes\n"
	"\n"
	"Optional ARGS are:\n"
	"  --dram_part_num <string>   DRAM PART NUM\n"
	"  --oem_id <value>           OEM ID\n"
	"  --oem_name <string>        OEM NAME\n"
	"  --erase_byte <uint8>       Byte used for empty space. Default:0xff\n"
	"  --format_version <uint16>  Data format version\n"
	"  --model_id <value>         Model ID\n"
	"  --fw_config <value>        Firmware configuration bit-field\n"
	"  --pcb_supplier <value>     PCB supplier\n"
	"  --ssfc <value>             Second Source Factory Cache bit-field\n"
	"  --rework_id <lvalue>       REWORK_ID\n"
	"\n"
	"<value> must be a positive integer <= 0XFFFFFFFF, <lvalue> must be a\n"
	"  positive integer <= 0xFFFFFFFFFFFFFFFF and field size can be\n"
	"  optionally specified by <value:size> notation: e.g. 0xabcd:4.\n"
	"<size> must be a positive integer <= 0XFFFF.\n"
	"<string> is a string\n"
	"\n";

const char help_show[] =
	"\n"
	"'%s show [ARGS]' shows data in an EEPROM image file.\n"
	"Required ARGS are:\n"
	"  --file <file>               Path to input file\n"
	"Optional ARGS are:\n"
	"  --all                       Dump all information\n"
	"It also validates the contents against the checksum and\n"
	"returns non-zero if validation fails.\n"
	"\n";

struct integer_field {
	uint32_t val;
	int size;
};

struct long_integer_field {
	uint64_t val;
	int size;
};

static void print_help_create(void)
{
	printf(help_create, cmd_name);
}

static void print_help_show(void)
{
	printf(help_show, cmd_name);
}

static void print_help(void)
{
	printf("\nUsage: %s <create|show> [ARGS]\n"
		"\n"
		"Utility for CBI:Cros Board Info images.\n", cmd_name);
	print_help_create();
	print_help_show();
}

static int write_file(const char *filename, const char *buf, int size)
{
	FILE *f;
	int i;

	/* Write to file */
	f = fopen(filename, "wb");
	if (!f) {
		perror("Error opening output file");
		return -1;
	}
	i = fwrite(buf, 1, size, f);
	fclose(f);
	if (i != size) {
		perror("Error writing to file");
		return -1;
	}

	return 0;
}

static uint8_t *read_file(const char *filename, uint32_t *size_ptr)
{
	FILE *f;
	uint8_t *buf;
	long size;

	*size_ptr = 0;

	f = fopen(filename, "rb");
	if (!f) {
		fprintf(stderr, "Unable to open file %s\n", filename);
		return NULL;
	}

	fseek(f, 0, SEEK_END);
	size = ftell(f);
	rewind(f);

	if (size < 0 || size > UINT32_MAX) {
		fclose(f);
		return NULL;
	}

	buf = malloc(size);
	if (!buf) {
		fclose(f);
		return NULL;
	}

	if (1 != fread(buf, size, 1, f)) {
		fprintf(stderr, "Unable to read from %s\n", filename);
		fclose(f);
		free(buf);
		return NULL;
	}

	fclose(f);

	*size_ptr = size;
	return buf;
}

static int estimate_field_size(uint32_t value)
{
	if (value <= UINT8_MAX)
		return 1;
	if (value <= UINT16_MAX)
		return 2;
	return 4;
}

static int parse_integer_field(const char *arg, struct integer_field *f)
{
	uint64_t val;
	char *e;
	char *ch;

	val = strtoull(arg, &e, 0);
	if (val > UINT32_MAX || !*arg || (e && *e && *e != ':')) {
		fprintf(stderr, "Invalid integer value\n");
		return -1;
	}
	f->val = val;

	ch = strchr(arg, ':');
	if (ch) {
		ch++;
		val = strtoull(ch, &e, 0);
		if (val < 1 || 4 < val || !*ch || (e && *e)) {
			fprintf(stderr, "Invalid size suffix\n");
			return -1;
		}
		f->size = val;
	} else {
		f->size = estimate_field_size(f->val);
	}

	if (f->val > (1ull << f->size * 8)) {
		fprintf(stderr, "Value (0x%x) exceeds field size (%d)\n",
			f->val, f->size);
		return -1;
	}

	return 0;
}

static int parse_uint64_field(const char *arg, struct long_integer_field *f)
{
	uint64_t val;
	char *e;
	char *ch;

	val = strtoul(arg, &e, 0);
	/* strtoul sets an errno for invalid input. If the value read is out of
	 * range of representable values by an unsigned long int, the function
	 * returns ULONG_MAX or ULONG_MIN and the errno is set to ERANGE.
	 */
	if (errno == ERANGE || !*arg || (e && *e && *e != ':')) {
		fprintf(stderr, "Invalid integer value\n");
		return -1;
	}
	f->val = val;

	ch = strchr(arg, ':');
	if (ch) {
		ch++;
		val = strtoul(ch, &e, 0);
		if (val < 1 || 8 < val || !*ch || (e && *e)) {
			fprintf(stderr, "Invalid size suffix\n");
			return -1;
		}
		f->size = val;
	} else {
		if (f->val < UINT32_MAX)
			f->size = estimate_field_size(f->val);
		else
			f->size = 8; /* assign default long int size */
	}

	if (f->size < 8 && (f->val > (1ull << f->size * 8))) {
		fprintf(stderr, "Value (0x%llx) exceeds field size (%d)\n",
			(unsigned long long)f->val, f->size);
		return -1;
	}

	return 0;
}

static int cmd_create(int argc, char **argv)
{
	uint8_t *cbi;
	struct board_info {
		struct integer_field ver;
		struct integer_field oem;
		struct integer_field sku;
		struct integer_field model;
		struct integer_field fw_config;
		struct integer_field pcb_supplier;
		struct integer_field ssfc;
		struct long_integer_field rework;
		const char *dram_part_num;
		const char *oem_name;
	} bi;
	struct cbi_header *h;
	int rv;
	uint8_t *p;
	const char *filename;
	uint32_t set_mask = 0;
	uint16_t size;
	uint8_t erase = 0xff;
	int i;

	memset(&bi, 0, sizeof(bi));

	while ((i = getopt_long(argc, argv, "", opts_create, NULL)) != -1) {
		uint64_t val;
		char *e;
		switch (i) {
		case '?': /* Unhandled option */
			print_help_create();
			return -1;
		case OPT_HELP:
			print_help_create();
			return 0;
		case OPT_BOARD_VERSION:
			if (parse_integer_field(optarg, &bi.ver))
				return -1;
			set_mask |= ARGS_MASK_BOARD_VERSION;
			break;
		case OPT_ERASE_BYTE:
			erase = strtoull(optarg, &e, 0);
			if (!*optarg || (e && *e)) {
				fprintf(stderr, "Invalid --erase_byte\n");
				return -1;
			}
			break;
		case OPT_FILENAME:
			filename = optarg;
			set_mask |= ARGS_MASK_FILENAME;
			break;
		case OPT_OEM_ID:
			if (parse_integer_field(optarg, &bi.oem))
				return -1;
			break;
		case OPT_SIZE:
			val = strtoull(optarg, &e, 0);
			if (val > UINT16_MAX || !*optarg || (e && *e)) {
				fprintf(stderr, "Invalid --size\n");
				return -1;
			}
			size = val;
			set_mask |= ARGS_MASK_SIZE;
			break;
		case OPT_SKU_ID:
			if (parse_integer_field(optarg, &bi.sku))
				return -1;
			set_mask |= ARGS_MASK_SKU_ID;
			break;
		case OPT_DRAM_PART_NUM:
			bi.dram_part_num = optarg;
			break;
		case OPT_OEM_NAME:
			bi.oem_name = optarg;
			break;
		case OPT_MODEL_ID:
			if (parse_integer_field(optarg, &bi.model))
				return -1;
			break;
		case OPT_FW_CONFIG:
			if (parse_integer_field(optarg, &bi.fw_config))
				return -1;
			break;
		case OPT_PCB_SUPPLIER:
			if (parse_integer_field(optarg, &bi.pcb_supplier))
				return -1;
			break;
		case OPT_SSFC:
			if (parse_integer_field(optarg, &bi.ssfc))
				return -1;
			break;
		case OPT_REWORK_ID:
			if (parse_uint64_field(optarg, &bi.rework))
				return -1;
			break;
		}
	}

	if (set_mask != (ARGS_MASK_BOARD_VERSION | ARGS_MASK_FILENAME |
			ARGS_MASK_SIZE | ARGS_MASK_SKU_ID)) {
		fprintf(stderr, "Missing required arguments\n");
		print_help_create();
		return -1;
	}

	cbi = malloc(size);
	if (!cbi) {
		fprintf(stderr, "Failed to allocate memory\n");
		return -1;
	}
	memset(cbi, erase, size);

	h = (struct cbi_header *)cbi;
	memcpy(h->magic, cbi_magic, sizeof(cbi_magic));
	h->major_version = CBI_VERSION_MAJOR;
	h->minor_version = CBI_VERSION_MINOR;
	p = h->data;
	p = cbi_set_data(p, CBI_TAG_BOARD_VERSION, &bi.ver.val, bi.ver.size);
	p = cbi_set_data(p, CBI_TAG_OEM_ID, &bi.oem.val, bi.oem.size);
	p = cbi_set_data(p, CBI_TAG_SKU_ID, &bi.sku.val, bi.sku.size);
	p = cbi_set_data(p, CBI_TAG_MODEL_ID, &bi.model.val, bi.model.size);
	p = cbi_set_data(p, CBI_TAG_FW_CONFIG, &bi.fw_config.val,
			 bi.fw_config.size);
	p = cbi_set_data(p, CBI_TAG_PCB_SUPPLIER, &bi.pcb_supplier.val,
			bi.pcb_supplier.size);
	p = cbi_set_data(p, CBI_TAG_SSFC, &bi.ssfc.val, bi.ssfc.size);
	p = cbi_set_data(p, CBI_TAG_REWORK_ID, &bi.rework.val, bi.rework.size);
	p = cbi_set_string(p, CBI_TAG_DRAM_PART_NUM, bi.dram_part_num);
	p = cbi_set_string(p, CBI_TAG_OEM_NAME, bi.oem_name);

	h->total_size = p - cbi;
	h->crc = cbi_crc8(h);

	/* Output image */
	rv = write_file(filename, cbi, size);
	free(cbi);
	if (rv) {
		fprintf(stderr, "Unable to write CBI image to %s\n", filename);
		return rv;
	}

	fprintf(stderr, "CBI image is created successfully\n");

	return 0;
}

static void print_string(const uint8_t *buf, enum cbi_data_tag tag)
{
	struct cbi_data *d = cbi_find_tag(buf, tag);
	const char *name;

	if (!d)
		return;

	name = d->tag < CBI_TAG_COUNT ? field_name[d->tag] : "???";

	printf("    %s: %.*s (%u, %u)\n", name, d->size, (const char *)d->value,
		d->tag, d->size);
}

static void print_integer(const uint8_t *buf, enum cbi_data_tag tag)
{
	uint64_t v;

	struct cbi_data *d = cbi_find_tag(buf, tag);
	const char *name;

	if (!d)
		return;

	name = d->tag < CBI_TAG_COUNT ? field_name[d->tag] : "???";

	switch (d->size) {
	case 1:
		v = *(uint8_t *)d->value;
		break;
	case 2:
		v = *(uint16_t *)d->value;
		break;
	case 4:
		v = *(uint32_t *)d->value;
		break;
	case 8:
		v = *(uint64_t *)d->value;
		break;
	default:
		printf("    %s: Integer of size %d not supported\n",
		       name, d->size);
		return;
	}
	printf("    %s: %llu (0x%llx, %u, %u)\n", name, (unsigned long long)v,
		(unsigned long long)v, d->tag, d->size);
}

static int cmd_show(int argc, char **argv)
{
	uint8_t *buf;
	uint32_t size;
	struct cbi_header *h;
	uint32_t set_mask = 0;
	const char *filename;
	int show_all = 0;
	int i;

	while ((i = getopt_long(argc, argv, "", opts_show, NULL)) != -1) {
		switch (i) {
		case '?': /* Unhandled option */
			print_help_show();
			return -1;
		case OPT_HELP:
			print_help_show();
			return 0;
		case OPT_FILENAME:
			filename = optarg;
			set_mask |= ARGS_MASK_FILENAME;
			break;
		case OPT_SHOW_ALL:
			show_all = 1;
			break;
		}
	}

	if (set_mask != ARGS_MASK_FILENAME) {
		fprintf(stderr, "Missing required arguments\n");
		print_help_show();
		return -1;
	}

	buf = read_file(filename, &size);
	if (!buf) {
		fprintf(stderr, "Unable to read CBI image\n");
		return -1;
	}

	h = (struct cbi_header *)buf;
	printf("CBI image: %s\n", filename);

	if (memcmp(h->magic, cbi_magic, sizeof(cbi_magic))) {
		fprintf(stderr, "Invalid Magic\n");
		free(buf);
		return -1;
	}

	if (cbi_crc8(h) != h->crc) {
		fprintf(stderr, "Invalid CRC\n");
		free(buf);
		return -1;
	}

	printf("  TOTAL_SIZE: %u\n", h->total_size);
	if (show_all)
		printf("  CBI_VERSION: %u\n", h->version);
	printf("  Data Field: name: value (hex, tag, size)\n");
	print_integer(buf, CBI_TAG_BOARD_VERSION);
	print_integer(buf, CBI_TAG_OEM_ID);
	print_integer(buf, CBI_TAG_SKU_ID);
	print_integer(buf, CBI_TAG_MODEL_ID);
	print_integer(buf, CBI_TAG_FW_CONFIG);
	print_integer(buf, CBI_TAG_PCB_SUPPLIER);
	print_integer(buf, CBI_TAG_SSFC);
	print_integer(buf, CBI_TAG_REWORK_ID);
	print_string(buf, CBI_TAG_DRAM_PART_NUM);
	print_string(buf, CBI_TAG_OEM_NAME);

	free(buf);

	printf("Data validated successfully\n");
	return 0;
}

int main(int argc, char **argv)
{
	if (argc < 2) {
		fprintf(stderr, "Unknown option or missing value\n");
		print_help();
		return -1;
	}

	if (!strncmp(argv[1], "create", sizeof("create")))
		return cmd_create(--argc, ++argv);
	else if (!strncmp(argv[1], "show", sizeof("show")))
		return cmd_show(--argc, ++argv);

	fprintf(stderr, "Unknown option or missing value\n");
	print_help();

	return -1;
}
