/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file
 */

#include "lib.h"
#include "array.h"

#include "sieve-common.h"
#include "sieve-stringlist.h"
#include "sieve-extensions.h"
#include "sieve-code.h"
#include "sieve-commands.h"
#include "sieve-validator.h"
#include "sieve-generator.h"
#include "sieve-interpreter.h"

#include "ext-ihave-common.h"

/*
 * Ihave test
 *
 * Syntax:
 *   ihave <capabilities: string-list>
 */

static bool
tst_ihave_validate(struct sieve_validator *valdtr, struct sieve_command *tst);
static bool
tst_ihave_validate_const(struct sieve_validator *valdtr,
			 struct sieve_command *tst, int *const_current,
			 int const_next);
static bool
tst_ihave_generate(const struct sieve_codegen_env *cgenv,
		   struct sieve_command *tst);

const struct sieve_command_def ihave_test = {
	.identifier = "ihave",
	.type = SCT_TEST,
	.positional_args = 1,
	.subtests = 0,
	.block_allowed = FALSE,
	.block_required = FALSE,
	.validate = tst_ihave_validate,
	.validate_const = tst_ihave_validate_const,
	.generate = tst_ihave_generate,
};

/*
 * Ihave operation
 */

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

const struct sieve_operation_def tst_ihave_operation = {
	.mnemonic = "IHAVE",
	.ext_def = &ihave_extension,
	.code = EXT_IHAVE_OPERATION_IHAVE,
	.dump = tst_ihave_operation_dump,
	.execute = tst_ihave_operation_execute,
};

/*
 * Code validation
 */

static bool
tst_ihave_validate(struct sieve_validator *valdtr, struct sieve_command *tst)
{
	struct _capability {
		const struct sieve_extension *ext;
		struct sieve_ast_argument *arg;
	};
	struct sieve_ast_argument *arg = tst->first_positional;
	struct sieve_ast_argument *stritem;
	enum sieve_compile_flags cpflags =
		sieve_validator_compile_flags(valdtr);
	bool no_global = ((cpflags & SIEVE_COMPILE_FLAG_NOGLOBAL) != 0);
	ARRAY(struct _capability) capabilities;
	struct _capability capability;
	const struct _capability *caps;
	unsigned int i, count;
	bool all_known = TRUE;

	t_array_init(&capabilities, 64);

	tst->data = (void *)FALSE;

	/* Check stringlist argument */
	if (!sieve_validate_positional_argument(valdtr, tst, arg,
						"capabilities", 1,
						SAAT_STRING_LIST))
		return FALSE;

	switch (sieve_ast_argument_type(arg)) {
	case SAAT_STRING:
		/* Single string */
		capability.arg = arg;
		capability.ext = sieve_extension_get_by_name(
			tst->ext->svinst, sieve_ast_argument_strc(arg));

		if (capability.ext == NULL ||
		    (no_global && capability.ext->global)) {
			all_known = FALSE;

			ext_ihave_ast_add_missing_extension(
				tst->ext, tst->ast_node->ast,
				sieve_ast_argument_strc(arg));
		} else {
			array_append(&capabilities, &capability, 1);
		}

		break;

	case SAAT_STRING_LIST:
		/* String list */
		stritem = sieve_ast_strlist_first(arg);

		while (stritem != NULL) {
			capability.arg = stritem;
			capability.ext = sieve_extension_get_by_name(
				tst->ext->svinst,
				sieve_ast_argument_strc(stritem));

			if (capability.ext == NULL ||
			    (no_global && capability.ext->global)) {
				all_known = FALSE;

				ext_ihave_ast_add_missing_extension(
					tst->ext, tst->ast_node->ast,
					sieve_ast_argument_strc(stritem));
			} else {
				array_append(&capabilities, &capability, 1);
			}
			stritem = sieve_ast_strlist_next(stritem);
		}

		break;
	default:
		i_unreached();
	}

	if (!all_known)
		return TRUE;

	/* RFC 5463, Section 4, page 4:

	   The "ihave" extension is designed to be used with other extensions
	   that add tests, actions, comparators, or arguments.  Implementations
	   MUST NOT allow it to be used with extensions that change the
	   underlying Sieve grammar, or extensions like encoded-character
	   [RFC5228], or variables [RFC5229] that change how the content of
	   Sieve scripts are interpreted.  The test MUST fail and the extension
	   MUST NOT be enabled if such usage is attempted.

	   FIXME: current implementation of this restriction is hardcoded and
	   therefore highly inflexible
	 */
	caps = array_get(&capabilities, &count);
	for (i = 0; i < count; i++) {
		if (sieve_extension_name_is(caps[i].ext, "variables") ||
		    sieve_extension_name_is(caps[i].ext, "encoded-character"))
			return TRUE;
	}

	/* Load all extensions */
	caps = array_get(&capabilities, &count);
	for (i = 0; i < count; i++) {
		if (!sieve_validator_extension_load(valdtr, tst, caps[i].arg,
						    caps[i].ext, FALSE))
			return FALSE;
	}

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

	tst->data = (void *)TRUE;
	return TRUE;
}

static bool
tst_ihave_validate_const(struct sieve_validator *valdtr ATTR_UNUSED,
			 struct sieve_command *tst, int *const_current,
			 int const_next ATTR_UNUSED)
{
	if ((bool)tst->data == TRUE)
		*const_current = -1;
	else
		*const_current = 0;
	return TRUE;
}

/*
 * Code generation
 */

bool tst_ihave_generate(const struct sieve_codegen_env *cgenv,
			struct sieve_command *tst)
{
	/* Emit opcode */
	sieve_operation_emit(cgenv->sblock, tst->ext, &tst_ihave_operation);

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

/*
 * Code dump
 */

static bool
tst_ihave_operation_dump(const struct sieve_dumptime_env *denv,
			 sieve_size_t *address)
{
	sieve_code_dumpf(denv, "IHAVE");
	sieve_code_descend(denv);

	return sieve_opr_stringlist_dump(denv, address, "capabilities");
}

/*
 * Code execution
 */

static int
tst_ihave_operation_execute(const struct sieve_runtime_env *renv,
			    sieve_size_t *address)
{
	const struct sieve_execute_env *eenv = renv->exec_env;
	struct sieve_instance *svinst = eenv->svinst;
	struct sieve_stringlist *capabilities;
	string_t *cap_item;
	bool matched;
	int ret;

	/*
	 * Read operands
	 */

	/* Read capabilities */
	if ((ret = sieve_opr_stringlist_read(renv, address, "capabilities",
					     &capabilities)) <= 0)
		return ret;

	/*
	 * Perform test
	 */

	/* Perform the test */
	sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS, "ihave test");
	sieve_runtime_trace_descend(renv);

	cap_item = NULL;
	matched = TRUE;
	while (matched &&
	       (ret = sieve_stringlist_next_item(capabilities,
						 &cap_item)) > 0) {
		const struct sieve_extension *ext;
		int sret;

		ext = sieve_extension_get_by_name(svinst, str_c(cap_item));
		if (ext == NULL) {
			sieve_runtime_trace_error(
				renv, "ihave: invalid extension name");
			return SIEVE_EXEC_BIN_CORRUPT;
		}
		sret = sieve_interpreter_extension_start(renv->interp, ext);
		if (sret == SIEVE_EXEC_FAILURE) {
			sieve_runtime_trace(
				renv, SIEVE_TRLVL_TESTS,
				"extension '%s' not available",
				sieve_extension_name(ext));
			matched = FALSE;
		} else if (sret == SIEVE_EXEC_OK) {
			sieve_runtime_trace(
				renv, SIEVE_TRLVL_TESTS,
				"extension '%s' available",
				sieve_extension_name(ext));
		} else {
			return sret;
		}
	}
	if (ret < 0) {
		sieve_runtime_trace_error(renv, "invalid capabilities item");
		return SIEVE_EXEC_BIN_CORRUPT;
	}

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