/*
 * NVRAM variable manipulation (common)
 *
 * Copyright 2004, Broadcom Corporation
 * Copyright 2009-2010, OpenWrt.org
 * All Rights Reserved.
 *
 * THIS SOFTWARE IS OFFERED "AS IS", AND BROADCOM GRANTS NO WARRANTIES OF ANY
 * KIND, EXPRESS OR IMPLIED, BY STATUTE, COMMUNICATION OR OTHERWISE. BROADCOM
 * SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A SPECIFIC PURPOSE OR NONINFRINGEMENT CONCERNING THIS SOFTWARE.
 *
 */

#include "nvram.h"

#define TRACE(msg) \
	printf("%s(%i) in %s(): %s\n", \
		__FILE__, __LINE__, __FUNCTION__, msg ? msg : "?")

size_t nvram_erase_size = 0;


/*
 * -- Helper functions --
 */

/* String hash */
static uint32_t hash(const char *s)
{
	uint32_t hash = 0;

	while (*s)
		hash = 31 * hash + *s++;

	return hash;
}

/* Free all tuples. */
static void _nvram_free(nvram_handle_t *h)
{
	uint32_t i;
	nvram_tuple_t *t, *next;

	/* Free hash table */
	for (i = 0; i < NVRAM_ARRAYSIZE(h->nvram_hash); i++) {
		for (t = h->nvram_hash[i]; t; t = next) {
			next = t->next;
			free(t);
		}
		h->nvram_hash[i] = NULL;
	}

	/* Free dead table */
	for (t = h->nvram_dead; t; t = next) {
		next = t->next;
		free(t);
	}

	h->nvram_dead = NULL;
}

/* (Re)allocate NVRAM tuples. */
static nvram_tuple_t * _nvram_realloc( nvram_handle_t *h, nvram_tuple_t *t,
	const char *name, const char *value )
{
	if ((strlen(value) + 1) > NVRAM_SPACE)
		return NULL;

	if (!t) {
		if (!(t = malloc(sizeof(nvram_tuple_t) + strlen(name) + 1)))
			return NULL;

		/* Copy name */
		t->name = (char *) &t[1];
		strcpy(t->name, name);

		t->value = NULL;
	}

	/* Copy value */
	if (!t->value || strcmp(t->value, value))
	{
		if(!(t->value = (char *) realloc(t->value, strlen(value)+1)))
			return NULL;

		strcpy(t->value, value);
		t->value[strlen(value)] = '\0';
	}

	return t;
}

/* (Re)initialize the hash table. */
static int _nvram_rehash(nvram_handle_t *h)
{
	nvram_header_t *header = nvram_header(h);
	char buf[] = "0xXXXXXXXX", *name, *value, *eq;

	/* (Re)initialize hash table */
	_nvram_free(h);

	/* Parse and set "name=value\0 ... \0\0" */
	name = (char *) &header[1];

	for (; *name; name = value + strlen(value) + 1) {
		if (!(eq = strchr(name, '=')))
			break;
		*eq = '\0';
		value = eq + 1;
		nvram_set(h, name, value);
		*eq = '=';
	}

	/* Set special SDRAM parameters */
	if (!nvram_get(h, "sdram_init")) {
		sprintf(buf, "0x%04X", (uint16_t)(header->crc_ver_init >> 16));
		nvram_set(h, "sdram_init", buf);
	}
	if (!nvram_get(h, "sdram_config")) {
		sprintf(buf, "0x%04X", (uint16_t)(header->config_refresh & 0xffff));
		nvram_set(h, "sdram_config", buf);
	}
	if (!nvram_get(h, "sdram_refresh")) {
		sprintf(buf, "0x%04X",
			(uint16_t)((header->config_refresh >> 16) & 0xffff));
		nvram_set(h, "sdram_refresh", buf);
	}
	if (!nvram_get(h, "sdram_ncdl")) {
		sprintf(buf, "0x%08X", header->config_ncdl);
		nvram_set(h, "sdram_ncdl", buf);
	}

	return 0;
}


/*
 * -- Public functions --
 */

/* Get nvram header. */
nvram_header_t * nvram_header(nvram_handle_t *h)
{
	return (nvram_header_t *) &h->mmap[NVRAM_START(nvram_erase_size)];
}

/* Get the value of an NVRAM variable. */
char * nvram_get(nvram_handle_t *h, const char *name)
{
	uint32_t i;
	nvram_tuple_t *t;
	char *value;

	if (!name)
		return NULL;

	/* Hash the name */
	i = hash(name) % NVRAM_ARRAYSIZE(h->nvram_hash);

	/* Find the associated tuple in the hash table */
	for (t = h->nvram_hash[i]; t && strcmp(t->name, name); t = t->next);

	value = t ? t->value : NULL;

	return value;
}

/* Set the value of an NVRAM variable. */
int nvram_set(nvram_handle_t *h, const char *name, const char *value)
{
	uint32_t i;
	nvram_tuple_t *t, *u, **prev;

	/* Hash the name */
	i = hash(name) % NVRAM_ARRAYSIZE(h->nvram_hash);

	/* Find the associated tuple in the hash table */
	for (prev = &h->nvram_hash[i], t = *prev;
		 t && strcmp(t->name, name); prev = &t->next, t = *prev);

	/* (Re)allocate tuple */
	if (!(u = _nvram_realloc(h, t, name, value)))
		return -12; /* -ENOMEM */

	/* Value reallocated */
	if (t && t == u)
		return 0;

	/* Move old tuple to the dead table */
	if (t) {
		*prev = t->next;
		t->next = h->nvram_dead;
		h->nvram_dead = t;
	}

	/* Add new tuple to the hash table */
	u->next = h->nvram_hash[i];
	h->nvram_hash[i] = u;

	return 0;
}

/* Unset the value of an NVRAM variable. */
int nvram_unset(nvram_handle_t *h, const char *name)
{
	uint32_t i;
	nvram_tuple_t *t, **prev;

	if (!name)
		return 0;

	/* Hash the name */
	i = hash(name) % NVRAM_ARRAYSIZE(h->nvram_hash);

	/* Find the associated tuple in the hash table */
	for (prev = &h->nvram_hash[i], t = *prev;
		 t && strcmp(t->name, name); prev = &t->next, t = *prev);

	/* Move it to the dead table */
	if (t) {
		*prev = t->next;
		t->next = h->nvram_dead;
		h->nvram_dead = t;
	}

	return 0;
}

/* Get all NVRAM variables. */
nvram_tuple_t * nvram_getall(nvram_handle_t *h)
{
	int i;
	nvram_tuple_t *t, *l, *x;

	l = NULL;

	for (i = 0; i < NVRAM_ARRAYSIZE(h->nvram_hash); i++) {
		for (t = h->nvram_hash[i]; t; t = t->next) {
			if( (x = (nvram_tuple_t *) malloc(sizeof(nvram_tuple_t))) != NULL )
			{
				x->name  = t->name;
				x->value = t->value;
				x->next  = l;
				l = x;
			}
			else
			{
				break;
			}
		}
	}

	return l;
}

/* Regenerate NVRAM. */
int nvram_commit(nvram_handle_t *h)
{
	nvram_header_t *header = nvram_header(h);
	char *init, *config, *refresh, *ncdl;
	char *ptr, *end;
	int i;
	nvram_tuple_t *t;
	nvram_header_t tmp;
	uint8_t crc;

	/* Regenerate header */
	header->magic = NVRAM_MAGIC;
	header->crc_ver_init = (NVRAM_VERSION << 8);
	if (!(init = nvram_get(h, "sdram_init")) ||
		!(config = nvram_get(h, "sdram_config")) ||
		!(refresh = nvram_get(h, "sdram_refresh")) ||
		!(ncdl = nvram_get(h, "sdram_ncdl"))) {
		header->crc_ver_init |= SDRAM_INIT << 16;
		header->config_refresh = SDRAM_CONFIG;
		header->config_refresh |= SDRAM_REFRESH << 16;
		header->config_ncdl = 0;
	} else {
		header->crc_ver_init |= (strtoul(init, NULL, 0) & 0xffff) << 16;
		header->config_refresh = strtoul(config, NULL, 0) & 0xffff;
		header->config_refresh |= (strtoul(refresh, NULL, 0) & 0xffff) << 16;
		header->config_ncdl = strtoul(ncdl, NULL, 0);
	}

	/* Clear data area */
	ptr = (char *) header + sizeof(nvram_header_t);
	memset(ptr, 0xFF, NVRAM_SPACE - sizeof(nvram_header_t));
	memset(&tmp, 0, sizeof(nvram_header_t));

	/* Leave space for a double NUL at the end */
	end = (char *) header + NVRAM_SPACE - 2;

	/* Write out all tuples */
	for (i = 0; i < NVRAM_ARRAYSIZE(h->nvram_hash); i++) {
		for (t = h->nvram_hash[i]; t; t = t->next) {
			if ((ptr + strlen(t->name) + 1 + strlen(t->value) + 1) > end)
				break;
			ptr += sprintf(ptr, "%s=%s", t->name, t->value) + 1;
		}
	}

	/* End with a double NULL and pad to 4 bytes */
	*ptr = '\0';
	ptr++;

	if( (int)ptr % 4 )
		memset(ptr, 0, 4 - ((int)ptr % 4));

	ptr++;

	/* Set new length */
	header->len = NVRAM_ROUNDUP(ptr - (char *) header, 4);

	/* Little-endian CRC8 over the last 11 bytes of the header */
	tmp.crc_ver_init   = header->crc_ver_init;
	tmp.config_refresh = header->config_refresh;
	tmp.config_ncdl    = header->config_ncdl;
	crc = hndcrc8((unsigned char *) &tmp + NVRAM_CRC_START_POSITION,
		sizeof(nvram_header_t) - NVRAM_CRC_START_POSITION, 0xff);

	/* Continue CRC8 over data bytes */
	crc = hndcrc8((unsigned char *) &header[0] + sizeof(nvram_header_t),
		header->len - sizeof(nvram_header_t), crc);

	/* Set new CRC8 */
	header->crc_ver_init |= crc;

	/* Write out */
	msync(h->mmap, h->length, MS_SYNC);
	fsync(h->fd);

	/* Reinitialize hash table */
	return _nvram_rehash(h);
}

/* Open NVRAM and obtain a handle. */
nvram_handle_t * nvram_open(const char *file, int rdonly)
{
	int fd;
	char *mtd = NULL;
	nvram_handle_t *h;
	nvram_header_t *header;

	/* If erase size or file are undefined then try to define them */
	if( (nvram_erase_size == 0) || (file == NULL) )
	{
		/* Finding the mtd will set the appropriate erase size */
		if( (mtd = nvram_find_mtd()) == NULL || nvram_erase_size == 0 )
		{
			free(mtd);
			return NULL;
		}
	}

	if( (fd = open(file ? file : mtd, O_RDWR)) > -1 )
	{
		char *mmap_area = (char *) mmap(
			NULL, nvram_erase_size, PROT_READ | PROT_WRITE,
			(( rdonly == NVRAM_RO ) ? MAP_PRIVATE : MAP_SHARED) | MAP_LOCKED, fd, 0);

		if( mmap_area != MAP_FAILED )
		{
			memset(mmap_area, 0xFF, NVRAM_START(nvram_erase_size));

			if((h = (nvram_handle_t *) malloc(sizeof(nvram_handle_t))) != NULL)
			{
				memset(h, 0, sizeof(nvram_handle_t));

				h->fd     = fd;
				h->mmap   = mmap_area;
				h->length = nvram_erase_size;

				header = nvram_header(h);

				if( header->magic == NVRAM_MAGIC )
				{
					_nvram_rehash(h);
					free(mtd);
					return h;
				}
				else
				{
					munmap(h->mmap, h->length);
					free(h);
				}
			}
		}
	}

	free(mtd);
	return NULL;
}

/* Close NVRAM and free memory. */
int nvram_close(nvram_handle_t *h)
{
	_nvram_free(h);
	munmap(h->mmap, h->length);
	close(h->fd);
	free(h);

	return 0;
}

/* Determine NVRAM device node. */
char * nvram_find_mtd(void)
{
	FILE *fp;
	int i, esz;
	char dev[PATH_MAX];
	char *path = NULL;
	struct stat s;
	int supported = 1;

	/* Refuse any operation on the WGT634U */
	if( (fp = fopen("/proc/diag/model", "r")) )
	{
		if( fgets(dev, sizeof(dev), fp) && !strncmp(dev, "Netgear WGT634U", 15) )
			supported = 0;

		fclose(fp);
	}

	if( supported && (fp = fopen("/proc/mtd", "r")) )
	{
		while( fgets(dev, sizeof(dev), fp) )
		{
			if( strstr(dev, "nvram") && sscanf(dev, "mtd%d: %08x", &i, &esz) )
			{
				nvram_erase_size = esz;

				sprintf(dev, "/dev/mtdblock/%d", i);
				if( stat(dev, &s) > -1 && (s.st_mode & S_IFBLK) )
				{
					if( (path = (char *) malloc(strlen(dev)+1)) != NULL )
					{
						strncpy(path, dev, strlen(dev)+1);
						break;
					}
				}
				else
				{
					sprintf(dev, "/dev/mtdblock%d", i);
					if( stat(dev, &s) > -1 && (s.st_mode & S_IFBLK) )
					{
						if( (path = (char *) malloc(strlen(dev)+1)) != NULL )
						{
							strncpy(path, dev, strlen(dev)+1);
							break;
						}
					}
				}
			}
		}
		fclose(fp);
	}

	return path;
}

/* Check NVRAM staging file. */
char * nvram_find_staging(void)
{
	struct stat s;

	if( (stat(NVRAM_STAGING, &s) > -1) && (s.st_mode & S_IFREG) )
	{
		return NVRAM_STAGING;
	}

	return NULL;
}

/* Copy NVRAM contents to staging file. */
int nvram_to_staging(void)
{
	int fdmtd, fdstg, stat;
	char *mtd = nvram_find_mtd();
	char buf[nvram_erase_size];

	stat = -1;

	if( (mtd != NULL) && (nvram_erase_size > 0) )
	{
		if( (fdmtd = open(mtd, O_RDONLY)) > -1 )
		{
			if( read(fdmtd, buf, sizeof(buf)) == sizeof(buf) )
			{
				if((fdstg = open(NVRAM_STAGING, O_WRONLY | O_CREAT, 0600)) > -1)
				{
					write(fdstg, buf, sizeof(buf));
					fsync(fdstg);
					close(fdstg);

					stat = 0;
				}
			}

			close(fdmtd);
		}
	}

	free(mtd);
	return stat;
}

/* Copy staging file to NVRAM device. */
int staging_to_nvram(void)
{
	int fdmtd, fdstg, stat;
	char *mtd = nvram_find_mtd();
	char buf[nvram_erase_size];

	stat = -1;

	if( (mtd != NULL) && (nvram_erase_size > 0) )
	{
		if( (fdstg = open(NVRAM_STAGING, O_RDONLY)) > -1 )
		{
			if( read(fdstg, buf, sizeof(buf)) == sizeof(buf) )
			{
				if( (fdmtd = open(mtd, O_WRONLY | O_SYNC)) > -1 )
				{
					write(fdmtd, buf, sizeof(buf));
					fsync(fdmtd);
					close(fdmtd);
					stat = 0;
				}
			}

			close(fdstg);

			if( !stat )
				stat = unlink(NVRAM_STAGING) ? 1 : 0;
		}
	}

	free(mtd);
	return stat;
}
