/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <sys/promif.h>
#include <sys/promimpl.h>

#ifdef	DPRINTF
#define	dprintf	prom_printf
#endif

/*
 * Check if the prom is 64-bit ready.
 */

/*
 * Table listing the minimum prom versions supported by this kernel.
 * The model value is expected to match the model in the flashprom node.
 */
static struct obp_rev_table {
	char *model;
	char *version;
} obp_min_revs[] = {
	{"SUNW,525-1414", "OBP 3.11.2 1997/12/05 10:25"},  /* pulsar */
	{"SUNW,525-1672", "OBP 3.7.107 1998/02/19 17:54"},  /* tazmo */
	{"SUNW,525-1431", "OBP 3.2.16 1998/06/08 16:58"},   /* sunfire */
	{ NULL, NULL}
};

#define	NMINS	60
#define	NHOURS	24
#define	NDAYS	31
#define	NMONTHS	12

#define	YEAR(y)	 ((y-1) * (NMONTHS * NDAYS * NHOURS * NMINS))
#define	MONTH(m) ((m-1) * (NDAYS * NHOURS * NMINS))
#define	DAY(d)   ((d-1) * (NHOURS * NMINS))
#define	HOUR(h)  ((h)   * (NMINS))
#define	MINUTE(m) (m)

static int
strtoi(char *str, char **pos)
{
	int c;
	int val = 0;

	for (c = *str++; c >= '0' && c <= '9'; c = *str++) {
		val *= 10;
		val += c - '0';
	}
	if (pos)
		*pos = str;
	return (val);
}

/*
 * obp_timestamp: based on the OBP flashprom version string of the
 * format "OBP x.y.z YYYY/MM/DD HH:MM" calculate a timestamp based
 * on the year, month, day, hour and minute by turning that into
 * a number of minutes.
 */
static int
obp_timestamp(char *v)
{
	char *c;
	int maj, year, month, day, hour, min;

	if (v[0] != 'O' || v[1] != 'B' || v[2] != 'P')
		return (-1);

	c = v + 3;

	/* Find first non-space character after OBP */
	while (*c != '\0' && (*c == ' ' || *c == '\t'))
		c++;
	if (prom_strlen(c) < 5)		/* need at least "x.y.z" */
		return (-1);

	maj = strtoi(c, &c);
	if (maj < 3)
		return (-1);

#if 0 /* XXX - not used */
	dot = dotdot = 0;
	if (*c == '.') {
		dot = strtoi(c + 1, &c);

		/* optional? dot-dot release */
		if (*c == '.')
			dotdot = strtoi(c + 1, &c);
	}
#endif

	/* Find space at the end of version number */
	while (*c != '\0' && *c != ' ')
		c++;
	if (prom_strlen(c) < 11)	/* need at least " xxxx/xx/xx" */
		return (-1);

	/* Point to first character of date */
	c++;

	/* Validate date format */
	if (c[4] != '/' || c[7] != '/')
		return (-1);

	year = strtoi(c, NULL);
	month = strtoi(c + 5, NULL);
	day = strtoi(c + 8, NULL);

	if (year < 1995 || month == 0 || day == 0)
		return (-1);

	/*
	 * Find space at the end of date number
	 */
	c += 10;
	while (*c != '\0' && *c != ' ')
		c++;
	if (prom_strlen(c) < 6)		/* need at least " xx:xx" */
		return (-1);

	/* Point to first character of time */
	c++;

	if (c[2] != ':')
		return (-1);

	hour = strtoi(c, NULL);
	min = strtoi(c + 3, NULL);

	return (YEAR(year) + MONTH(month) +
	    DAY(day) + HOUR(hour) + MINUTE(min));
}

/*
 * Check the prom against the obp_min_revs table and complain if
 * the system has an older prom installed.  The actual major/minor/
 * dotdot numbers are not checked, only the date/time stamp.
 */

static struct obp_rev_table *flashprom_ortp;
static pnode_t flashprom_node;
static int flashprom_checked;
static int flashprom_return_code;

int
check_timestamp(char *model, int tstamp)
{
	int min_tstamp;
	struct obp_rev_table *ortp;

	for (ortp = obp_min_revs; ortp->model != NULL; ortp++) {
		if (prom_strcmp(model, ortp->model) == 0) {
			min_tstamp = obp_timestamp(ortp->version);
			if (min_tstamp == -1) {
#ifdef	DEBUG
				prom_printf("prom_version_check: "
				    "invalid OBP version string in table "
				    " (entry %d)", (int)(ortp - obp_min_revs));
#endif
				continue;
			}
			if (tstamp < min_tstamp) {
#ifdef	DPRINTF
				dprintf("prom_version_check: "
				    "Down-rev OBP detected.  "
				    "Please update to at least:\n\t%s\n\n",
				    ortp->version);
#endif
				flashprom_ortp = ortp;
				return (1);
			}
		}
	} /* for each obp_rev_table entry */

	return (0);
}

static pnode_t
visit(pnode_t node)
{
	int tstamp, plen, i;
	char vers[512], model[64];
	static pnode_t openprom_node;
	static char version[] = "version";
	static char model_name[] = "model";
	static char flashprom[] = "flashprom";

	/*
	 * if name isn't 'flashprom', continue.
	 */
	if (prom_getproplen(node, OBP_NAME) != sizeof (flashprom))
		return ((pnode_t)0);
	(void) prom_getprop(node, OBP_NAME, model);
	if (prom_strncmp(model, flashprom, sizeof (flashprom)) != 0)
		return ((pnode_t)0);

	plen = prom_getproplen(node, version);
	if (plen <= 0 || plen > sizeof (vers))
		return ((pnode_t)0);
	(void) prom_getprop(node, version, vers);
	vers[plen] = '\0';

	/* Make sure it's an OBP flashprom */
	if (vers[0] != 'O' && vers[1] != 'B' && vers[2] != 'P')
		return ((pnode_t)0);

	plen = prom_getproplen(node, model_name);
	if (plen <= 0 || plen > sizeof (model))
		return ((pnode_t)0);
	(void) prom_getprop(node, model_name, model);
	model[plen] = '\0';

	tstamp = obp_timestamp(vers);
	if (tstamp == -1) {
		prom_printf("prom_version_check: node contains "
		    "improperly formatted version property,\n"
		    "\tnot checking prom version");
		return ((pnode_t)0);
	}

	i = check_timestamp(model, tstamp);

	if (i == 0)
		return ((pnode_t)0);

	/*
	 * We know that "node"'s flashprom image contains downrev firmware,
	 * however, a multi-board server might be running correct firmware.
	 * Check for that case by looking at the "/openprom" node,
	 * which always contains the running version. (We needed the
	 * "model" value to be able to do this, so we can use it as
	 * an index value into the table.)
	 *
	 * If it turns out we're running 'current' firmware,
	 * but detect down-rev firmware, use a different return code.
	 */

	flashprom_return_code = PROM_VER64_UPGRADE;

	openprom_node = prom_finddevice("/openprom");
	if (openprom_node == OBP_BADNODE)
		return (node);

	plen = prom_getproplen(node, version);
	if (plen <= 0 || plen > sizeof (vers))
		return (node);
	(void) prom_getprop(node, version, vers);
	vers[plen] = '\0';

	if (vers[0] != 'O' && vers[1] != 'B' && vers[2] != 'P') {
		prom_printf("prom_version_check: "
		    "unknown <version> string in </openprom>\n");
		return (node);
	}

	tstamp = obp_timestamp(vers);
	if (tstamp == -1) {
		prom_printf("prom_version_check: "
		    "</openprom> node <version> property: bad tstamp\n");
		return (node);
	}

	i = check_timestamp(model, tstamp);
	/*
	 * If that returned zero, then the running version is
	 * adequate ... so we can 'suggest' instead of 'require'.
	 */
	if (i == 0)
		flashprom_return_code = PROM_VER64_SUGGEST;

	return (node);
}

/*
 * visit each node in the device tree, until we get a non-null answer
 */
static pnode_t
walk(pnode_t node)
{
	pnode_t id;

	if (visit(node))
		return (node);

	for (node = prom_childnode(node); node; node = prom_nextnode(node))
		if ((id = walk(node)) != (pnode_t)0)
			return (id);

	return ((pnode_t)0);
}

/*
 * Check if the prom is 64-bit ready.
 *
 * If it's ready (or the test doesn't apply), return PROM_VER64_OK.
 * If downrev firmware is running, return PROM_VER64_UPGRADE.
 * If downrev firmware is detected (but not running), return PROM_VER64_SUGGEST.
 *
 * For PROM_VER64_UPGRADE and PROM_VER64_SUGGEST return code values:
 * Return the nodeid of the flashprom node in *nodeid.
 * and a printable message in *buf, buflen.
 */
int
prom_version_check(char *buf, size_t buflen, pnode_t *nodeid)
{
	char *p;
	pnode_t node = flashprom_node;
	size_t i;

	/*
	 * If we already checked, we already know the answer.
	 */
	if (flashprom_checked == 0) {
		flashprom_node = node = walk(prom_rootnode());
		flashprom_checked = 1;
	}

	if (nodeid)
		*nodeid = node;

	if (node == (pnode_t)0) {
		if (buf && buflen)
			*buf = '\0';
		return (PROM_VER64_OK);
	}

	/* bzero the callers buffer */
	for (i = buflen, p = buf; i != 0; --i, ++p)
		*p = '\0';

	/*
	 * Do a bounded copy of the output string into the callers buffer
	 */
	if (buflen <= 1)
		return (flashprom_return_code);

	(void) prom_strncpy(buf, flashprom_ortp->version, buflen - 1);
	return (flashprom_return_code);
}