/* Copyright (c) Pigeonhole authors, see top-level COPYING file */

/* Extension encoded-character
 * ---------------------------
 *
 * Authors: Stephan Bosch
 * Specification: RFC 5228
 * Implementation: full
 * Status: testing
 *
 */

#include "lib.h"
#include "unichar.h"

#include "sieve-extensions.h"
#include "sieve-commands.h"
#include "sieve-validator.h"

#include <ctype.h>

/*
 * Extension
 */

static bool
ext_encoded_character_validator_load(const struct sieve_extension *ext,
				     struct sieve_validator *valdtr);

const struct sieve_extension_def encoded_character_extension = {
	.name = "encoded-character",
	.validator_load = ext_encoded_character_validator_load,
};

/*
 * Encoded string argument
 */

bool arg_encoded_string_validate(struct sieve_validator *valdtr,
				 struct sieve_ast_argument **arg,
				 struct sieve_command *context);

const struct sieve_argument_def encoded_string_argument = {
	.identifier = "@encoded-string",
	.validate = arg_encoded_string_validate
};

/* Parsing */

static bool _skip_whitespace(const char **in, const char *inend)
{
	while (*in < inend) {
		if (**in == '\r') {
			(*in)++;
			if (**in != '\n')
				return FALSE;
			continue;
		}

		/* (Loose LF is non-standard) */
		if (**in != ' ' && **in != '\n' && **in != '\t')
			break;

		(*in)++;
	}

	return TRUE;
}

static bool
_parse_hexint(const char **in, const char *inend, int max_digits,
	      unsigned int *result)
{
	const char *p = *in;
	int digit = 0;

	/* Determine the width of the hex run ourselves (bounded by inend and
	   max_digits) rather than relying on str_parse_uint_hex()'s endp_r,
	   which is documented undefined on error and, for a hex run long
	   enough to overflow even its internal 64-bit accumulator, is never
	   written at all. */
	while (p < inend && (max_digits == 0 || digit < max_digits) &&
	       i_isxdigit(*p)) {
		p++;
		digit++;
	}

	if (digit == 0)
		return FALSE;

	if (digit == max_digits && p < inend && i_isxdigit(*p)) {
		/* Hex digit _MUST_ end here */
		return FALSE;
	}

	/* An overflowing hex value can never be a valid Unicode code point;
	   force it out of range rather than silently wrapping. */
	if (str_parse_uint_hex(*in, result, NULL) < 0)
		*result = UINT_MAX;

	*in = p;
	return TRUE;
}

static bool _decode_hex(const char **in, const char *inend, string_t *result)
{
	int values = 0;

	while (*in < inend) {
		unsigned int hexpair;

		if (!_skip_whitespace(in, inend))
			return FALSE;
		if (!_parse_hexint(in, inend, 2, &hexpair))
			break;

		str_append_c(result, (unsigned char)hexpair);
		values++;
	}

	return (values > 0);
}

static bool
_decode_unicode(const char **in, const char *inend, string_t *result,
		unsigned int *error_hex)
{
	int values = 0;
	bool valid = TRUE;

	while (*in < inend) {
		unsigned int unicode_hex;

		if (!_skip_whitespace(in, inend))
			return FALSE;
		if (!_parse_hexint(in, inend, 0, &unicode_hex))
			break;

		if (uni_is_valid_ucs4((unichar_t) unicode_hex))
			uni_ucs4_to_utf8_c((unichar_t)unicode_hex, result);
		else {
			if (valid)
				*error_hex = unicode_hex;
			valid = FALSE;
		}
		values++;
	}

	return (values > 0);
}

bool arg_encoded_string_validate(struct sieve_validator *valdtr,
				 struct sieve_ast_argument **arg,
				 struct sieve_command *cmd)
{
	bool result = TRUE;
	enum { ST_NONE, ST_OPEN, ST_TYPE, ST_CLOSE } state;
	string_t *str = sieve_ast_argument_str(*arg);
	string_t *tmpstr, *newstr = NULL;
	const char *p, *mark, *strstart, *substart = NULL;
	const char *strval = str_c(str);
	const char *strend = strval + str_len(str);
	unsigned int error_hex = 0;

	state = ST_NONE;
	T_BEGIN {
		tmpstr = t_str_new(32);

		p = strval;
		strstart = p;
		while (result && p < strend) {
			switch (state) {
			/* Normal string */
			case ST_NONE:
				if (*p == '$') {
					substart = p;
					state = ST_OPEN;
				}
				p++;
				break;
			/* Parsed '$' */
			case ST_OPEN:
				if (*p == '{') {
					state = ST_TYPE;
					p++;
				} else
					state = ST_NONE;
				break;
			/* Parsed '${' */
			case ST_TYPE:
				mark = p;
				/* Scan for 'hex' or 'unicode' */
				while (p < strend && i_isalpha(*p))
					p++;

				if (*p != ':') {
					state = ST_NONE;
					break;
				}

				state = ST_CLOSE;

				str_truncate(tmpstr, 0);
				if (strncasecmp(mark, "hex", p - mark) == 0) {
					/* Hexadecimal */
					p++;
					if (!_decode_hex(&p, strend, tmpstr))
						state = ST_NONE;
				} else if (strncasecmp(mark, "unicode", p - mark) == 0) {
					/* Unicode */
					p++;
					if (!_decode_unicode(&p, strend, tmpstr, &error_hex))
						state = ST_NONE;
				} else {
					/* Invalid encoding */
					p++;
					state = ST_NONE;
				}
				break;
			case ST_CLOSE:
				if (*p == '}') {
					/* We now know that the substitution is valid */

					if (error_hex != 0) {
						sieve_argument_validate_error(
							valdtr, *arg,
							"invalid unicode character 0x%08x in encoded character substitution",
							error_hex);
						result = FALSE;
						break;
					}

					if (newstr == NULL) {
						newstr = str_new(sieve_ast_pool((*arg)->ast),
								 str_len(str) * 2);
					}

					str_append_data(newstr, strstart,
							substart-strstart);
					str_append_str(newstr, tmpstr);

					strstart = p + 1;
					substart = strstart;

					p++;
				}
				state = ST_NONE;
			}
		}
	} T_END;

	if (!result)
		return FALSE;

	if (newstr != NULL) {
		if (strstart != strend)
			str_append_data(newstr, strstart, strend - strstart);

		sieve_ast_argument_string_set(*arg, newstr);
	}

	/* Pass the processed string to a (possible) next layer of processing */
	return sieve_validator_argument_activate_super(valdtr, cmd, *arg, TRUE);
}

/*
 * Extension implementation
 */

static bool
ext_encoded_character_validator_load(const struct sieve_extension *ext,
				     struct sieve_validator *valdtr)
{
	/* Override the constant string argument with our own */
	sieve_validator_argument_override(valdtr, SAT_CONST_STRING, ext,
					  &encoded_string_argument);
	return TRUE;
}
