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

#include "lib.h"
#include "array.h"
#include "istream.h"
#include "istream-chain.h"
#include "imap-arg.h"
#include "imap-parser.h"
#include "test-common.h"

static void test_imap_parser_crlf(void)
{
	static const char *test_input = "foo\r\nx\ry\n";
	struct istream *input;
	struct imap_parser *parser;
	const struct imap_arg *args;
	unsigned int i;
	enum imap_parser_error parse_error;

	test_begin("imap parser crlf handling");
	input = test_istream_create(test_input);
	parser = imap_parser_create(input, NULL, 1024, NULL);

	/* must return -2 until LF is read */
	for (i = 0; test_input[i] != '\n'; i++) {
		test_istream_set_size(input, i+1);
		(void)i_stream_read(input);
		test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
	}
	test_istream_set_size(input, i+1);
	(void)i_stream_read(input);
	test_assert(imap_parser_read_args(parser, 0, 0, &args) == 1);
	test_assert(args[0].type == IMAP_ARG_ATOM);
	test_assert(args[1].type == IMAP_ARG_EOL);

	/* CR without LF should fail with error */
	imap_parser_reset(parser);
	i_stream_seek(input, ++i);
	test_istream_set_size(input, ++i);
	(void)i_stream_read(input);
	test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
	test_istream_set_size(input, ++i);
	(void)i_stream_read(input);
	test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
	test_istream_set_size(input, ++i);
	(void)i_stream_read(input);
	test_assert(imap_parser_read_args(parser, 0, 0, &args) == -1);
	test_assert(strcmp(imap_parser_get_error
		(parser, &parse_error), "CR sent without LF") == 0 &&
		parse_error == IMAP_PARSE_ERROR_BAD_SYNTAX);

	imap_parser_unref(&parser);
	i_stream_destroy(&input);
	test_end();
}

static void test_imap_parser_partial_list(void)
{
	static const char *test_input = "((((foo {1000000}\r\n";
	struct istream *input;
	struct imap_parser *parser;
	const struct imap_arg *args, *sub_list;

	test_begin("imap parser partial list");
	input = test_istream_create(test_input);
	parser = imap_parser_create(input, NULL, 1024, NULL);

	(void)i_stream_read(input);
	test_assert(imap_parser_read_args(parser, 0,
		IMAP_PARSE_FLAG_LITERAL_SIZE, &args) == 1);
	for (unsigned int i = 0; i < 4; i++) {
		sub_list = imap_arg_as_list(&args[0]);
		test_assert(IMAP_ARG_IS_EOL(&args[1]));
		args = sub_list;
	}
	test_assert(imap_arg_atom_equals(&args[0], "foo"));
	test_assert(args[1].type == IMAP_ARG_LITERAL_SIZE);
	test_assert(IMAP_ARG_IS_EOL(&args[2]));

	imap_parser_unref(&parser);
	i_stream_destroy(&input);
	test_end();
}

static void test_imap_parser_list_limit(void)
{
	struct {
		const char *input;
		int ret;
		const char *error;
	} tests[] = {
		{ "(())\r\n", 1, NULL },
		{ "((\r\n", -1, "Missing ')'" },
		{ "(()\r\n", -1, "Missing ')'" },
		{ "(()))\r\n", -1, "Unexpected ')'" },
		{ "((()))\r\n", -1, "Too many '('" },
		{ "(({10}\r\n", -2, NULL },
		{ "((({10}\r\n", -1, "Too many '('" },
	};
	struct istream_chain *chain;
	struct istream *chain_input;
	struct imap_parser *parser;
	const struct imap_arg *args;

	test_begin("imap parser list limit");
	const struct imap_parser_params params = {
		.list_count_limit = 2,
	};

	for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
		chain_input = i_stream_create_chain(&chain, SIZE_MAX);
		parser = imap_parser_create(chain_input, NULL, 1024, &params);

		for (unsigned int j = 0; j < 2; j++) {
			struct istream *input =
				test_istream_create(tests[i].input);
			i_stream_chain_append(chain, input);
			i_stream_unref(&input);

			(void)i_stream_read(chain_input);

			test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret);
			if (tests[i].ret == -1) {
				enum imap_parser_error err;
				test_assert_strcmp_idx(imap_parser_get_error(parser, &err), tests[i].error, i);
			}
			/* skip over CRLF */
			i_stream_skip(chain_input, i_stream_get_data_size(chain_input));

			/* make sure parser reset works */
			imap_parser_reset(parser);
		}
		imap_parser_unref(&parser);
		i_stream_destroy(&chain_input);
	}
	test_end();
}

static void test_imap_parser_read_tag_cmd(void)
{
	enum read_type {
		BOTH,
		TAG,
		COMMAND
	};
	struct {
		const char *input;
		const char *tag;
		int ret;
		enum read_type type;
	} tests[] = {
		{ "tag foo", "tag", 1, BOTH },
		{ "tag\r", "tag", 1, BOTH },
		{ "tag\rfoo", "tag", 1, BOTH },
		{ "tag\nfoo", "tag", 1, BOTH },
		{ "tag\r\nfoo", "tag", 1, BOTH },
		{ "\n", NULL, -1, BOTH },
		{ "tag", NULL, 0, BOTH },
		{ "tag\t", NULL, -1, BOTH },
		{ "tag\x01", NULL, -1, BOTH },
		{ "tag\x1f", NULL, -1, BOTH },
		{ "tag\x7f", NULL, -1, BOTH },
		{ "tag\x80", NULL, -1, BOTH },
		{ "tag\xff", NULL, -1, BOTH },
		{ "tag(", NULL, -1, BOTH },
		{ "tag)", NULL, -1, BOTH },
		{ "tag{", NULL, -1, BOTH },
		{ "tag/ ", "tag/", 1, BOTH },
		{ "tag%", NULL, -1, BOTH },
		{ "tag*", NULL, -1, BOTH },
		{ "tag\"", NULL, -1, BOTH },
		{ "tag\\", NULL, -1, BOTH },
		{ "tag] ", "tag]", 1, TAG },
		{ "tag]", NULL, -1, COMMAND},
		{ "tag+", NULL, -1, TAG },
		{ "tag+ ", "tag+", 1, COMMAND },
	};
	struct istream *input;
	struct imap_parser *parser;
	const char *atom;
	int ret;

	test_begin("imap_parser_read_tag and imap_parser_read_command_name");
	for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
		if (tests[i].type != COMMAND) {
			input = test_istream_create(tests[i].input);
			test_assert(i_stream_read(input) > 0);
			parser = imap_parser_create(input, NULL, 1024, NULL);
			ret = imap_parser_read_tag(parser, &atom);
			test_assert_idx(ret == tests[i].ret, i);
			test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
			imap_parser_unref(&parser);
			i_stream_destroy(&input);
		}

		if (tests[i].type != TAG) {
			input = test_istream_create(tests[i].input);
			test_assert(i_stream_read(input) > 0);
			parser = imap_parser_create(input, NULL, 1024, NULL);
			ret = imap_parser_read_command_name(parser, &atom);
			test_assert_idx(ret == tests[i].ret, i);
			test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
			imap_parser_unref(&parser);
			i_stream_destroy(&input);
		}
	}
	test_end();
}

#define TEST_CASE(str, lit, flags, ret) \
	{ (str), sizeof((str)), (lit), (flags), (ret) }

static void test_imap_parser_read_literal(void)
{
	test_begin("imap_parser_read_literal");

	const struct {
		const char *input;
		size_t len;
		uoff_t lit_len;
		enum imap_parser_flags flags;
		int ret;
	} tests[] = {
		TEST_CASE("{0}\n\n", 0, 0, 1),
		TEST_CASE("{0+}\n\n", 0, 0, 1),
		TEST_CASE("{0}\n", 0, 0, -1),
		TEST_CASE("{-0}\n", 0, 0, -1),
		TEST_CASE("{5}\nHello\n", 5, 0, 1),
		TEST_CASE("{5+}\nHello\n\n", 5, 0, 1),
		TEST_CASE("~{5}\nHello\n", 5, IMAP_PARSE_FLAG_LITERAL8, 1),
		TEST_CASE("~{5+}\nHello\n\n", 5, IMAP_PARSE_FLAG_LITERAL8, 1),
		TEST_CASE("{6+}\nhell\0o\n", 6, 0, 1),
		TEST_CASE("~{6+}\nhell\xc3\xa5\n", 6, IMAP_PARSE_FLAG_LITERAL8, 1),
		TEST_CASE("{6+}\nhello", 0, 0, -2),
		TEST_CASE("{6+}\nhello\n", 0, 0, -1),
	};

	for (size_t i = 0; i < N_ELEMENTS(tests); i++) {
		struct istream *input =
			test_istream_create_data(tests[i].input, tests[i].len);
		test_assert(i_stream_read(input) > 0);
		struct imap_parser *parser =
			imap_parser_create(input, NULL, 1024, NULL);
		const struct imap_arg *args;
		int ret = imap_parser_read_args(parser, 0, tests[i].flags, &args);
		if (ret != tests[i].ret) {
			if (tests[i].ret < 0)
				test_failed(t_strdup_printf("%zu: Unexpected success %d", i, ret));
			else
				test_failed(t_strdup_printf("%zu: Unexpected failure %d", i, ret));
		} else if (ret >= 0) {
			test_assert_cmp_idx(args[0].str_len, ==, tests[i].lit_len, i);
		}
		imap_parser_unref(&parser);
		i_stream_destroy(&input);
	}
	test_end();
}

static void test_imap_parser_quoted_escapes(void)
{
	const struct {
		const char *input;
		const char *output;
	} tests[] = {
		{ "\"foo\"\n", "foo" },		/* no escape */
		{ "\"a\\\"b\"\n", "a\"b" },	/* "a\"b"  -> a"b */
		{ "\"a\\\\b\"\n", "a\\b" },	/* "a\\b"  -> a\b */
		{ "\"\\\"\\\\\"\n", "\"\\" },	/* "\"\\"  -> "\  */
	};

	test_begin("imap parser quoted-string escapes");
	for (size_t i = 0; i < N_ELEMENTS(tests); i++) {
		struct istream *input = test_istream_create(tests[i].input);
		(void)i_stream_read(input);
		struct imap_parser *parser =
			imap_parser_create(input, NULL, 1024, NULL);
		const struct imap_arg *args;
		const char *str;

		test_assert_idx(imap_parser_read_args(parser, 0, 0, &args) == 1, i);
		test_assert_idx(imap_arg_get_astring(&args[0], &str), i);
		test_assert_strcmp_idx(str, tests[i].output, i);

		imap_parser_unref(&parser);
		i_stream_destroy(&input);
	}
	test_end();
}

static void test_imap_arg_get_list_empty(void)
{
	/* An IMAP_ARG_LIST whose backing array is empty - i.e. holds no
	   elements at all, not even the temporary IMAP_ARG_EOL - must be
	   reported as a zero-length list. The earlier code indexed the
	   element one past the (empty) array, reading out of bounds. */
	pool_t pool = pool_alloconly_create("test imap arg", 256);
	struct imap_arg arg;
	const struct imap_arg *list;
	unsigned int count = 0xdead;

	test_begin("imap arg get_list empty");
	i_zero(&arg);
	arg.type = IMAP_ARG_LIST;
	p_array_init(&arg._data.list, pool, 1);

	test_assert(imap_arg_get_list_full(&arg, &list, &count));
	test_assert(count == 0);

	pool_unref(&pool);
	test_end();
}

int main(void)
{
	static void (*const test_functions[])(void) = {
		test_imap_parser_crlf,
		test_imap_parser_partial_list,
		test_imap_parser_list_limit,
		test_imap_parser_read_tag_cmd,
		test_imap_parser_read_literal,
		test_imap_parser_quoted_escapes,
		test_imap_arg_get_list_empty,
		NULL
	};
	return test_run(test_functions);
}
