/*
 * 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 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


/*
 * Descriptor parsing functions
 */
#define	USBA_FRAMEWORK
#include <sys/usb/usba/usba_impl.h>
#include <sys/strsun.h>

#define	INCREMENT_BUF(buf) \
		if ((buf)[0] == 0) { \
			break; \
		} else { \
			(buf) += (buf)[0]; \
		}
#define	isdigit(ch) ((ch >= '0') && (ch <= '9'))

extern usba_cfg_pwr_descr_t default_cfg_power;
extern usba_if_pwr_descr_t default_if_power;

size_t
usb_parse_data(char	*format,
	uchar_t 	*data,
	size_t		datalen,
	void		*structure,
	size_t		structlen)
{
	int	fmt;
	int	counter = 1;
	int	multiplier = 0;
	uchar_t	*dataend = data + datalen;
	char	*structstart = (char *)structure;
	void	*structend = (void *)((intptr_t)structstart + structlen);

	if ((format == NULL) || (data == NULL) || (structure == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while ((fmt = *format) != '\0') {

		/*
		 * Could some one pass a "format" that is greater than
		 * the structlen? Conversely, one could pass a ret_buf_len
		 * that is less than the "format" length.
		 * If so, we need to protect against writing over memory.
		 */
		if (counter++ > structlen) {
			break;
		}

		if (fmt == 'c') {
			uint8_t	*cp = (uint8_t *)structure;

			cp = (uint8_t *)(((uintptr_t)cp + _CHAR_ALIGNMENT - 1) &
			    ~(_CHAR_ALIGNMENT - 1));
			if (((data + 1) > dataend) ||
			    ((cp + 1) > (uint8_t *)structend))
				break;

			*cp++ = *data++;
			structure = (void *)cp;
			if (multiplier) {
				multiplier--;
			}
			if (multiplier == 0) {
				format++;
			}
		} else if (fmt == 's') {
			uint16_t	*sp = (uint16_t *)structure;

			sp = (uint16_t *)
			    (((uintptr_t)sp + _SHORT_ALIGNMENT - 1) &
			    ~(_SHORT_ALIGNMENT - 1));
			if (((data + 2) > dataend) ||
			    ((sp + 1) > (uint16_t *)structend))
				break;

			*sp++ = (data[1] << 8) + data[0];
			data += 2;
			structure = (void *)sp;
			if (multiplier) {
				multiplier--;
			}
			if (multiplier == 0) {
				format++;
			}
		} else if (fmt == 'l') {
			uint32_t	*lp = (uint32_t *)structure;

			lp = (uint32_t *)
			    (((uintptr_t)lp + _INT_ALIGNMENT - 1) &
			    ~(_INT_ALIGNMENT - 1));
			if (((data + 4) > dataend) ||
			    ((lp + 1) > (uint32_t *)structend))
				break;

			*lp++ = (((((
			    (uint32_t)data[3] << 8) | data[2]) << 8) |
			    data[1]) << 8) | data[0];
			data += 4;
			structure = (void *)lp;
			if (multiplier) {
				multiplier--;
			}
			if (multiplier == 0) {
				format++;
			}
		} else if (fmt == 'L') {
			uint64_t	*llp = (uint64_t *)structure;

			llp = (uint64_t *)
			    (((uintptr_t)llp + _LONG_LONG_ALIGNMENT - 1) &
			    ~(_LONG_LONG_ALIGNMENT - 1));
			if (((data + 8) > dataend) ||
			    ((llp + 1) >= (uint64_t *)structend))
				break;

			*llp++ = (((((((((((((data[7] << 8) |
			    data[6]) << 8) | data[5]) << 8) |
			    data[4]) << 8) | data[3]) << 8) |
			    data[2]) << 8) | data[1]) << 8) |
			    data[0];
			data += 8;
			structure = (void *)llp;
			if (multiplier) {
				multiplier--;
			}
			if (multiplier == 0) {
				format++;
			}
		} else if (isdigit(fmt)) {
			multiplier = (multiplier * 10) + (fmt - '0');
			format++;
			counter--;
		} else {
			multiplier = 0;
			break;
		}
	}

	return ((intptr_t)structure - (intptr_t)structstart);
}


size_t
usb_parse_CV_descr(char *format,
	uchar_t *data,
	size_t	datalen,
	void	*structure,
	size_t	structlen)
{
	return (usb_parse_data(format, data, datalen, structure,
	    structlen));
}


/*
 *	Helper function: returns pointer to n-th descriptor of
 *	type descr_type, unless the end of the buffer or a descriptor
 *	of type	stop_descr_type1 or stop_descr_type2 is encountered first.
 */
static uchar_t *
usb_nth_descr(uchar_t	*buf,
	size_t		buflen,
	int		descr_type,
	uint_t		n,
	int		stop_descr_type1,
	int		stop_descr_type2)
{
	uchar_t	*bufstart = buf;
	uchar_t *bufend = buf + buflen;

	if (buf == NULL) {

		return (NULL);
	}

	while (buf + 2 <= bufend) {
		if ((buf != bufstart) && ((buf[1] == stop_descr_type1) ||
		    (buf[1] == stop_descr_type2))) {

			return (NULL);
		}

		if ((descr_type == USB_DESCR_TYPE_ANY) ||
		    (buf[1] == descr_type)) {
			if (n-- == 0) {

				return (buf);
			}
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (NULL);
}


size_t
usb_parse_dev_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(DEVICE) */
	size_t			buflen,
	usb_dev_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	if ((buf == NULL) || (ret_descr == NULL) ||
	    (buflen < 2) || (buf[1] != USB_DESCR_TYPE_DEV)) {

		return (USB_PARSE_ERROR);
	}

	return (usb_parse_data("ccsccccssscccc",
	    buf, buflen, ret_descr, ret_buf_len));
}


size_t
usb_parse_cfg_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	usb_cfg_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	if ((buf == NULL) || (ret_descr == NULL) ||
	    (buflen < 2) || (buf[1] != USB_DESCR_TYPE_CFG)) {

		return (USB_PARSE_ERROR);
	}

	return (usb_parse_data("ccsccccc",
	    buf, buflen, ret_descr, ret_buf_len));
}


size_t
usba_parse_cfg_pwr_descr(
	uchar_t			*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	usba_cfg_pwr_descr_t	*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}
	while (buf + 2 <= bufend) {

		if (buf[1] == USBA_DESCR_TYPE_CFG_PWR_1_1) {
			return (usb_parse_data("ccsccccccccsss",
			    buf, buflen, ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	/* return the default configuration power descriptor */
	bcopy(&default_cfg_power, ret_descr, USBA_CFG_PWR_DESCR_SIZE);

	return (ret_descr->bLength);

}


size_t
usb_parse_ia_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	size_t			first_if,
	usb_ia_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + USB_IA_DESCR_SIZE <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_IA) &&
		    (buf[2] == first_if)) {

			return (usb_parse_data("cccccccc",
			    buf, _PTRDIFF(bufend, buf),
			    ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}


size_t
usb_parse_if_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	uint_t			if_number,
	uint_t			alt_if_setting,
	usb_if_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + 4 <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_IF) &&
		    (buf[2] == if_number) &&
		    (buf[3] == alt_if_setting)) {

			return (usb_parse_data("ccccccccc",
			    buf, _PTRDIFF(bufend, buf),
			    ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}

size_t
usba_parse_if_pwr_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	uint_t			if_number,
	uint_t			alt_if_setting,
	usba_if_pwr_descr_t	*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + 4 <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_IF) &&
		    (buf[2] == if_number) &&
		    (buf[3] == alt_if_setting)) {

			buf += buf[0];

			if (buf + 2 <= bufend) {
				if (buf[1] == USBA_DESCR_TYPE_IF_PWR_1_1) {

					return (
					    usb_parse_data("cccccccccsss", buf,
					    _PTRDIFF(bufend, buf), ret_descr,
					    ret_buf_len));
				} else {
					break;
				}
			} else {
				break;
			}
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	/* return the default interface power descriptor */
	bcopy(&default_if_power, ret_descr, USBA_IF_PWR_DESCR_SIZE);

	return (ret_descr->bLength);
}


/*
 * the endpoint index is relative to the interface. index 0 is
 * the first endpoint
 */
size_t
usb_parse_ep_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	uint_t			if_number,
	uint_t			alt_if_setting,
	uint_t			ep_index,
	usb_ep_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while ((buf + 4) <= bufend) {
		if (buf[1] == USB_DESCR_TYPE_IF &&
		    buf[2] == if_number &&
		    buf[3] == alt_if_setting) {
			if ((buf = usb_nth_descr(buf,
			    _PTRDIFF(bufend, buf),
			    USB_DESCR_TYPE_EP, ep_index,
			    USB_DESCR_TYPE_IF, -1)) == NULL) {

				break;
			}

			return (usb_parse_data("ccccsc",
			    buf, _PTRDIFF(bufend, buf),
			    ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}


/*
 * Returns (at ret_descr) a null-terminated string.  Null termination is
 * guaranteed, even if the string is longer than the buffer.  Thus, a
 * maximum of (ret_buf_len - 1) characters are returned.
 * Stops silently on first character not in UNICODE format.
 */
/*ARGSUSED*/
size_t
usba_ascii_string_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(STRING) */
	size_t			buflen,
	char			*ret_descr,
	size_t			ret_buf_len)
{
	int	i = 1;
	char	*retstart = ret_descr;
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL) ||
	    (ret_buf_len == 0) || (buflen < 2) ||
	    (buf[0] < 2) || (buf[1] != USB_DESCR_TYPE_STRING)) {

		return (USB_PARSE_ERROR);
	}

	for (buf = buf + 2; buf+1 < bufend && ret_buf_len > 1 &&
	    buf[0] != 0 && buf[1] == 0 && (i < ret_buf_len); buf += 2, i++) {
		*ret_descr++ = buf[0];
	}

	*ret_descr++ = 0;

	return (_PTRDIFF(ret_descr, retstart));
}


size_t
usb_parse_CV_cfg_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	char			*fmt,
	uint_t			descr_type,
	uint_t			descr_index,
	void			*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL) || (fmt == NULL) ||
	    (buflen < 2) || ((buf = usb_nth_descr(buf, buflen, descr_type,
	    descr_index, -1, -1)) == NULL)) {

		return (USB_PARSE_ERROR);
	}

	return (usb_parse_data(fmt, buf,
	    _PTRDIFF(bufend, buf), ret_descr,
	    ret_buf_len));
}


size_t
usb_parse_CV_if_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	char			*fmt,
	uint_t			if_number,
	uint_t			alt_if_setting,
	uint_t			descr_type,
	uint_t			descr_index,
	void			*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL) || (fmt == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + 4 <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_IF) &&
		    (buf[2] == if_number) &&
		    (buf[3] == alt_if_setting)) {
			if ((buf = usb_nth_descr(buf,
			    _PTRDIFF(bufend, buf), descr_type,
			    descr_index, USB_DESCR_TYPE_IF, -1)) ==
			    NULL) {
				break;
			}

			return (usb_parse_data(fmt, buf,
			    _PTRDIFF(bufend, buf),
			    ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infinite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}


size_t
usb_parse_CV_ep_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	char			*fmt,
	uint_t			if_number,
	uint_t			alt_if_setting,
	uint_t			ep_index,
	uint_t			descr_type,
	uint_t			descr_index,
	void			*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL) || (fmt == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + 4 <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_IF) &&
		    (buf[2] == if_number) &&
		    (buf[3] == alt_if_setting)) {
			if ((buf = usb_nth_descr(buf,
			    _PTRDIFF(bufend, buf),
			    USB_DESCR_TYPE_EP, ep_index,
			    USB_DESCR_TYPE_IF, -1)) == NULL) {

				break;
			}

			if ((buf = usb_nth_descr(buf,
			    _PTRDIFF(bufend, buf),
			    descr_type, descr_index,
			    USB_DESCR_TYPE_EP,
			    USB_DESCR_TYPE_IF)) == NULL) {

				break;
			}

			return (usb_parse_data(fmt, buf,
			    _PTRDIFF(bufend, buf),
			    ret_descr, ret_buf_len));
		}

		/*
		 * Check for a bad buffer.
		 * If buf[0] is 0, then this will be an infite loop
		 */
		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}

size_t
usb_parse_bos_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(BOS) */
	size_t			buflen,
	usb_bos_descr_t		*ret_descr,
	size_t			ret_buf_len)
{
	if ((buf == NULL) || (ret_descr == NULL) ||
	    (buflen < 2) || (buf[1] != USB_DESCR_TYPE_BOS)) {

		return (USB_PARSE_ERROR);
	}

	return (usb_parse_data("ccsc",
	    buf, buflen, ret_descr, ret_buf_len));
}

size_t
usb_parse_uwb_bos_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(BOS) */
	size_t			buflen,
	usb_uwb_cap_descr_t	*ret_descr,
	size_t			ret_buf_len)
{
	uchar_t *bufend = buf + buflen;

	if ((buf == NULL) || (ret_descr == NULL)) {

		return (USB_PARSE_ERROR);
	}

	while (buf + 3 <= bufend) {
		if ((buf[1] == USB_DESCR_TYPE_DEV_CAPABILITY) &&
		    (buf[2] == USB_CAP_TYPE_WUSB)) {

			return (usb_parse_data("ccccsccsc",
			    buf, _PTRDIFF(bufend, buf), ret_descr,
			    ret_buf_len));
		}

		INCREMENT_BUF(buf);
	}

	return (USB_PARSE_ERROR);
}

size_t
usb_parse_comp_ep_descr(uchar_t	*buf,	/* from GET_DESCRIPTOR(CONFIGURATION) */
	size_t			buflen,
	uint_t			if_number,
	uint_t			alt_if_setting,
	uint_t			ep_index,
	usb_ep_comp_descr_t	*ret_descr,
	size_t			ret_buf_len)
{
	return (usb_parse_CV_ep_descr(buf, buflen, "ccccsscc",
	    if_number, alt_if_setting, ep_index,
	    USB_DESCR_TYPE_WIRELESS_EP_COMP, 0,
	    ret_descr, ret_buf_len));
}