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

/* Match-type ':regex'
 */

#include "lib.h"
#include "mempool.h"
#include "buffer.h"
#include "array.h"
#include "str.h"
#include "str-sanitize.h"

#include "sieve-common.h"
#include "sieve-limits.h"
#include "sieve-ast.h"
#include "sieve-stringlist.h"
#include "sieve-commands.h"
#include "sieve-validator.h"
#include "sieve-interpreter.h"
#include "sieve-comparators.h"
#include "sieve-match-types.h"
#include "sieve-match.h"

#include "ext-regex-common.h"
#include "dregex.h"

#include <sys/types.h>
#include <ctype.h>

/*
 * Configuration
 */

#define MCHT_REGEX_MAX_SUBSTITUTIONS SIEVE_MAX_MATCH_VALUES

/*
 * Match type
 */

static bool
mcht_regex_validate_context(struct sieve_validator *valdtr,
			    struct sieve_ast_argument *arg,
			    struct sieve_match_type_context *ctx,
			    struct sieve_ast_argument *key_arg);

static void mcht_regex_match_init(struct sieve_match_context *mctx);
static int
mcht_regex_match_keys(struct sieve_match_context *mctx,
		      const char *val, size_t val_size,
		      struct sieve_stringlist *key_list);
static void mcht_regex_match_deinit(struct sieve_match_context *mctx);

const struct sieve_match_type_def regex_match_type = {
	SIEVE_OBJECT("regex", &regex_match_type_operand, 0),
	.validate_context = mcht_regex_validate_context,
	.match_init = mcht_regex_match_init,
	.match_keys = mcht_regex_match_keys,
	.match_deinit = mcht_regex_match_deinit,
};

/*
 * Match type validation
 */

static int
mcht_regex_validate_regexp(struct sieve_validator *valdtr,
			   struct sieve_match_type_context *mtctx ATTR_UNUSED,
			   struct sieve_ast_argument *key, int cflags)
{
	int ret;
	const char *dregex_str = sieve_ast_argument_strc(key);
	const char *error;

	struct dregex_code *code = dregex_code_create();
	ret = dregex_code_compile(code, dregex_str, cflags, &error);
	dregex_code_free(&code);

	if (ret != 0) {
		sieve_argument_validate_error(
			valdtr, key,
			"invalid regular expression '%s' for regex match: %s",
			str_sanitize(dregex_str, 128),
			error);
		return -1;
	}

	return 1;
}

struct _regex_key_context {
	struct sieve_validator *valdtr;
	struct sieve_match_type_context *mtctx;
	int cflags;
};

static int
mcht_regex_validate_key_argument(void *context, struct sieve_ast_argument *key)
{
	struct _regex_key_context *keyctx = (struct _regex_key_context *)context;

	/* FIXME: We can currently only handle string literal argument, so
	   variables are not allowed.
	 */
	if (sieve_argument_is_string_literal(key)) {
		return mcht_regex_validate_regexp(keyctx->valdtr, keyctx->mtctx,
						  key, keyctx->cflags);
	}
	return 1;
}

static bool
mcht_regex_validate_context(struct sieve_validator *valdtr,
			    struct sieve_ast_argument *arg ATTR_UNUSED,
			    struct sieve_match_type_context *mtctx,
			    struct sieve_ast_argument *key_arg)
{
	const struct sieve_comparator *cmp = mtctx->comparator;
	int cflags = DREGEX_NOSUB;
	struct _regex_key_context keyctx;
	struct sieve_ast_argument *kitem;

	if (cmp != NULL) {
		if (sieve_comparator_is(cmp, i_ascii_casemap_comparator))
			cflags |= DREGEX_ICASE | DREGEX_ASCII_ONLY;
		else if (sieve_comparator_is(cmp, i_octet_comparator))
			cflags |= DREGEX_ASCII_ONLY;
		else if (sieve_comparator_is(cmp, i_unicode_casemap_comparator))
			cflags |= DREGEX_ICASE;
		else {
			sieve_argument_validate_error(
				valdtr, mtctx->argument,
				"regex match type only supports "
				"i;octet, i;ascii-casemap and i;unicode-casemap comparators");
			return FALSE;
		}
	}

	/* Validate regular expression keys */

	keyctx.valdtr = valdtr;
	keyctx.mtctx = mtctx;
	keyctx.cflags = cflags;

	kitem = key_arg;
	if (sieve_ast_stringlist_map(&kitem, &keyctx,
				     mcht_regex_validate_key_argument) <= 0)
		return FALSE;

	return TRUE;
}

/*
 * Match type implementation
 */

struct mcht_regex_key {
	struct dregex_code *regexp;
	int status;
};

struct mcht_regex_context {
	ARRAY(struct mcht_regex_key) reg_expressions;
	ARRAY_TYPE(const_string) pmatch;
	bool all_compiled:1;
	bool capture_groups;
};

static void mcht_regex_match_init(struct sieve_match_context *mctx)
{
	pool_t pool = mctx->pool;
	struct mcht_regex_context *ctx;

	/* Create context */
	ctx = p_new(pool, struct mcht_regex_context, 1);

	/* Create storage for match values if match values are requested */
	ctx->capture_groups = sieve_match_values_are_enabled(mctx->runenv);

	/* Assign context */
	mctx->data = ctx;
}

static int
mcht_regex_match_key(struct sieve_match_context *mctx, const char *val,
		     struct dregex_code *code)
{
	struct mcht_regex_context *ctx =
		(struct mcht_regex_context *)mctx->data;
	const char *error;
	int ret;
	ARRAY_TYPE(const_string) pmatch = ARRAY_INIT;

	if (ctx->capture_groups)
		i_array_init(&pmatch, 8);

	/* Execute regex */

	if (!ctx->capture_groups)
		ret = dregex_code_match(code, val, &error);
	else
		ret = dregex_code_match_groups(code, val, &pmatch, &error);

	/* Handle match values if necessary */

	if (ret > 0 && ctx->capture_groups && array_count(&pmatch) > 0) {
		struct sieve_match_values *mvalues;
		string_t *subst = t_str_new(32);
		const char *mvalue;

		/* Start new list of match values */
		mvalues = sieve_match_values_start(mctx->runenv);

		i_assert(mvalues != NULL);

		array_foreach_elem(&pmatch, mvalue) {
			str_append(subst, mvalue);
			sieve_match_values_add(mvalues, subst);
			str_truncate(subst, 0);
		}

		/* Substitute the new match values */
		sieve_match_values_commit(mctx->runenv, &mvalues);
	}

	array_free(&pmatch);
	if (ret > 0)
		return 1;
	return 0;
}

static int
mcht_regex_match_keys(struct sieve_match_context *mctx,
		      const char *val, size_t val_size ATTR_UNUSED,
		      struct sieve_stringlist *key_list)
{
	const struct sieve_runtime_env *renv = mctx->runenv;
	bool trace = sieve_runtime_trace_active(renv, SIEVE_TRLVL_MATCHING);
	struct mcht_regex_context *ctx =
		(struct mcht_regex_context *)mctx->data;
	const struct sieve_comparator *cmp = mctx->comparator;
	int match;

	if (!ctx->all_compiled) {
		string_t *key_item = NULL;
		unsigned int i;
		int ret;

		/* Regular expressions still need to be compiled */

		if (!array_is_created(&ctx->reg_expressions))
			p_array_init(&ctx->reg_expressions, mctx->pool, 16);

		i = 0;
		match = 0;
		while (match == 0 &&
		       (ret = sieve_stringlist_next_item(key_list, &key_item)) > 0) {

			T_BEGIN {
				struct mcht_regex_key *rkey;

				if (i >= array_count(&ctx->reg_expressions)) {
					int cflags = 0;

					rkey = array_append_space(&ctx->reg_expressions);

					/* Configure case-sensitivity according to comparator */
					if (sieve_comparator_is(cmp, i_octet_comparator))
						cflags |= DREGEX_ASCII_ONLY;
					else if (sieve_comparator_is(cmp, i_ascii_casemap_comparator))
						cflags |= (DREGEX_ICASE | DREGEX_ASCII_ONLY);
					else if (sieve_comparator_is(cmp, i_unicode_casemap_comparator))
						cflags |= DREGEX_ICASE;
					else
						rkey->status = -1; /* Not supported */

					if (rkey->status >= 0) {
						const char *dregex_str = str_c(key_item);
						const char *error;
						int rxret;

						/* Indicate whether match values need to be produced */
						if (!ctx->capture_groups)
							cflags |= DREGEX_NOSUB;

						struct dregex_code *code = dregex_code_create();
						/* Compile regular expression */
						rxret = dregex_code_compile(code, dregex_str, cflags, &error);
						if (rxret != 0) {
							sieve_runtime_error(renv, NULL,
								"invalid regular expression '%s' for regex match: %s",
								str_sanitize(dregex_str, 128),
								error);
							rkey->status = -1;
							dregex_code_free(&code);
						} else {
							rkey->status = 1;
							rkey->regexp = code;
						}
					}
				} else {
					rkey = array_idx_modifiable(&ctx->reg_expressions, i);
				}

				if (rkey->status > 0) {
					match = mcht_regex_match_key(
						mctx, val, rkey->regexp);

					if (trace) {
						sieve_runtime_trace(renv, 0,
							"with regex '%s' [id=%u] => %d",
							str_sanitize(str_c(key_item), 80),
							i, match);
					}
				}
			} T_END;

			i++;
		}

		if (ret == 0) {
			ctx->all_compiled = TRUE;
		} else if (ret < 0) {
			mctx->exec_status = key_list->exec_status;
			match = -1;
		}

	} else {
		const struct mcht_regex_key *rkeys;
		unsigned int i, count;

		/* Regular expressions are compiled */

		rkeys = array_get(&ctx->reg_expressions, &count);

		i = 0;
		match = 0;
		while (match == 0 && i < count) {
			if (rkeys[i].status > 0) {
				match = mcht_regex_match_key(
					mctx, val, rkeys[i].regexp);

				if (trace) {
					sieve_runtime_trace(renv, 0,
						"with compiled regex [id=%u] => %d",
						i, match);
				}
			}

			i++;
		}
	}

	return match;
}

void mcht_regex_match_deinit(struct sieve_match_context *mctx)
{
	struct mcht_regex_context *ctx =
		(struct mcht_regex_context *)mctx->data;
	struct mcht_regex_key *rkeys;
	unsigned int count, i;

	/* Clean up compiled regular expressions */
	if (array_is_created(&ctx->reg_expressions)) {
		rkeys = array_get_modifiable(&ctx->reg_expressions, &count);
		for (i = 0; i < count; i++)
			dregex_code_free(&rkeys[i].regexp);
	}
}
