// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2023 Rafał Miłecki <rafal@milecki.pl>
 */

#include <byteswap.h>
#include <endian.h>
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#if !defined(__BYTE_ORDER)
#error "Unknown byte order"
#endif

#if __BYTE_ORDER == __BIG_ENDIAN
#define cpu_to_le32(x)	bswap_32(x)
#define le32_to_cpu(x)	bswap_32(x)
#define cpu_to_be32(x)	(x)
#define be32_to_cpu(x)	(x)
#elif __BYTE_ORDER == __LITTLE_ENDIAN
#define cpu_to_le32(x)	(x)
#define le32_to_cpu(x)	(x)
#define cpu_to_be32(x)	bswap_32(x)
#define be32_to_cpu(x)	bswap_32(x)
#else
#error "Unsupported endianness"
#endif

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))

#define BCMCLM_MAGIC		"CLM DATA"

/* Raw data */

struct bcmclm_header {
	char magic[8];
	uint32_t unk0;
	uint8_t unk1[2];
	char api[20];
	char compiler[10];
	uint32_t virtual_header_address;
	uint32_t lookup_table_address;
	char clm_import_ver[30];
	char manufacturer[22];
};

struct bcmclm_lookup_table {
	uint32_t channels_2ghz;
	uint32_t channels_5ghz;
	uint32_t offset2;
	uint32_t offset3;
	uint32_t offset4;
	uint32_t offset5;
	uint32_t offset6;
	uint32_t offset7;
	uint32_t offset8;
	uint32_t offset9;
	uint32_t offset10;
	uint32_t offset11;
	uint32_t offset12;
	uint32_t offset13;
	uint32_t offset14;
	uint32_t offset15;
	uint32_t offset16;
	uint32_t offset17;
	uint32_t offset18;
	uint32_t offset19;
	uint32_t offset20;
	uint32_t offset21;
	uint32_t offset22;
	uint32_t offset23;
	uint32_t offset_creation_date;
	uint32_t offset25;
	uint32_t offset26;
	uint32_t offset27;
	uint32_t offset28;
	uint32_t offset29;
	uint32_t offset30;
	uint32_t offset31;
	uint32_t offset32;
	uint32_t offset33;
	uint32_t offset34;
	uint32_t offset35;
	uint32_t offset36;
	uint32_t offset37;
	uint32_t offset38;
	uint32_t offset39;
	uint32_t offset40;
	uint32_t offset41;
	uint32_t offset42;
	uint32_t offset43;
	uint32_t offset44;
	uint32_t offset45;
	uint32_t offset46;
	uint32_t offset47;
};

struct bcmclm_channels_lookup {
	uint32_t num;
	uint32_t offset;
};

struct bcmclm_channels_range {
	uint8_t first;
	uint8_t last;
	uint8_t unknown;
};

/* Parsed info */

struct bcmclm_info {
	struct bcmclm_header header;
	struct bcmclm_lookup_table lookup_table;
	size_t file_size;
	size_t clm_offset;
	size_t offsets_fixup;
};

static inline size_t bcmclm_min(size_t x, size_t y)
{
	return x < y ? x : y;
}

/**************************************************
 * Helpers
 **************************************************/

static FILE *bcmclm_open(const char *pathname, const char *mode)
{
	struct stat st;

	if (pathname)
		return fopen(pathname, mode);

	if (isatty(fileno(stdin))) {
		fprintf(stderr, "Reading from TTY stdin is unsupported\n");
		return NULL;
	}

	if (fstat(fileno(stdin), &st)) {
		fprintf(stderr, "Failed to fstat stdin: %d\n", -errno);
		return NULL;
	}

	if (S_ISFIFO(st.st_mode)) {
		fprintf(stderr, "Reading from pipe stdin is unsupported\n");
		return NULL;
	}

	return stdin;
}

static void bcmclm_close(FILE *fp)
{
	if (fp != stdin)
		fclose(fp);
}

/**************************************************
 * Existing CLM parser
 **************************************************/

static int bcmclm_search(FILE *fp, struct bcmclm_info *info)
{
	uint8_t buf[1024];
	size_t offset = 0;
	size_t bytes;
	int i;

	while ((bytes = fread(buf, 1, sizeof(buf), fp)) == sizeof(buf)) {
		for (i = 0; i < bytes - 12; i += 4) {
			uint32_t unk = le32_to_cpu(*(uint32_t *)(&buf[i + 8]));

			if (!memcmp(&buf[i], BCMCLM_MAGIC, 8) && !(unk & 0xff00ffff)) {
				info->clm_offset = offset + i;

				printf("Found CLM at offset 0x%zx\n", info->clm_offset);
				printf("\n");

				return 0;
			}
		}

		offset += bytes;
	}

	return -ENOENT;
}

static int bcmclm_parse(FILE *fp, struct bcmclm_info *info)
{
	struct bcmclm_header *header = &info->header;
	struct bcmclm_lookup_table *lookup_table = &info->lookup_table;
	struct stat st;
	int err = 0;

	/* File size */

	if (fstat(fileno(fp), &st)) {
		err = -errno;
		fprintf(stderr, "Failed to fstat: %d\n", err);
		return err;
	}
	info->file_size = st.st_size;

	/* Header */

	fseek(fp, info->clm_offset, SEEK_SET);

	if (fread(header, 1, sizeof(*header), fp) != sizeof(*header)) {
		fprintf(stderr, "Failed to read CLM header\n");
		return -EIO;
	}

	if (strncmp(header->magic, BCMCLM_MAGIC, 8)) {
		fprintf(stderr, "Invalid CLM header magic\n");
		return -EPROTO;
	}

	info->offsets_fixup = info->clm_offset - le32_to_cpu(header->virtual_header_address);

	/* Lookup table */

	fseek(fp, le32_to_cpu(info->header.lookup_table_address) + info->offsets_fixup, SEEK_SET);

	if (fread(lookup_table, 1, sizeof(*lookup_table), fp) != sizeof(*lookup_table)) {
		fprintf(stderr, "Failed to read lookup table\n");
		return -EIO;
	}

	return 0;
}

/**************************************************
 * Info
 **************************************************/

static void bcmclm_print_channels(FILE *fp, struct bcmclm_info *info, const char *band, size_t num, size_t offset)
{
	struct bcmclm_channels_range *ranges;
	size_t read;
	int i;

	ranges = calloc(num, sizeof(struct bcmclm_channels_range));
	if (!ranges) {
		return;
	}

	fseek(fp, offset + info->offsets_fixup, SEEK_SET);

	read = fread(ranges, sizeof(struct bcmclm_channels_range), num, fp);
	if (read != num) {
		goto out;
	}

	for (i = 0; i < num; i++) {
		printf("%3s GHz band channels: %d - %d\n", band, le32_to_cpu(ranges[i].first), le32_to_cpu(ranges[i].last));
	}

out:
	free(ranges);
}

static void bcmclm_print_lookup_data(FILE *fp, struct bcmclm_info *info)
{
	uint8_t buf[64];
	size_t bytes;

	if (info->lookup_table.channels_2ghz) {
		struct bcmclm_channels_lookup channels;

		fseek(fp, le32_to_cpu(info->lookup_table.channels_2ghz) + info->offsets_fixup, SEEK_SET);

		bytes = fread(&channels, 1, sizeof(channels), fp);
		if (bytes == sizeof(channels)) {
			bcmclm_print_channels(fp, info, "2.4", le32_to_cpu(channels.num), le32_to_cpu(channels.offset));
		}
	}

	if (info->lookup_table.channels_5ghz) {
		struct bcmclm_channels_lookup channels;

		fseek(fp, le32_to_cpu(info->lookup_table.channels_5ghz) + info->offsets_fixup, SEEK_SET);

		bytes = fread(&channels, 1, sizeof(channels), fp);
		if (bytes == sizeof(channels)) {
			bcmclm_print_channels(fp, info, "5", le32_to_cpu(channels.num), le32_to_cpu(channels.offset));
		}
	}

	if (info->lookup_table.offset_creation_date) {
		fseek(fp, le32_to_cpu(info->lookup_table.offset_creation_date) + info->offsets_fixup, SEEK_SET);

		bytes = fread(buf, 1, sizeof(buf), fp);
		if (bytes) {
			printf("Creation date: %s\n", buf);
		}
	}
}

static int bcmclm_info(int argc, char **argv)
{
	struct bcmclm_info info = {};
	const char *pathname = NULL;
	int search = 0;
	FILE *fp;
	int c;
	int err = 0;

	while ((c = getopt(argc, argv, "i:s")) != -1) {
		switch (c) {
		case 'i':
			pathname = optarg;
			break;
		case 's':
			search = 1;
			break;
		}
	}

	fp = bcmclm_open(pathname, "r");
	if (!fp) {
		fprintf(stderr, "Failed to open CLM\n");
		err = -EACCES;
		goto out;
	}

	if (search) {
		err = bcmclm_search(fp, &info);
		if (err) {
			fprintf(stderr, "Failed to find CLM in input file\n");
			goto err_close;
		}
	}

	err = bcmclm_parse(fp, &info);
	if (err) {
		fprintf(stderr, "Failed to parse CLM\n");
		goto err_close;
	}

	printf("API: %s\n", info.header.api);
	printf("Compiler: %s\n", info.header.compiler);
	printf("clm_import_ver: %s\n", info.header.clm_import_ver);
	printf("Manufacturer: %s\n", info.header.manufacturer);
	printf("\n");
	printf("Virtual header address: 0x%08x (real: 0x%zx)\n", le32_to_cpu(info.header.virtual_header_address), le32_to_cpu(info.header.virtual_header_address) + info.offsets_fixup);
	printf("Virtual lookup table address: 0x%08x (real: 0x%zx)\n", le32_to_cpu(info.header.lookup_table_address), le32_to_cpu(info.header.lookup_table_address) + info.offsets_fixup);
	printf("\n");

	bcmclm_print_lookup_data(fp, &info);

err_close:
	bcmclm_close(fp);
out:
	return err;
}

/**************************************************
 * Start
 **************************************************/

static void usage()
{
	printf("Usage:\n");
	printf("\n");
	printf("Info about CLM:\n");
	printf("\tbcmclm info <options>\n");
	printf("\t-i <file>\t\t\t\t\tinput CLM\n");
	printf("\t-s\t\t\t\t\tsearch for CLM data in bigger file\n");
	printf("\n");
	printf("Examples:\n");
	printf("\tbcmclm info -i x.clm\n");
	printf("\tbcmclm info -s -i brcmfmac4366c-pcie.bin\n");
}

int main(int argc, char **argv)
{
	if (argc > 1) {
		optind++;
		if (!strcmp(argv[1], "info"))
			return bcmclm_info(argc, argv);
	}

	usage();

	return 0;
}
