/* Copyright (c) 2002-2018 Pigeonhole authors, see the included 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)
{
	int digit = 0;
	*result = 0;

	while ( *in < inend && (max_digits == 0 || digit < max_digits) ) {

		if ( (**in) >= '0' && (**in) <= '9' )
			*result = ((*result) << 4) + (**in) - ((unsigned int) '0');
		else if ( (**in) >= 'a' && (**in) <= 'f' )
			*result = ((*result) << 4) + (**in) - ((unsigned int) 'a') + 0x0a;
		else if ( (**in) >= 'A' && (**in) <= 'F' )
			*result = ((*result) << 4) + (**in) - ((unsigned int) 'A') + 0x0a;
		else
			return ( digit > 0 );

		(*in)++;
		digit++;
	}

	if ( digit == max_digits ) {
		/* Hex digit _MUST_ end here */
		if ( (**in >= '0' && **in <= '9')	|| (**in >= 'a' && **in <= 'f') ||
			(**in >= 'A' && **in <= 'F') )
			return FALSE;

		return TRUE;
	}

	return ( digit > 0 );
}

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 = ST_NONE;
	string_t *str = sieve_ast_argument_str(*arg);
	string_t *tmpstr, *newstr = NULL;
	const char *p, *mark, *strstart, *substart = NULL;
	const char *strval = (const char *) str_data(str);
	const char *strend = strval + str_len(str);
	unsigned int error_hex = 0;

	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;
}
