/*
 * Copyright (C) 2007-2012, 2014 Apple Inc. All rights reserved.
 * Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
 *
 * This document is the property of Apple Computer, Inc.
 * It is considered confidential and proprietary.
 *
 * This document may not be reproduced or transmitted in any form,
 * in whole or in part, without the express written permission of
 * Apple Computer, Inc.
 */
/*
 * Support for detecting and reading the iPod-style SysCfg data
 */

#include <debug.h>
#include <stdlib.h>
#include <string.h>
#include <lib/syscfg.h>
#include <lib/blockdev.h>
#include <sys/menu.h>

static u_int32_t	syscfgKeyCount;
static uint8_t		*syscfgData;
static size_t		syscfgDataLength;

struct syscfgHeader {
	u_int32_t	shMagic;
#define kSysCfgHeaderMagic	'SCfg'
	u_int32_t	shSize;
	u_int32_t	shMaxSize;
	u_int32_t	shVersion;
	u_int32_t	shBigEndian;
	u_int32_t	shKeyCount;
};

void syscfg_reinit(void)
{
	free(syscfgData);
	syscfgDataLength = 0;
	syscfgData = NULL;
	syscfgKeyCount = 0;
}

int
do_syscfg(int argc, struct cmd_arg *args)
{
	struct syscfgHeader	hdr;
	struct syscfgMemEntry	entry;
	int			index, curArg;
	size_t			size, i, j;
	char			*sfx;
	bool			isExt;
	size_t			lowExt;
	size_t			highExt;
	size_t			extSize = 0;
	uint8_t			*buffer;
	bool			showExt = false;

	if (NULL == syscfgData) {
		printf("syscfg is not initialized!\n");
		return(0);
	}

	if (syscfgDataLength < sizeof(struct syscfgHeader))
		return (0);

	memcpy(&hdr, syscfgData, sizeof(struct syscfgHeader));
	printf("version 0x%08x with %d entries\n\n", hdr.shVersion, hdr.shKeyCount);

	lowExt = hdr.shMaxSize;
	highExt = hdr.shSize;

	for (index = 0; ; index++) {
		if (!syscfgFindByIndex(index, &entry))
			break;
			
		if (argc > 1) {
			
			/* When parameters are present, always show in extended mode */
			showExt = true;
			
			/* If the first parameter is ext, assume match in all cases by skipping the match block */
			if(strcmp("ext", args[1].str) != 0) { 
			
				/* Scan the list of arguments looking for a match */
				for (curArg = 1; curArg < argc; curArg ++) {
					if (((entry.seTag >> 24) & 0xFF) == args[curArg].str[0] &&
					    ((entry.seTag >> 16) & 0xFF) == args[curArg].str[1] &&
					    ((entry.seTag >> 8) & 0xFF)  == args[curArg].str[2] &&
					    ((entry.seTag >> 0) & 0xFF)  == args[curArg].str[3])
					break;
				}
				if (curArg == argc) continue;
			}
		}
			
		sfx = "";
		
		/* Is this a standard entry or an Offset entry? */
		if (entry.seDataOffset != 0) {
			size = entry.seDataSize;
			if (size > sizeof(entry.seData)) {
				
				/* The Size of the entry is too big for the structure
				 * Should it be truncated? */
				if (!showExt) {
					size = sizeof(entry.seData);
					sfx = "...";
				}
			}
			if (entry.seDataOffset + entry.seDataSize > hdr.shMaxSize) {
				printf("entry->seDataOffset not within syscfgData");
				return(0);
			}
			
			/* Allocate the requested space aligned on 16 bytes to zero
			 * pad the display. */
			buffer = calloc(1, (size + 15) & ~15);
			memcpy(buffer, syscfgData + entry.seDataOffset, size);

			// Calculate the extents of the offset section
			if (entry.seDataOffset < lowExt) {
				lowExt = entry.seDataOffset;
			}
			if ((entry.seDataOffset + entry.seDataSize) > highExt) {
				highExt = entry.seDataOffset + entry.seDataSize;
			}
			isExt = true;
		} else {
			/* allocate enough space to mirror the entrys buffer */
			size = sizeof(entry.seData);
			
			buffer = malloc(sizeof(entry.seData));
			memcpy(buffer, entry.seData, size);
			
			isExt = false;
		}
		printf("0x%08x '%c%c%c%c'  %4dB %c  ",
		    entry.seTag,
		    (entry.seTag >> 24) & 0xff,
		    (entry.seTag >> 16) & 0xff,
		    (entry.seTag >> 8) & 0xff,
		    entry.seTag & 0xff,
		    entry.seDataSize,
		    isExt ? '*' : ' ');
		
		/* Format the data in nice columns */
		for (i = 0; i < size; i += 16 ) {
			printf("%*s", (i >= 16) ? 28 : 0, "");
			
			/* Starting at the offset, display the data values up to 4 per line. */
			for (j = i; j < (i + 16); j += 4) {
				printf("0x%08x ", *((u_int32_t *)&buffer[j]));
			}
				
			printf("%s\n", sfx);
		}
		
		free(buffer);
	}

	printf("\n");
	printf("header @ 0-%zu", sizeof(struct syscfgHeader) - 1);
	printf("; entries @ %zu-%d", sizeof(struct syscfgHeader), hdr.shSize - 1);
	if (lowExt < highExt) {
		extSize = highExt - lowExt;
		printf("; extra data @ %zu-%zu", lowExt, highExt - 1);
	}
	printf("; using %zu of %d bytes\n", hdr.shSize + extSize, hdr.shMaxSize);

	return(0);
}

bool
syscfgInitWithBdev(const char *bdevName)
{
	int result;
	struct blockdev	*candidate;
	struct syscfgHeader hdr;

	if (syscfgData != NULL)
		return(false);
	
	/* look for the suggested bdev */
	if ((candidate = lookup_blockdev(bdevName)) == NULL) {
		dprintf(DEBUG_INFO, "syscfg: can't find bdev \"%s\"\n", bdevName);
		return(false);
	}

	/* Make sure it's big enough */
	if (candidate->total_len <= kSysCfgBdevOffset + sizeof(hdr)) {
		dprintf(DEBUG_INFO, "syscfg: bdev size 0x%llx too small\n", candidate->total_len);
		return (false);
	}

	/* look for the header */
	if (blockdev_read(candidate, &hdr, kSysCfgBdevOffset, sizeof(hdr)) < (int)sizeof(hdr)) {
		dprintf(DEBUG_INFO, "syscfg: bdev read fail\n");
		return(false);
	}

	if (hdr.shMagic != kSysCfgHeaderMagic) {
		dprintf(DEBUG_INFO, "syscfg: bad magic\n");
		return(false);
	}

	dprintf(DEBUG_CRITICAL, "syscfg: version 0x%08x with %d entries using %d of %d bytes\n",
	    hdr.shVersion, hdr.shKeyCount, hdr.shSize, hdr.shMaxSize);

	/* If there is a syscfg there will also be diag info... protect them. */
	blockdev_set_protection(candidate, 0x2000, 0x6000);

	syscfgDataLength = candidate->total_len - kSysCfgBdevOffset;
	if (hdr.shSize < syscfgDataLength)
		syscfgDataLength = hdr.shMaxSize;

	syscfgData = malloc(syscfgDataLength);

	result = blockdev_read(candidate, syscfgData, kSysCfgBdevOffset, syscfgDataLength);

	if (result < 0 || (size_t)result < syscfgDataLength) {
		dprintf(DEBUG_INFO, "syscfg: bdev read fail (%d)\n", result);
		syscfg_reinit();
		return false;
	}

	syscfgKeyCount = hdr.shKeyCount;

	return(true);
}

void *
syscfgGetData(struct syscfgMemEntry *entry)
{
	/* Handle data stored externally from the entry */
	if (entry->seDataOffset != 0) {
		if (entry->seDataOffset > syscfgDataLength)
			return NULL;
		if (entry->seDataOffset + entry->seDataSize > syscfgDataLength)
			return NULL;

		return syscfgData + entry->seDataOffset;
	} else {
		return entry->seData;
	}
}

uint32_t
syscfgGetSize(struct syscfgMemEntry *entry)
{
	return entry->seDataSize;
}

int
syscfgCopyDataForTag(u_int32_t tag, u_int8_t *buffer, size_t size)
{
	bool			result;
	struct syscfgMemEntry	entry;
	void			*data;

	result = syscfgFindByTag(tag, &entry);
	if (!result)
		return(-1);
	if (size > entry.seDataSize)
		size = entry.seDataSize;

	data = syscfgGetData(&entry);
	if (data == NULL)
		return(-1);

	memcpy(buffer, data, size);

	// XXX check what to do with the assert that was removed from here
	return size;
}

bool
syscfgFindByTag(u_int32_t tag, struct syscfgMemEntry *entry)
{
	static struct syscfgMemEntry	temp;
	bool				result;
	u_int32_t			index;

	for (index = 0; index < syscfgKeyCount; index++) {
		result = syscfgFindByIndex(index, &temp);
		if (result && (temp.seTag == tag)) {
			memcpy(entry, &temp, sizeof(*entry));
			return true;
		}
	}
	return false;
}

bool
syscfgFindByIndex(u_int32_t index, struct syscfgMemEntry *result)
{
	struct syscfgEntry	entry;
	struct syscfgEntryCNTB	*entryCNTB;

	if (index >= syscfgKeyCount)
		return false;

	size_t offset = sizeof(struct syscfgHeader) + index * sizeof(struct syscfgEntry);
	if (offset >= syscfgDataLength || offset + sizeof(struct syscfgEntry) >= syscfgDataLength)
		return false;

	memcpy(&entry, syscfgData + offset, sizeof(entry));

	if (entry.seTag == 'CNTB') {
		entryCNTB = (struct syscfgEntryCNTB *)&entry;
		result->seTag = entryCNTB->seRealTag;
		memset(result->seData, 0, sizeof(result->seData));
		result->seDataSize = entryCNTB->seDataSize;
		result->seDataOffset = entryCNTB->seDataOffset;
	} else {
		result->seTag = entry.seTag;
		memcpy(result->seData, entry.seData, sizeof(result->seData));
		result->seDataSize = sizeof(result->seData);
		result->seDataOffset = 0;
	}
	return true;
}

bool
syscfg_find_tag(uint32_t tag, void **data_out, uint32_t *size_out)
{
	static struct syscfgMemEntry result;

	if (syscfgFindByTag(tag, &result)) {
		if (data_out) {
			*data_out = syscfgGetData(&result);
		}
		if (size_out) {
			*size_out = syscfgGetSize(&result);
		}

		return true;
	} else {
		return false;
	}
}
