/*
 * This file is part of LibCSS.
 * Licensed under the MIT License,
 *                http://www.opensource.org/licenses/mit-license.php
 * Copyright 2008 John-Mark Bell <jmb@netsurf-browser.org>
 */

#include <stdbool.h>
#include <string.h>

#include <parserutils/charset/mibenum.h>

#include "charset/detect.h"
#include "utils/utils.h"

static parserutils_error css_charset_read_bom_or_charset(const uint8_t *data,
		size_t len, uint16_t *mibenum);
static parserutils_error try_utf32_charset(const uint8_t *data,
		size_t len, uint16_t *result);
static parserutils_error try_utf16_charset(const uint8_t *data,
		size_t len, uint16_t *result);
static parserutils_error try_ascii_compatible_charset(const uint8_t *data,
		size_t len, uint16_t *result);

/**
 * Extract a charset from a chunk of data
 *
 * \param data     Pointer to buffer containing data
 * \param len      Buffer length
 * \param mibenum  Pointer to location containing current MIB enum
 * \param source   Pointer to location containing current charset source
 * \return PARSERUTILS_OK on success, appropriate error otherwise
 *
 * ::mibenum and ::source will be updated on exit
 *
 * CSS 2.1 $4.4
 */
parserutils_error css__charset_extract(const uint8_t *data, size_t len,
		uint16_t *mibenum, uint32_t *source)
{
	parserutils_error error;
	uint16_t charset = 0;

	if (data == NULL || mibenum == NULL || source == NULL)
		return PARSERUTILS_BADPARM;

	/* If the charset was dictated by the client, we've nothing to detect */
	if (*source == CSS_CHARSET_DICTATED)
		return PARSERUTILS_OK;

	/* Look for a BOM and/or @charset */
	error = css_charset_read_bom_or_charset(data, len, &charset);
	if (error != PARSERUTILS_OK)
		return error;

	if (charset != 0) {
		*mibenum = charset;
		*source = CSS_CHARSET_DOCUMENT;

		return PARSERUTILS_OK;
	}

	/* If we've already got a charset from the linking mechanism or
	 * referring document, then we've nothing further to do */
	if (*source != CSS_CHARSET_DEFAULT)
		return PARSERUTILS_OK;

	/* We've not yet found a charset, so use the default fallback */
	charset = parserutils_charset_mibenum_from_name("UTF-8", SLEN("UTF-8"));

	*mibenum = charset;
	*source = CSS_CHARSET_DEFAULT;

	return PARSERUTILS_OK;
}


/**
 * Inspect the beginning of a buffer of data for the presence of a
 * UTF Byte Order Mark and/or an @charset rule
 *
 * \param data     Pointer to buffer containing data
 * \param len      Buffer length
 * \param mibenum  Pointer to location to receive MIB enum
 * \return PARSERUTILS_OK on success, appropriate error otherwise
 */
parserutils_error css_charset_read_bom_or_charset(const uint8_t *data,
		size_t len, uint16_t *mibenum)
{
	parserutils_error error;
	uint16_t charset = 0;

	if (data == NULL)
		return PARSERUTILS_BADPARM;

	/* We require at least 4 bytes of data */
	if (len < 4)
		return PARSERUTILS_NEEDDATA;


	/* Look for BOM */
	if (data[0] == 0x00 && data[1] == 0x00 &&
			data[2] == 0xFE && data[3] == 0xFF) {
		charset = parserutils_charset_mibenum_from_name("UTF-32BE",
				SLEN("UTF-32BE"));
	} else if (data[0] == 0xFF && data[1] == 0xFE &&
			data[2] == 0x00 && data[3] == 0x00) {
		charset = parserutils_charset_mibenum_from_name("UTF-32LE",
				SLEN("UTF-32LE"));
	} else if (data[0] == 0xFE && data[1] == 0xFF) {
		charset = parserutils_charset_mibenum_from_name("UTF-16BE",
				SLEN("UTF-16BE"));
	} else if (data[0] == 0xFF && data[1] == 0xFE) {
		charset = parserutils_charset_mibenum_from_name("UTF-16LE",
				SLEN("UTF-16LE"));
	} else if (data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) {
		charset = parserutils_charset_mibenum_from_name("UTF-8",
				SLEN("UTF-8"));
	}

	/* BOM beats @charset.
	 * UAs differ here, but none appear to match the spec.
	 * The spec indicates that any @charset present in conjunction with a
	 * BOM, should match the BOM. In reality, it appears UAs just take the
	 * BOM as gospel and ignore any @charset rule. The w3c CSS validator
	 * appears to do the same (at the least, it doesn't complain about a
	 * mismatch).
	 */
	if (charset != 0) {
		*mibenum = charset;
		return PARSERUTILS_OK;
	}

	error = try_utf32_charset(data, len, &charset);
	if (error == PARSERUTILS_OK && charset != 0) {
		*mibenum = charset;
		return PARSERUTILS_OK;
	}

	error = try_utf16_charset(data, len, &charset);
	if (error == PARSERUTILS_OK && charset != 0) {
		*mibenum = charset;
		return PARSERUTILS_OK;
	}

	error = try_ascii_compatible_charset(data, len, &charset);
	if (error == PARSERUTILS_OK)
		*mibenum = charset;

	return PARSERUTILS_OK;
}

static parserutils_error try_utf32_charset(const uint8_t *data,
		size_t len, uint16_t *result)
{
	uint16_t charset = 0;

#define CHARSET_BE "\0\0\0@\0\0\0c\0\0\0h\0\0\0a\0\0\0r\0\0\0s\0\0\0e\0\0\0t\0\0\0 \0\0\0\""
#define CHARSET_LE "@\0\0\0c\0\0\0h\0\0\0a\0\0\0r\0\0\0s\0\0\0e\0\0\0t\0\0\0 \0\0\0\"\0\0\0"

	if (len <= SLEN(CHARSET_LE))
		return PARSERUTILS_NEEDDATA;

	/* Look for @charset, assuming UTF-32 source data */
	if (memcmp(data, CHARSET_LE, SLEN(CHARSET_LE)) == 0) {
		const uint8_t *start = data + SLEN(CHARSET_LE);
		const uint8_t *end;
		char buf[8];
		char *ptr = buf;

		/* Look for "; at end of charset declaration */
		for (end = start; end < data + len - 4; end += 4) {
			uint32_t c = end[0] | (end[1] << 8) |
				     (end[2] << 16) | (end[3] << 24);

			/* Bail if non-ASCII */
			if (c > 0x007f)
				break;

			/* Reached the end? */
			if (c == '"' && end < data + len - 8) {
				uint32_t d = end[4] | (end[5] << 8) |
				    (end[6] << 16) | (end[7] << 24);

				if (d == ';')
					break;
			}

			/* Append to buf, if there's space */
			if ((size_t) (ptr - buf) < sizeof(buf)) {
				/* Uppercase */
				if ('a' <= c && c <= 'z')
					*ptr++ = c & ~0x20;
				else
					*ptr++ = c;
			}
		}

		if (end == data + len - 4) {
			/* Ran out of input */
			return PARSERUTILS_NEEDDATA;
		}

		/* Ensure we have something that looks like UTF-32(LE)? */
		if ((ptr - buf == SLEN("UTF-32LE") &&
				memcmp(buf, "UTF-32LE", ptr - buf) == 0) ||
				(ptr - buf == SLEN("UTF-32") &&
				memcmp(buf, "UTF-32", ptr - buf) == 0)) {
			/* Convert to MIB enum */
			charset = parserutils_charset_mibenum_from_name(
					"UTF-32LE", SLEN("UTF-32LE"));
		}
	} else if (memcmp(data, CHARSET_BE, SLEN(CHARSET_BE)) == 0) {
		const uint8_t *start = data + SLEN(CHARSET_BE);
		const uint8_t *end;
		char buf[8];
		char *ptr = buf;

		/* Look for "; at end of charset declaration */
		for (end = start; end < data + len - 4; end += 4) {
			uint32_t c = end[3] | (end[2] << 8) |
				     (end[1] << 16) | (end[0] << 24);

			/* Bail if non-ASCII */
			if (c > 0x007f)
				break;

			/* Reached the end? */
			if (c == '"' && end < data + len - 8) {
				uint32_t d = end[7] | (end[6] << 8) |
				    (end[5] << 16) | (end[4] << 24);

				if (d == ';')
					break;
			}

			/* Append to buf, if there's space */
			if ((size_t) (ptr - buf) < sizeof(buf)) {
				/* Uppercase */
				if ('a' <= c && c <= 'z')
					*ptr++ = c & ~0x20;
				else
					*ptr++ = c;
			}
		}

		if (end == data + len - 4) {
			/* Ran out of input */
			return PARSERUTILS_NEEDDATA;
		}

		/* Ensure we have something that looks like UTF-32(BE)? */
		if ((ptr - buf == SLEN("UTF-32BE") &&
				memcmp(buf, "UTF-32BE", ptr - buf) == 0) ||
				(ptr - buf == SLEN("UTF-32") &&
				memcmp(buf, "UTF-32", ptr - buf) == 0)) {
			/* Convert to MIB enum */
			charset = parserutils_charset_mibenum_from_name(
					"UTF-32BE", SLEN("UTF-32BE"));
		}
	}

#undef CHARSET_LE
#undef CHARSET_BE

	*result = charset;

	return PARSERUTILS_OK;
}

static parserutils_error try_utf16_charset(const uint8_t *data,
		size_t len, uint16_t *result)
{
	uint16_t charset = 0;

#define CHARSET_BE "\0@\0c\0h\0a\0r\0s\0e\0t\0 \0\""
#define CHARSET_LE "@\0c\0h\0a\0r\0s\0e\0t\0 \0\"\0"

	if (len <= SLEN(CHARSET_LE))
		return PARSERUTILS_NEEDDATA;

	/* Look for @charset, assuming UTF-16 source data */
	if (memcmp(data, CHARSET_LE, SLEN(CHARSET_LE)) == 0) {
		const uint8_t *start = data + SLEN(CHARSET_LE);
		const uint8_t *end;
		char buf[8];
		char *ptr = buf;

		/* Look for "; at end of charset declaration */
		for (end = start; end < data + len - 2; end += 2) {
			uint32_t c = end[0] | (end[1] << 8);

			/* Bail if non-ASCII */
			if (c > 0x007f)
				break;

			/* Reached the end? */
			if (c == '"' && end < data + len - 4) {
				uint32_t d = end[2] | (end[3] << 8);

				if (d == ';')
					break;
			}

			/* Append to buf, if there's space */
			if ((size_t) (ptr - buf) < sizeof(buf)) {
				/* Uppercase */
				if ('a' <= c && c <= 'z')
					*ptr++ = c & ~0x20;
				else
					*ptr++ = c;
			}
		}

		if (end == data + len - 2) {
			/* Ran out of input */
			return PARSERUTILS_NEEDDATA;
		}

		/* Ensure we have something that looks like UTF-16(LE)? */
		if ((ptr - buf == SLEN("UTF-16LE") &&
				memcmp(buf, "UTF-16LE", ptr - buf) == 0) ||
				(ptr - buf == SLEN("UTF-16") &&
				memcmp(buf, "UTF-16", ptr - buf) == 0)) {
			/* Convert to MIB enum */
			charset = parserutils_charset_mibenum_from_name(
					"UTF-16LE", SLEN("UTF-16LE"));
		}
	} else if (memcmp(data, CHARSET_BE, SLEN(CHARSET_BE)) == 0) {
		const uint8_t *start = data + SLEN(CHARSET_BE);
		const uint8_t *end;
		char buf[8];
		char *ptr = buf;

		/* Look for "; at end of charset declaration */
		for (end = start; end < data + len - 2; end += 2) {
			uint32_t c = end[1] | (end[0] << 8);

			/* Bail if non-ASCII */
			if (c > 0x007f)
				break;

			/* Reached the end? */
			if (c == '"' && end < data + len - 4) {
				uint32_t d = end[3] | (end[2] << 8);

				if (d == ';')
					break;
			}

			/* Append to buf, if there's space */
			if ((size_t) (ptr - buf) < sizeof(buf)) {
				/* Uppercase */
				if ('a' <= c && c <= 'z')
					*ptr++ = c & ~0x20;
				else
					*ptr++ = c;
			}
		}

		if (end == data + len - 2) {
			/* Ran out of input */
			return PARSERUTILS_NEEDDATA;
		}

		/* Ensure we have something that looks like UTF-16(BE)? */
		if ((ptr - buf == SLEN("UTF-16BE") &&
				memcmp(buf, "UTF-16BE", ptr - buf) == 0) ||
				(ptr - buf == SLEN("UTF-16") &&
				memcmp(buf, "UTF-16", ptr - buf) == 0)) {
			/* Convert to MIB enum */
			charset = parserutils_charset_mibenum_from_name(
					"UTF-16BE", SLEN("UTF-16BE"));
		}
	}

#undef CHARSET_LE
#undef CHARSET_BE

	*result = charset;

	return PARSERUTILS_OK;
}

parserutils_error try_ascii_compatible_charset(const uint8_t *data, size_t len,
		uint16_t *result)
{
	uint16_t charset = 0;

#define CHARSET "@charset \""

	if (len <= SLEN(CHARSET))
		return PARSERUTILS_NEEDDATA;

	/* Look for @charset, assuming ASCII-compatible source data */
	if (memcmp(data, CHARSET, SLEN(CHARSET)) == 0) {
		const uint8_t *start = data + SLEN(CHARSET);
		const uint8_t *end;

		/* Look for "; at end of charset declaration */
		for (end = start; end < data + len; end++) {
			if (*end == '"' && end < data + len - 1 &&
					*(end + 1) == ';')
				break;
		}

		if (end == data + len) {
			/* Ran out of input */
			return PARSERUTILS_NEEDDATA;
		}

		/* Convert to MIB enum */
		charset = parserutils_charset_mibenum_from_name(
				(const char *) start,  end - start);

		/* Any non-ASCII compatible charset must be ignored, as
		 * we've just used an ASCII parser to read it. */
		if (charset == parserutils_charset_mibenum_from_name(
					"UTF-32", SLEN("UTF-32")) ||
			charset == parserutils_charset_mibenum_from_name(
					"UTF-32LE", SLEN("UTF-32LE")) ||
			charset == parserutils_charset_mibenum_from_name(
					"UTF-32BE", SLEN("UTF-32BE")) ||
			charset == parserutils_charset_mibenum_from_name(
					"UTF-16", SLEN("UTF-16")) ||
			charset == parserutils_charset_mibenum_from_name(
					"UTF-16LE", SLEN("UTF-16LE")) ||
			charset == parserutils_charset_mibenum_from_name(
					"UTF-16BE", SLEN("UTF-16BE"))) {

			charset = 0;
		}
	}

#undef CHARSET

	*result = charset;

	return PARSERUTILS_OK;
}
