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

#include "sieve-extensions.h"
#include "sieve-commands.h"
#include "sieve-stringlist.h"
#include "sieve-code.h"
#include "sieve-comparators.h"
#include "sieve-match-types.h"
#include "sieve-address-parts.h"

#include "sieve-validator.h"
#include "sieve-generator.h"
#include "sieve-binary.h"
#include "sieve-interpreter.h"
#include "sieve-dump.h"
#include "sieve-match.h"

#include "ext-body-common.h"

/*
 * Body test
 *
 * Syntax
 *   body [COMPARATOR] [MATCH-TYPE] [BODY-TRANSFORM]
 *     <key-list: string-list>
 */

static bool
tst_body_registered(struct sieve_validator *valdtr,
		    const struct sieve_extension *ext,
		    struct sieve_command_registration *cmd_reg);
static bool
tst_body_validate(struct sieve_validator *valdtr, struct sieve_command *tst);
static bool
tst_body_generate(const struct sieve_codegen_env *cgenv,
		  struct sieve_command *ctx);

const struct sieve_command_def body_test = {
	.identifier = "body",
	.type = SCT_TEST,
	.positional_args = 1,
	.subtests = 0,
	.block_allowed = FALSE,
	.block_required = FALSE,
	.registered = tst_body_registered,
	.validate = tst_body_validate,
	.generate = tst_body_generate
};

/*
 * Body operation
 */

static bool
ext_body_operation_dump(const struct sieve_dumptime_env *denv,
			sieve_size_t *address);
static int
ext_body_operation_execute(const struct sieve_runtime_env *renv,
			   sieve_size_t *address);

const struct sieve_operation_def body_operation = {
	.mnemonic = "body",
	.ext_def = &body_extension,
	.dump = ext_body_operation_dump,
	.execute = ext_body_operation_execute
};

/*
 * Optional operands
 */

enum tst_body_optional {
	OPT_BODY_TRANSFORM = SIEVE_MATCH_OPT_LAST
};

/*
 * Tagged arguments
 */

/* Forward declarations */

static bool
tag_body_transform_validate(struct sieve_validator *valdtr,
			    struct sieve_ast_argument **arg,
			    struct sieve_command *cmd);
static bool
tag_body_transform_generate(const struct sieve_codegen_env *cgenv,
			    struct sieve_ast_argument *arg,
			    struct sieve_command *cmd);

/* Argument objects */

static const struct sieve_argument_def body_raw_tag = {
	.identifier = "raw",
	.validate = tag_body_transform_validate,
	.generate = tag_body_transform_generate
};

static const struct sieve_argument_def body_content_tag = {
	.identifier = "content",
	.validate = tag_body_transform_validate,
	.generate = tag_body_transform_generate
};

static const struct sieve_argument_def body_text_tag = {
	.identifier = "text",
	.validate = tag_body_transform_validate,
	.generate = tag_body_transform_generate
};

/* Argument implementation */

static bool
tag_body_transform_validate(struct sieve_validator *valdtr,
			    struct sieve_ast_argument **arg,
			    struct sieve_command *cmd)
{
	enum tst_body_transform transform;
	struct sieve_ast_argument *tag = *arg;

	/* BODY-TRANSFORM:
	     :raw
	       / :content <content-types: string-list>
	       / :text
	 */
	if ((bool)cmd->data) {
		sieve_argument_validate_error(
			valdtr, *arg,
			"the :raw, :content and :text arguments for the body test are mutually "
			"exclusive, but more than one was specified");
		return FALSE;
	}

	/* Skip tag */
	*arg = sieve_ast_argument_next(*arg);

	/* :content tag has a string-list argument */
	if (sieve_argument_is(tag, body_raw_tag))
		transform = TST_BODY_TRANSFORM_RAW;
	else if (sieve_argument_is(tag, body_text_tag))
		transform = TST_BODY_TRANSFORM_TEXT;
	else if (sieve_argument_is(tag, body_content_tag)) {
		/* Check syntax:
		 *   :content <content-types: string-list>
		 */
		if (!sieve_validate_tag_parameter(valdtr, cmd, tag, *arg,
						  NULL, 0, SAAT_STRING_LIST,
						  FALSE))
			return FALSE;

		/* Assign tag parameters */
		tag->parameters = *arg;
		*arg = sieve_ast_arguments_detach(*arg,1);

		transform = TST_BODY_TRANSFORM_CONTENT;
	} else {
		return FALSE;
	}

	/* Signal the presence of this tag */
	cmd->data = (void *)TRUE;

	/* Assign context data */
	tag->argument->data = (void *)transform;

	return TRUE;
}

/*
 * Command Registration
 */

static bool
tst_body_registered(struct sieve_validator *valdtr,
		    const struct sieve_extension *ext,
		    struct sieve_command_registration *cmd_reg)
{
	/* The order of these is not significant */
	sieve_comparators_link_tag(valdtr, cmd_reg, SIEVE_MATCH_OPT_COMPARATOR);
	sieve_match_types_link_tags(valdtr, cmd_reg,
				    SIEVE_MATCH_OPT_MATCH_TYPE);

	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &body_raw_tag, OPT_BODY_TRANSFORM);
	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &body_content_tag, OPT_BODY_TRANSFORM);
	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &body_text_tag, OPT_BODY_TRANSFORM);

	return TRUE;
}

/*
 * Validation
 */

static bool
tst_body_validate(struct sieve_validator *valdtr, struct sieve_command *tst)
{
	struct sieve_ast_argument *arg = tst->first_positional;
	const struct sieve_match_type mcht_default =
		SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
	const struct sieve_comparator cmp_default =
		SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);

	if (!sieve_validate_positional_argument(valdtr, tst, arg, "key list",
						1, SAAT_STRING_LIST))
		return FALSE;

	if (!sieve_validator_argument_activate(valdtr, tst, arg, FALSE))
		return FALSE;

	/* Validate the key argument to a specified match type */
	return sieve_match_type_validate(valdtr, tst, arg,
					 &mcht_default, &cmp_default);
}

/*
 * Code generation
 */

static bool
tst_body_generate(const struct sieve_codegen_env *cgenv,
		  struct sieve_command *cmd)
{
	(void)sieve_operation_emit(cgenv->sblock, cmd->ext, &body_operation);

	/* Generate arguments */
	return sieve_generate_arguments(cgenv, cmd, NULL);
}

static bool
tag_body_transform_generate(const struct sieve_codegen_env *cgenv,
			    struct sieve_ast_argument *arg,
			    struct sieve_command *cmd ATTR_UNUSED)
{
	enum tst_body_transform transform =
		POINTER_CAST_TO(arg->argument->data, enum tst_body_transform);

	sieve_binary_emit_byte(cgenv->sblock, transform);
	sieve_generate_argument_parameters(cgenv, cmd, arg);
	return TRUE;
}

/*
 * Code dump
 */

static bool
ext_body_operation_dump(const struct sieve_dumptime_env *denv,
			sieve_size_t *address)
{
	unsigned int transform;
	int opt_code = 0;

	sieve_code_dumpf(denv, "BODY");
	sieve_code_descend(denv);

	/* Handle any optional arguments */
	for (;;) {
		int opt;

		opt = sieve_match_opr_optional_dump(denv, address, &opt_code);
		if (opt	< 0)
			return FALSE;
		if (opt == 0)
			break;

		switch (opt_code) {
		case OPT_BODY_TRANSFORM:
			if (!sieve_binary_read_byte(denv->sblock, address,
						    &transform))
				return FALSE;

			switch (transform) {
			case TST_BODY_TRANSFORM_RAW:
				sieve_code_dumpf(denv, "BODY-TRANSFORM: RAW");
				break;
			case TST_BODY_TRANSFORM_TEXT:
				sieve_code_dumpf(denv, "BODY-TRANSFORM: TEXT");
				break;
			case TST_BODY_TRANSFORM_CONTENT:
				sieve_code_dumpf(
					denv, "BODY-TRANSFORM: CONTENT");

				sieve_code_descend(denv);
				if (!sieve_opr_stringlist_dump(denv, address,
							       "content types"))
					return FALSE;
				sieve_code_ascend(denv);
				break;
			default:
				return FALSE;
			}
			break;
		default:
			return FALSE;
		}
	};

	return sieve_opr_stringlist_dump(denv, address, "key list");
}

/*
 * Interpretation
 */

static int
ext_body_operation_execute(const struct sieve_runtime_env *renv,
			   sieve_size_t *address)
{
	int opt_code = 0;
	struct sieve_comparator cmp =
		SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);
	struct sieve_match_type mcht =
		SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
	unsigned int transform = TST_BODY_TRANSFORM_TEXT;
	struct sieve_stringlist *ctype_list, *value_list, *key_list;
	bool mvalues_active;
	const char *const *content_types = NULL;
	int match, ret;

	/*
	 * Read operands
	 */

	/* Optional operands */

	ctype_list = NULL;
	for (;;) {
		int opt;

		opt = sieve_match_opr_optional_read(renv, address, &opt_code,
						    &ret, &cmp, &mcht);
		if (ret < 0)
			return ret;
		if (opt == 0)
			break;

		switch (opt_code) {
		case OPT_BODY_TRANSFORM:
			if (!sieve_binary_read_byte(renv->sblock, address,
						    &transform) ||
			    transform > TST_BODY_TRANSFORM_TEXT) {
				sieve_runtime_trace_error(
					renv, "invalid body transform type");
				return SIEVE_EXEC_BIN_CORRUPT;
			}

			if (transform == TST_BODY_TRANSFORM_CONTENT &&
			    (ret = sieve_opr_stringlist_read(
				renv, address, "content-type-list",
				&ctype_list)) <= 0)
				return ret;
			break;
		default:
			sieve_runtime_trace_error(
				renv, "unknown optional operand");
			return SIEVE_EXEC_BIN_CORRUPT;
		}
	}

	/* Read key-list */

	ret = sieve_opr_stringlist_read(renv, address, "key-list", &key_list);
	if (ret <= 0)
		return ret;

	if (ctype_list != NULL &&
	    sieve_stringlist_read_all(ctype_list, pool_datastack_create(),
				      &content_types) < 0) {
		sieve_runtime_trace_error(
			renv, "failed to read content-type-list operand");
		return ctype_list->exec_status;
	}

	/*
	 * Perform operation
	 */

	sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS, "body test");

	/* Extract requested parts */
	ret = ext_body_get_part_list(renv, (enum tst_body_transform)transform,
				     content_types, &value_list);
	if (ret <= 0)
		return ret;

	/* Disable match values processing as required by RFC */
	mvalues_active = sieve_match_values_set_enabled(renv, FALSE);

	/* Perform match */
	match = sieve_match(renv, &mcht, &cmp, value_list, key_list, &ret);

	/* Restore match values processing */
	(void)sieve_match_values_set_enabled(renv, mvalues_active);

	if (match < 0)
		return ret;

	/* Set test result for subsequent conditional jump */
	sieve_interpreter_set_test_result(renv->interp, match > 0);
	return SIEVE_EXEC_OK;
}
