/* SPDX-License-Identifier: GPL-2.0-only */

#include <b64_decode.h>
#include <cbmem.h>
#include <console/console.h>
#include <stdlib.h>
#include <string.h>

#include <vendorcode/google/chromeos/chromeos.h>
#include <drivers/vpd/vpd.h>

/*
 * This file provides functions looking in the VPD for WiFi calibration data,
 * and if found, copying the calibration blobs into CBMEM.
 *
 * Per interface calibration data is stored in the VPD in opaque blobs. The
 * keys of the blobs follow one of two possible patterns:
 * "wifi_base64_calibration<N>" or "wifi_calibration<N>", where <N> is the
 * interface number.
 *
 * This function accommodates up to 4 interfaces. All calibration blobs found
 * in the VPD are packed into a single CBMEM entry as describe by the
 * structures below:
 */

/* This structure describes a single calibration data blob */
struct calibration_blob {
	uint32_t blob_size;  /* Total size. rounded up to fall on a 4 byte
				   boundary. */
	uint32_t key_size;   /* Size of the name of this entry, \0 included. */
	uint32_t value_size; /* Size of the value of this entry */
	/* Zero terminated name(key) goes here, immediately followed by value */
};

/*
 * This is the structure of the CBMEM entry containing WiFi calibration blobs.
 * It starts with the total size (header size included) followed by an
 * arbitrary number of concatenated 4 byte aligned calibration blobs.
 */
struct calibration_entry {
	uint32_t size;
	struct calibration_blob entries[0];  /* A varialble size container. */
};


#define MAX_WIFI_INTERFACE_COUNT 4

/*
 * Structure of the cache to keep information about calibration blobs present
 * in the VPD, one cache entry per VPD blob.
 *
 * Maintaing the cache allows to scan the VPD once, determine the CBMEM entry
 * memory requirements, then allocate as much room as necessary and fill it
 * up.
 */
struct vpd_blob_cache_t {
	/* The longest name template must fit with an extra character. */
	char key_name[40];
	void  *value_pointer;
	unsigned blob_size;
	unsigned key_size;
	unsigned value_size;
};

static const char * const templates[] = {
	"wifi_base64_calibrationX",
	"wifi_calibrationX"
};

/*
 * Scan the VPD for WiFi calibration data, checking for all possible key names
 * and caching discovered blobs.
 *
 * Return the sum of sizes of all blobs, as stored in CBMEM.
 */
static size_t fill_up_entries_cache(struct vpd_blob_cache_t *cache,
				    size_t max_entries, size_t *filled_entries)
{
	int i;
	int cbmem_entry_size = 0;
	size_t used_entries = 0;


	for (i = 0;
	     (i < ARRAY_SIZE(templates)) && (used_entries < max_entries);
	     i++) {
		int j;
		const int index_location = strlen(templates[i]) - 1;
		const int key_length = index_location + 2;

		if (key_length > sizeof(cache->key_name))
			continue;

		for (j = 0; j < MAX_WIFI_INTERFACE_COUNT; j++) {
			const void *payload;
			void *decoded_payload;
			int payload_size;
			size_t decoded_size;

			strcpy(cache->key_name, templates[i]);
			cache->key_name[index_location] = j + '0';

			payload = vpd_find(cache->key_name, &payload_size, VPD_RO_THEN_RW);
			if (!payload)
				continue;

			decoded_size = B64_DECODED_SIZE(payload_size);
			decoded_payload = malloc(decoded_size);
			if (!decoded_payload) {
				printk(BIOS_ERR,
				       "%s: failed allocating %zd bytes\n",
				       __func__, decoded_size);
				continue;
			}

			decoded_size = b64_decode(payload, payload_size,
						  decoded_payload);
			if (!decoded_size) {
				free(decoded_payload);
				printk(BIOS_ERR, "%s: failed decoding %s\n",
				       __func__, cache->key_name);
				continue;
			}

			cache->value_pointer = decoded_payload;
			cache->key_size = key_length;
			cache->value_size = decoded_size;
			cache->blob_size =
				ALIGN(sizeof(struct calibration_blob) +
				      cache->key_size +
				      cache->value_size, 4);
			cbmem_entry_size += cache->blob_size;

			used_entries++;
			if (used_entries == max_entries)
				break;

			cache++;
		}
	}

	*filled_entries = used_entries;
	return cbmem_entry_size;
}

void cbmem_add_vpd_calibration_data(void)
{
	size_t cbmem_entry_size, filled_entries;
	struct calibration_entry *cbmem_entry;
	struct calibration_blob *cal_blob;
	int i;
	/*
	 * Allocate one more cache entry than max required, to make sure that
	 * the last entry can be identified by the key size of zero.
	 */
	struct vpd_blob_cache_t vpd_blob_cache[ARRAY_SIZE(templates) *
					       MAX_WIFI_INTERFACE_COUNT];

	cbmem_entry_size = fill_up_entries_cache(vpd_blob_cache,
						 ARRAY_SIZE(vpd_blob_cache),
						 &filled_entries);

	if (!cbmem_entry_size)
		return; /* No calibration data found in the VPD. */

	cbmem_entry_size += sizeof(struct calibration_entry);
	cbmem_entry = cbmem_add(CBMEM_ID_WIFI_CALIBRATION, cbmem_entry_size);
	if (!cbmem_entry) {
		printk(BIOS_ERR, "%s: no room in cbmem to add %zd bytes\n",
		       __func__, cbmem_entry_size);
		return;
	}

	cbmem_entry->size = cbmem_entry_size;

	/* Copy cached data into the CBMEM entry. */
	cal_blob = cbmem_entry->entries;

	for (i = 0; i < filled_entries; i++) {
		/* Use this as a pointer to the current cache entry. */
		struct vpd_blob_cache_t *cache = vpd_blob_cache + i;
		char *pointer;

		cal_blob->blob_size = cache->blob_size;
		cal_blob->key_size = cache->key_size;
		cal_blob->value_size = cache->value_size;

		/* copy the key */
		pointer = (char *)(cal_blob + 1);
		memcpy(pointer, cache->key_name, cache->key_size);

		/* and the value */
		pointer += cache->key_size;
		memcpy(pointer, cache->value_pointer, cache->value_size);
		free(cache->value_pointer);

		printk(BIOS_INFO, "%s: added %s to CBMEM\n",
		       __func__, cache->key_name);

		cal_blob = (struct calibration_blob *)
			((char *)cal_blob + cal_blob->blob_size);
	}
}