/*-
 * Copyright (c) 2016 Landon Fuller <landonf@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer,
 *    without modification.
 * 2. Redistributions in binary form must reproduce at minimum a disclaimer
 *    similar to the "NO WARRANTY" disclaimer below ("Disclaimer") and any
 *    redistribution must be conditioned upon including a substantially
 *    similar Disclaimer requirement for further binary redistribution.
 *
 * NO WARRANTY
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF NONINFRINGEMENT, MERCHANTIBILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGES.
 */

#include <sys/cdefs.h>
#ifdef _KERNEL

#include <sys/param.h>
#include <sys/ctype.h>
#include <sys/malloc.h>
#include <sys/systm.h>

#else /* !_KERNEL */

#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#endif /* _KERNEL */

#include "bhnd_nvram_private.h"

#include "bhnd_nvram_datavar.h"
#include "bhnd_nvram_data_bcmvar.h"

/*
 * Broadcom-RAW NVRAM data class.
 * 
 * The Broadcom NVRAM NUL-delimited ASCII format is used by most
 * Broadcom SoCs.
 * 
 * The NVRAM data is encoded as a stream of NUL-terminated 'key=value'
 * strings; the end of the stream is denoted by a single extra NUL character.
 */

struct bhnd_nvram_bcmraw;

/** BCM-RAW NVRAM data class instance */
struct bhnd_nvram_bcmraw {
	struct bhnd_nvram_data		 nv;	/**< common instance state */
	char				*data;	/**< backing buffer */
	size_t				 size;	/**< buffer size */
	size_t				 count;	/**< variable count */
};

BHND_NVRAM_DATA_CLASS_DEFN(bcmraw, "Broadcom (RAW)",
    BHND_NVRAM_DATA_CAP_DEVPATHS, sizeof(struct bhnd_nvram_bcmraw))

static int
bhnd_nvram_bcmraw_probe(struct bhnd_nvram_io *io)
{
	char	 envp[16];
	size_t	 envp_len;
	size_t	 io_size;
	int	 error;

	io_size = bhnd_nvram_io_getsize(io);

	/*
	 * Fetch initial bytes
	 */
	envp_len = bhnd_nv_ummin(sizeof(envp), io_size);
	if ((error = bhnd_nvram_io_read(io, 0x0, envp, envp_len)))
		return (error);

	/* An empty BCM-RAW buffer should still contain a single terminating
	 * NUL */
	if (envp_len == 0)
		return (ENXIO);

	if (envp_len == 1) {
		if (envp[0] != '\0')
			return (ENXIO);

		return (BHND_NVRAM_DATA_PROBE_MAYBE);
	}

	/* Must contain only printable ASCII characters delimited
	 * by NUL record delimiters */
	for (size_t i = 0; i < envp_len; i++) {
		char c = envp[i];

		/* If we hit a newline, this is probably BCM-TXT */
		if (c == '\n')
			return (ENXIO);

		if (c == '\0' && !bhnd_nv_isprint(c))
			continue;
	}

	/* A valid BCM-RAW buffer should contain a terminating NUL for
	 * the last record, followed by a final empty record terminated by
	 * NUL */
	envp_len = 2;
	if (io_size < envp_len)
		return (ENXIO);

	if ((error = bhnd_nvram_io_read(io, io_size-envp_len, envp, envp_len)))
		return (error);

	if (envp[0] != '\0' || envp[1] != '\0')
		return (ENXIO);

	return (BHND_NVRAM_DATA_PROBE_MAYBE + 1);
}

static int
bhnd_nvram_bcmraw_getvar_direct(struct bhnd_nvram_io *io, const char *name,
    void *buf, size_t *len, bhnd_nvram_type type)
{
	return (bhnd_nvram_bcm_getvar_direct_common(io, name, buf, len, type,
	    false));
}

static int
bhnd_nvram_bcmraw_serialize(bhnd_nvram_data_class *cls, bhnd_nvram_plist *props,
    bhnd_nvram_plist *options, void *outp, size_t *olen)
{
	bhnd_nvram_prop	*prop;
	size_t		 limit, nbytes;
	int		 error;

	/* Determine output byte limit */
	if (outp != NULL)
		limit = *olen;
	else
		limit = 0;

	nbytes = 0;

	/* Write all properties */
	prop = NULL;
	while ((prop = bhnd_nvram_plist_next(props, prop)) != NULL) {
		const char	*name;
		char		*p;
		size_t		 prop_limit;
		size_t		 name_len, value_len;

		if (outp == NULL || limit < nbytes) {
			p = NULL;
			prop_limit = 0;
		} else {
			p = ((char *)outp) + nbytes;
			prop_limit = limit - nbytes;
		}

		/* Fetch and write name + '=' to output */
		name = bhnd_nvram_prop_name(prop);
		name_len = strlen(name) + 1;

		if (prop_limit > name_len) {
			memcpy(p, name, name_len - 1);
			p[name_len - 1] = '=';

			prop_limit -= name_len;
			p += name_len;
		} else {
			prop_limit = 0;
			p = NULL;
		}

		/* Advance byte count */
		if (SIZE_MAX - nbytes < name_len)
			return (EFTYPE); /* would overflow size_t */

		nbytes += name_len;

		/* Attempt to write NUL-terminated value to output */
		value_len = prop_limit;
		error = bhnd_nvram_prop_encode(prop, p, &value_len,
		    BHND_NVRAM_TYPE_STRING);

		/* If encoding failed for any reason other than ENOMEM (which
		 * we'll detect and report after encoding all properties),
		 * return immediately */
		if (error && error != ENOMEM) {
			BHND_NV_LOG("error serializing %s to required type "
			    "%s: %d\n", name,
			    bhnd_nvram_type_name(BHND_NVRAM_TYPE_STRING),
			    error);
			return (error);
		}

		/* Advance byte count */
		if (SIZE_MAX - nbytes < value_len)
			return (EFTYPE); /* would overflow size_t */

		nbytes += value_len;
	}

	/* Write terminating '\0' */
	if (limit > nbytes)
		*((char *)outp + nbytes) = '\0';

	if (nbytes == SIZE_MAX)
		return (EFTYPE); /* would overflow size_t */
	else
		nbytes++;

	/* Provide required length */
	*olen = nbytes;
	if (limit < *olen) {
		if (outp == NULL)
			return (0);

		return (ENOMEM);
	}

	return (0);
}

/**
 * Initialize @p bcm with the provided NVRAM data mapped by @p src.
 * 
 * @param bcm A newly allocated data instance.
 */
static int
bhnd_nvram_bcmraw_init(struct bhnd_nvram_bcmraw *bcm, struct bhnd_nvram_io *src)
{
	size_t	 io_size;
	size_t	 capacity, offset;
	int	 error;

	/* Fetch the input image size */
	io_size = bhnd_nvram_io_getsize(src);

	/* Allocate a buffer large enough to hold the NVRAM image, and
	 * an extra EOF-signaling NUL (on the chance it's missing from the
	 * source data) */
	if (io_size == SIZE_MAX)
		return (ENOMEM);

	capacity = io_size + 1 /* room for extra NUL */;
	bcm->size = io_size;
	if ((bcm->data = bhnd_nv_malloc(capacity)) == NULL)
		return (ENOMEM);

	/* Copy in the NVRAM image */
	if ((error = bhnd_nvram_io_read(src, 0x0, bcm->data, io_size)))
		return (error);

	/* Process the buffer */
	bcm->count = 0;
	for (offset = 0; offset < bcm->size; offset++) {
		char		*envp;
		const char	*name, *value;
		size_t		 envp_len;
		size_t		 name_len, value_len;

		/* Parse the key=value string */
		envp = (char *) (bcm->data + offset);
		envp_len = strnlen(envp, bcm->size - offset);
		error = bhnd_nvram_parse_env(envp, envp_len, '=', &name,
					     &name_len, &value, &value_len);
		if (error) {
			BHND_NV_LOG("error parsing envp at offset %#zx: %d\n",
			    offset, error);
			return (error);
		}

		/* Insert a '\0' character, replacing the '=' delimiter and
		 * allowing us to vend references directly to the variable
		 * name */
		*(envp + name_len) = '\0';

		/* Add to variable count */
		bcm->count++;

		/* Seek past the value's terminating '\0' */
		offset += envp_len;
		if (offset == io_size) {
			BHND_NV_LOG("missing terminating NUL at offset %#zx\n",
			    offset);
			return (EINVAL);
		}

		/* If we hit EOF without finding a terminating NUL
		 * byte, we need to append it */
		if (++offset == bcm->size) {
			BHND_NV_ASSERT(offset < capacity,
			    ("appending past end of buffer"));
			bcm->size++;
			*(bcm->data + offset) = '\0';
		}

		/* Check for explicit EOF (encoded as a single empty NUL
		 * terminated string) */
		if (*(bcm->data + offset) == '\0')
			break;
	}

	/* Reclaim any unused space in the backing buffer */
	if (offset < bcm->size) {
		bcm->data = bhnd_nv_reallocf(bcm->data, bcm->size);
		if (bcm->data == NULL)
			return (ENOMEM);
	}

	return (0);
}

static int
bhnd_nvram_bcmraw_new(struct bhnd_nvram_data *nv, struct bhnd_nvram_io *io)
{
	struct bhnd_nvram_bcmraw	*bcm;
	int				 error;

	bcm = (struct bhnd_nvram_bcmraw *)nv;

	/* Parse the BCM input data and initialize our backing
	 * data representation */
	if ((error = bhnd_nvram_bcmraw_init(bcm, io))) {
		bhnd_nvram_bcmraw_free(nv);
		return (error);
	}

	return (0);
}

static void
bhnd_nvram_bcmraw_free(struct bhnd_nvram_data *nv)
{
	struct bhnd_nvram_bcmraw *bcm = (struct bhnd_nvram_bcmraw *)nv;

	if (bcm->data != NULL)
		bhnd_nv_free(bcm->data);
}

static bhnd_nvram_plist *
bhnd_nvram_bcmraw_options(struct bhnd_nvram_data *nv)
{
	return (NULL);
}

static size_t
bhnd_nvram_bcmraw_count(struct bhnd_nvram_data *nv)
{
	struct bhnd_nvram_bcmraw *bcm = (struct bhnd_nvram_bcmraw *)nv;

	return (bcm->count);
}

static uint32_t
bhnd_nvram_bcmraw_caps(struct bhnd_nvram_data *nv)
{
	return (BHND_NVRAM_DATA_CAP_READ_PTR|BHND_NVRAM_DATA_CAP_DEVPATHS);
}

static const char *
bhnd_nvram_bcmraw_next(struct bhnd_nvram_data *nv, void **cookiep)
{
	struct bhnd_nvram_bcmraw	*bcm;
	const char			*envp;

	bcm = (struct bhnd_nvram_bcmraw *)nv;

	if (*cookiep == NULL) {
		/* Start at the first NVRAM data record */
		envp = bcm->data;
	} else {
		/* Seek to next record */
		envp = *cookiep;
		envp += strlen(envp) + 1;	/* key + '\0' */
		envp += strlen(envp) + 1;	/* value + '\0' */
	}

	/* EOF? */
	if (*envp == '\0')
		return (NULL);

	*cookiep = (void *)(uintptr_t)envp;
	return (envp);
}

static void *
bhnd_nvram_bcmraw_find(struct bhnd_nvram_data *nv, const char *name)
{
	return (bhnd_nvram_data_generic_find(nv, name));
}

static int
bhnd_nvram_bcmraw_getvar_order(struct bhnd_nvram_data *nv, void *cookiep1,
    void *cookiep2)
{
	if (cookiep1 < cookiep2)
		return (-1);

	if (cookiep1 > cookiep2)
		return (1);

	return (0);
}

static int
bhnd_nvram_bcmraw_getvar(struct bhnd_nvram_data *nv, void *cookiep, void *buf,
    size_t *len, bhnd_nvram_type type)
{
	return (bhnd_nvram_data_generic_rp_getvar(nv, cookiep, buf, len, type));
}

static int
bhnd_nvram_bcmraw_copy_val(struct bhnd_nvram_data *nv, void *cookiep,
    bhnd_nvram_val **value)
{
	return (bhnd_nvram_data_generic_rp_copy_val(nv, cookiep, value));
}

static const void *
bhnd_nvram_bcmraw_getvar_ptr(struct bhnd_nvram_data *nv, void *cookiep,
    size_t *len, bhnd_nvram_type *type)
{
	const char *envp;

	/* Cookie points to key\0value\0 -- get the value address */
	envp = cookiep;
	envp += strlen(envp) + 1;	/* key + '\0' */
	*len = strlen(envp) + 1;	/* value + '\0' */
	*type = BHND_NVRAM_TYPE_STRING;

	return (envp);
}

static const char *
bhnd_nvram_bcmraw_getvar_name(struct bhnd_nvram_data *nv, void *cookiep)
{
	/* Cookie points to key\0value\0 */
	return (cookiep);
}

static int
bhnd_nvram_bcmraw_filter_setvar(struct bhnd_nvram_data *nv, const char *name,
    bhnd_nvram_val *value, bhnd_nvram_val **result)
{
	bhnd_nvram_val	*str;
	int		 error;

	/* Name (trimmed of any path prefix) must be valid */
	if (!bhnd_nvram_validate_name(bhnd_nvram_trim_path_name(name)))
		return (EINVAL);

	/* Value must be bcm-formatted string */
	error = bhnd_nvram_val_convert_new(&str, &bhnd_nvram_val_bcm_string_fmt,
	    value, BHND_NVRAM_VAL_DYNAMIC);
	if (error)
		return (error);

	/* Success. Transfer result ownership to the caller. */
	*result = str;
	return (0);
}

static int
bhnd_nvram_bcmraw_filter_unsetvar(struct bhnd_nvram_data *nv, const char *name)
{
	/* We permit deletion of any variable */
	return (0);
}