/* Copyright 2013-2017 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <skiboot.h>
#include <nvram.h>

/*
 * NVRAM Format as specified in PAPR
 */

struct chrp_nvram_hdr {
	uint8_t		sig;
	uint8_t		cksum;
	be16		len;
	char		name[12];
};

static struct chrp_nvram_hdr *skiboot_part_hdr;

#define NVRAM_SIG_FW_PRIV	0x51
#define NVRAM_SIG_SYSTEM	0x70
#define NVRAM_SIG_FREE		0x7f

#define NVRAM_NAME_COMMON	"common"
#define NVRAM_NAME_FW_PRIV	"ibm,skiboot"
#define NVRAM_NAME_FREE		"wwwwwwwwwwww"

/* 64k should be enough, famous last words... */
#define NVRAM_SIZE_COMMON	0x10000

/* 4k should be enough, famous last words... */
#define NVRAM_SIZE_FW_PRIV	0x1000

static uint8_t chrp_nv_cksum(struct chrp_nvram_hdr *hdr)
{
	struct chrp_nvram_hdr h_copy = *hdr;
	uint8_t b_data, i_sum, c_sum;
	uint8_t *p = (uint8_t *)&h_copy;
	unsigned int nbytes = sizeof(h_copy);

	h_copy.cksum = 0;
	for (c_sum = 0; nbytes; nbytes--) {
		b_data = *(p++);
		i_sum = c_sum + b_data;
		if (i_sum < c_sum)
			i_sum++;
		c_sum = i_sum;
	}
	return c_sum;
}

int nvram_format(void *nvram_image, uint32_t nvram_size)
{
	struct chrp_nvram_hdr *h;
	unsigned int offset = 0;

	prerror("NVRAM: Re-initializing (size: 0x%08x)\n", nvram_size);
	memset(nvram_image, 0, nvram_size);

	/* Create private partition */
	if (nvram_size - offset < NVRAM_SIZE_FW_PRIV)
		return -1;
	h = nvram_image + offset;
	h->sig = NVRAM_SIG_FW_PRIV;
	h->len = cpu_to_be16(NVRAM_SIZE_FW_PRIV >> 4);
	strcpy(h->name, NVRAM_NAME_FW_PRIV);
	h->cksum = chrp_nv_cksum(h);
	prlog(PR_DEBUG, "NVRAM: Created '%s' partition at 0x%08x"
	      " for size 0x%08x with cksum 0x%02x\n",
	      NVRAM_NAME_FW_PRIV, offset,
	      be16_to_cpu(h->len), h->cksum);
	offset += NVRAM_SIZE_FW_PRIV;

	/* Create common partition */
	if (nvram_size - offset < NVRAM_SIZE_COMMON)
		return -1;
	h = nvram_image + offset;
	h->sig = NVRAM_SIG_SYSTEM;
	h->len = cpu_to_be16(NVRAM_SIZE_COMMON >> 4);
	strcpy(h->name, NVRAM_NAME_COMMON);
	h->cksum = chrp_nv_cksum(h);
	prlog(PR_DEBUG, "NVRAM: Created '%s' partition at 0x%08x"
	      " for size 0x%08x with cksum 0x%02x\n",
	      NVRAM_NAME_COMMON, offset,
	      be16_to_cpu(h->len), h->cksum);
	offset += NVRAM_SIZE_COMMON;

	/* Create free space partition */
	if (nvram_size - offset < sizeof(struct chrp_nvram_hdr))
		return -1;
	h = nvram_image + offset;
	h->sig = NVRAM_SIG_FREE;
	h->len = cpu_to_be16((nvram_size - offset) >> 4);
	/* We have the full 12 bytes here */
	memcpy(h->name, NVRAM_NAME_FREE, 12);
	h->cksum = chrp_nv_cksum(h);
	prlog(PR_DEBUG, "NVRAM: Created '%s' partition at 0x%08x"
	      " for size 0x%08x with cksum 0x%02x\n",
	      NVRAM_NAME_FREE, offset, be16_to_cpu(h->len), h->cksum);
	return 0;
}

/*
 * Check that the nvram partition layout is sane and that it
 * contains our required partitions. If not, we re-format the
 * lot of it
 */
int nvram_check(void *nvram_image, const uint32_t nvram_size)
{
	unsigned int offset = 0;
	bool found_common = false;

	skiboot_part_hdr = NULL;

	while (offset + sizeof(struct chrp_nvram_hdr) < nvram_size) {
		struct chrp_nvram_hdr *h = nvram_image + offset;

		if (chrp_nv_cksum(h) != h->cksum) {
			prerror("NVRAM: Partition at offset 0x%x"
				" has bad checksum: 0x%02x vs 0x%02x\n",
				offset, h->cksum, chrp_nv_cksum(h));
			goto failed;
		}
		if (be16_to_cpu(h->len) < 1) {
			prerror("NVRAM: Partition at offset 0x%x"
				" has incorrect 0 length\n", offset);
			goto failed;
		}

		if (h->sig == NVRAM_SIG_SYSTEM &&
		    strcmp(h->name, NVRAM_NAME_COMMON) == 0)
			found_common = true;

		if (h->sig == NVRAM_SIG_FW_PRIV &&
		    strcmp(h->name, NVRAM_NAME_FW_PRIV) == 0)
			skiboot_part_hdr = h;

		offset += be16_to_cpu(h->len) << 4;
		if (offset > nvram_size) {
			prerror("NVRAM: Partition at offset 0x%x"
				" extends beyond end of nvram !\n", offset);
			goto failed;
		}
	}
	if (!found_common) {
		prlog_once(PR_ERR, "NVRAM: Common partition not found !\n");
		goto failed;
	}

	if (!skiboot_part_hdr) {
		prlog_once(PR_ERR, "NVRAM: Skiboot private partition not found !\n");
		goto failed;
	} else {
		/*
		 * The OF NVRAM format requires config strings to be NUL
		 * terminated and unused memory to be set to zero. Well behaved
		 * software should ensure this is done for us, but we should
		 * always check.
		 */
		const char *last_byte = (const char *) skiboot_part_hdr +
			be16_to_cpu(skiboot_part_hdr->len) * 16 - 1;

		if (*last_byte != 0) {
			prerror("NVRAM: Skiboot private partition is not NUL terminated");
			goto failed;
		}
	}

	prlog(PR_INFO, "NVRAM: Layout appears sane\n");
	assert(skiboot_part_hdr);
	return 0;
 failed:
	return -1;
}

static const char *find_next_key(const char *start, const char *end)
{
	/*
	 * Unused parts of the partition are set to NUL. If we hit two
	 * NULs in a row then we assume that we have hit the end of the
	 * partition.
	 */
	if (*start == 0)
		return NULL;

	while (start < end) {
		if (*start == 0)
			return start + 1;

		start++;
	}

	return NULL;
}

/*
 * nvram_query() - Searches skiboot NVRAM partition for a key=value pair.
 *
 * Returns a pointer to a NUL terminated string that contains the value
 * associated with the given key.
 */
const char *nvram_query(const char *key)
{
	const char *part_end, *start;
	int key_len = strlen(key);

	assert(key);

	if (!nvram_has_loaded()) {
		prlog(PR_DEBUG,
			"NVRAM: Query for '%s' must wait for NVRAM to load\n",
			key);
		if (!nvram_wait_for_load()) {
			prlog(PR_CRIT, "NVRAM: Failed to load\n");
			return NULL;
		}
	}

	/*
	 * The running OS can modify the NVRAM as it pleases so we need to be
	 * a little paranoid and check that it's ok before we try parse it.
	 *
	 * NB: nvram_validate() can update skiboot_part_hdr
	 */
	if (!nvram_validate())
		return NULL;

	assert(skiboot_part_hdr);

	part_end = (const char *) skiboot_part_hdr
		+ be16_to_cpu(skiboot_part_hdr->len) * 16 - 1;

	start = (const char *) skiboot_part_hdr
		+ sizeof(*skiboot_part_hdr);

	if (!key_len) {
		prlog(PR_WARNING, "NVRAM: search key is empty!\n");
		return NULL;
	}

	if (key_len > 32)
		prlog(PR_WARNING, "NVRAM: search key '%s' is longer than 32 chars\n", key);

	while (start) {
		int remaining = part_end - start;

		prlog(PR_TRACE, "NVRAM: '%s' (%lu)\n",
			start, strlen(start));

		if (key_len + 1 > remaining)
			return NULL;

		if (!strncmp(key, start, key_len) && start[key_len] == '=') {
			const char *value = &start[key_len + 1];

			prlog(PR_DEBUG, "NVRAM: Searched for '%s' found '%s'\n",
				key, value);

			return value;
		}

		start = find_next_key(start, part_end);
	}

	prlog(PR_DEBUG, "NVRAM: '%s' not found\n", key);

	return NULL;
}


/*
 * nvram_query_eq() - Check if the given 'key' exists and
 * is set to 'value'.
 *
 * Note: Its an error to check for non-existence of a key
 * by passing 'value == NULL' as a key's value can never be
 * NULL in nvram.
 */
bool nvram_query_eq(const char *key, const char *value)
{
	const char *s = nvram_query(key);

	if (!s)
		return false;

	assert(value != NULL);
	return !strcmp(s, value);
}
