/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "str.h"
#include "istream.h"
#include "message-header-parser.h"
#include "istream-header-filter.h"
#include "test-common.h"

struct run_ctx {
	header_filter_callback *callback;
	unsigned int callback_call_count;
	bool null_hdr_seen;
	bool eoh_seen;
	bool callback_called;
};

static void run_callback(struct header_filter_istream *input,
			 struct message_header_line *hdr,
			 bool *matched, struct run_ctx *ctx)
{
	i_assert(!ctx->null_hdr_seen);

	ctx->callback_call_count++;
	if (hdr == NULL)
		ctx->null_hdr_seen = TRUE;
	else {
		i_assert(!ctx->eoh_seen);
		if (hdr->eoh)
			ctx->eoh_seen = TRUE;
	}
	if (ctx->callback != NULL)
		ctx->callback(input, hdr, matched, NULL);
	ctx->callback_called = TRUE;
}

static inline void
test_istream_run_prep(struct run_ctx *run_ctx,
		      header_filter_callback *callback)
{
	i_zero(run_ctx);
	run_ctx->callback = callback;
	run_ctx->null_hdr_seen = FALSE;
	run_ctx->eoh_seen = FALSE;
	run_ctx->callback_called = FALSE;
}

static void
test_istream_run_check(struct run_ctx *run_ctx,
		       struct istream *filter,
		       const char *output,
		       enum header_filter_flags flags,
		       bool first,
		       size_t *size_r)
{
	const unsigned char *data;
	const struct stat *st;

	if (first)
		test_assert(run_ctx->null_hdr_seen);
	else
		test_assert(run_ctx->null_hdr_seen == run_ctx->callback_called);

	if (first && ((flags & HEADER_FILTER_ADD_MISSING_EOH) != 0))
		test_assert(run_ctx->eoh_seen);

	data = i_stream_get_data(filter, size_r);
	test_assert(*size_r == strlen(output) &&
		    memcmp(data, output, *size_r) == 0);

	test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
		    (uoff_t)st->st_size == *size_r);

	/* make sure buffer doesn't change when returning -1 */
	i_stream_skip(filter, 1);
	test_assert(i_stream_read(filter) == -1);
	test_assert(memcmp(data, output, *size_r) == 0);
}

static void
test_istream_run(struct istream *test_istream,
		 unsigned int input_len, const char *output,
		 enum header_filter_flags flags,
		 header_filter_callback *callback)
{
	struct run_ctx run_ctx;
	struct istream *filter;
	unsigned int i, orig_callback_call_count;
	size_t size;

	test_istream_run_prep(&run_ctx, callback);

	filter = i_stream_create_header_filter(test_istream, flags, NULL, 0,
					       run_callback, &run_ctx);

	for (i = 1; i < input_len; i++) {
		test_istream_set_size(test_istream, i);
		test_assert(i_stream_read(filter) >= 0);
	}
	test_istream_set_size(test_istream, input_len);
	test_assert(i_stream_read(filter) > 0);
	test_assert(i_stream_read(filter) == -1);

	test_istream_run_check(&run_ctx, filter, output, flags, TRUE, &size);
	orig_callback_call_count = run_ctx.callback_call_count;

	/* run again to make sure it's still correct the second time */
	test_istream_run_prep(&run_ctx, callback);

	i_stream_skip(filter, size);
	i_stream_seek(filter, 0);
	while (i_stream_read(filter) > 0) ;
	test_istream_run_check(&run_ctx, filter, output, flags, FALSE, &size);
	test_assert(run_ctx.callback_call_count == 0 ||
		    run_ctx.callback_call_count == orig_callback_call_count);

	i_stream_unref(&filter);
}

static void ATTR_NULL(3)
filter_callback(struct header_filter_istream *input ATTR_UNUSED,
		struct message_header_line *hdr,
		bool *matched, void *context ATTR_UNUSED)
{
	if (hdr != NULL && (hdr->name_offset == 0 ||
			    strcmp(hdr->name, "X-Drop") == 0)) {
		/* drop 1) first header, 2) X-Drop header */
		*matched = TRUE;
	}
}

static void test_istream_filter(void)
{
	static const char *exclude_headers[] = { "Subject", "To" };
	const char *input = "From: foo\nFrom: abc\nTo: bar\nSubject: plop\nX-Drop: 1\n\nhello world\n";
	const char *output = "From: abc\n\nhello world\n";
	struct istream *istream, *filter, *filter2;
	unsigned int i;
	size_t input_len = strlen(input);
	size_t output_len = strlen(output);
	const unsigned char *data;
	const struct stat *st;
	size_t size;

	test_begin("i_stream_create_header_filter: exclude");
	istream = test_istream_create(input);
	filter = i_stream_create_header_filter(istream,
					       HEADER_FILTER_EXCLUDE |
					       HEADER_FILTER_NO_CR,
					       exclude_headers,
					       N_ELEMENTS(exclude_headers),
					       filter_callback, NULL);
	filter2 = i_stream_create_header_filter(filter,
						HEADER_FILTER_EXCLUDE |
						HEADER_FILTER_NO_CR,
						exclude_headers,
						N_ELEMENTS(exclude_headers),
						*null_header_filter_callback,
						NULL);
	i_stream_unref(&filter);
	filter = filter2;

	for (i = 1; i < input_len; i++) {
		test_istream_set_size(istream, i);
		test_assert(i_stream_read(filter) >= 0);
	}
	test_istream_set_size(istream, input_len);
	test_assert(i_stream_read(filter) > 0);
	test_assert(i_stream_read(filter) == -1);

	data = i_stream_get_data(filter, &size);
	test_assert(size == output_len && memcmp(data, output, size) == 0);

	test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
		    (uoff_t)st->st_size == size);

	i_stream_skip(filter, size);
	i_stream_seek(filter, 0);
	while (i_stream_read(filter) > 0) ;
	data = i_stream_get_data(filter, &size);
	test_assert(size == output_len && memcmp(data, output, size) == 0);
	test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
		    (uoff_t)st->st_size == size);

	i_stream_unref(&filter);
	i_stream_unref(&istream);

	test_end();
}

static void add_random_text(string_t *dest, unsigned int count)
{
	unsigned int i;

	for (i = 0; i < count; i++)
		str_append_c(dest, i_rand_minmax('a', 'z'));
}

static void ATTR_NULL(3)
filter2_callback(struct header_filter_istream *input ATTR_UNUSED,
		 struct message_header_line *hdr,
		 bool *matched, bool *null_hdr_seen)
{
	if (hdr == NULL)
		*null_hdr_seen = TRUE;
	else if (strcmp(hdr->name, "To") == 0)
		*matched = TRUE;
}

static void test_istream_filter_large_buffer(void)
{
	string_t *input, *output;
	struct istream *istream, *filter;
	const struct stat *st;
	const unsigned char *data;
	size_t size, prefix_len;
	const char *p;
	unsigned int i;
	bool null_hdr_seen = FALSE;

	test_begin("i_stream_create_header_filter: large buffer");

	input = str_new(default_pool, 1024*128);
	output = str_new(default_pool, 1024*128);
	str_append(input, "From: ");
	add_random_text(input, 1024*31);
	str_append(input, "\nTo: ");
	add_random_text(input, 1024*32);
	str_append(input, "\nSubject: ");
	add_random_text(input, 1024*34);
	str_append(input, "\n\nbody\n");

	istream = test_istream_create_data(str_data(input), str_len(input));
	test_istream_set_max_buffer_size(istream, 8192);

	filter = i_stream_create_header_filter(istream,
					       HEADER_FILTER_EXCLUDE |
					       HEADER_FILTER_NO_CR,
					       NULL, 0,
					       filter2_callback,
					       &null_hdr_seen);

	for (i = 0; i < 2; i++) {
		for (;;) {
			ssize_t ret = i_stream_read(filter);
			i_assert(ret != 0);
			if (ret == -1)
				break;
			if (ret == -2) {
				unsigned char orig_data[128];
				size_t orig_data_size;

				data = i_stream_get_data(filter, &size);
				orig_data_size = I_MIN(sizeof(orig_data), size);
				memcpy(orig_data, data, orig_data_size);

				/* skip only a bit */
				size = I_MIN(size, 1);
				str_append_data(output, data, size);
				i_stream_skip(filter, size);

				/* do another read */
				ret = i_stream_read(filter);
				i_assert(ret == -2 || ret > 0);
				/* make sure the old data pointer is still
				   usable if -2 is returned */
				if (ret != -2)
					data = i_stream_get_data(filter, &size);
				else {
					test_assert(memcmp(data, orig_data, orig_data_size) == 0);
					data = i_stream_get_data(filter, &size);
				}
				str_append_data(output, data, size);
				i_stream_skip(filter, size);
			}
		}
		/* callbacks are called only once */
		test_assert(null_hdr_seen == (i == 0));

		data = i_stream_get_data(filter, &size);
		test_assert(size <= 8192);
		str_append_data(output, data, size);

		p = strstr(str_c(input), "To: ");
		i_assert(p != NULL);
		prefix_len = p - str_c(input);
		test_assert(strncmp(str_c(input), str_c(output), prefix_len) == 0);

		p = strchr(p, '\n');
		i_assert(p != NULL);
		test_assert(strcmp(p+1, str_c(output) + prefix_len) == 0);

		test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
			    (uoff_t)st->st_size == filter->v_offset + size);

		/* seek back and retry once with caching and different
		   buffer size */
		i_stream_seek(filter, 0);
		str_truncate(output, 0);
		test_istream_set_max_buffer_size(istream, 4096);
		null_hdr_seen = FALSE;
	}

	str_free(&input);
	str_free(&output);
	i_stream_unref(&filter);
	i_stream_unref(&istream);

	test_end();
}

static void test_istream_filter_large_buffer2(void)
{
	static const char *wanted_headers[] = { "References" };
	string_t *input, *output;
	struct istream *istream, *filter;
	const struct stat *st;
	const unsigned char *data;
	size_t size;
	unsigned int i;
	int ret;

	test_begin("i_stream_create_header_filter: large buffer2");

	input = str_new(default_pool, 1024*128);
	output = str_new(default_pool, 1024*128);
	str_append(input, "References: ");
	add_random_text(input, 1024*64);
	str_append(input, "\r\n\r\n");

	istream = test_istream_create_data(str_data(input), str_len(input));
	test_istream_set_max_buffer_size(istream, 8192);

	filter = i_stream_create_header_filter(istream,
		HEADER_FILTER_INCLUDE | HEADER_FILTER_HIDE_BODY,
		wanted_headers, N_ELEMENTS(wanted_headers),
		*null_header_filter_callback, NULL);

	for (i = 0; i < 2; i++) {
		while ((ret = i_stream_read_more(filter, &data, &size)) > 0) {
			str_append_data(output, data, size);
			i_stream_skip(filter, size);
		}
		test_assert(ret == -1);
		test_assert(filter->stream_errno == 0);

		test_assert(strcmp(str_c(input), str_c(output)) == 0);
		test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
			    (uoff_t)st->st_size == filter->v_offset + size);

		/* seek back and retry once with caching and different
		   buffer size */
		i_stream_seek(filter, 0);
		str_truncate(output, 0);
		test_istream_set_max_buffer_size(istream, 4096);
	}

	str_free(&input);
	str_free(&output);
	i_stream_unref(&filter);
	i_stream_unref(&istream);

	test_end();
}

static void
filter3_callback(struct header_filter_istream *input ATTR_UNUSED,
		 struct message_header_line *hdr,
		 bool *matched ATTR_UNUSED, string_t *dest)
{
	if (hdr != NULL)
		message_header_line_write(dest, hdr);
}

static void test_istream_callbacks(void)
{
	string_t *input, *output;
	const struct stat *st;
	struct istream *istream, *filter;
	unsigned int i;

	test_begin("i_stream_create_header_filter: callbacks");

	input = str_new(default_pool, 1024*128);
	output = str_new(default_pool, 1024*128);
	str_append(input, "From: first line\n ");
	add_random_text(input, 1024*31);
	str_append(input, "\nTo: first line\n\tsecond line\n\t");
	add_random_text(input, 1024*32);
	str_append(input, "\n last line\nSubject: ");
	add_random_text(input, 1024*34);
	str_append(input, "\n");

	istream = test_istream_create_data(str_data(input), str_len(input));
	test_istream_set_max_buffer_size(istream, 8192);

	filter = i_stream_create_header_filter(istream,
					       HEADER_FILTER_EXCLUDE |
					       HEADER_FILTER_NO_CR,
					       NULL, 0,
					       filter3_callback,
					       output);

	/* callback should be called exactly once for all the header input */
	for (i = 0; i < 2; i++) {
		while (i_stream_read(filter) != -1)
			i_stream_skip(filter, i_stream_get_data_size(filter));
	}

	test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
		    (uoff_t)st->st_size == str_len(output));
	test_assert(strcmp(str_c(output), str_c(input)) == 0);
	str_free(&input);
	str_free(&output);
	i_stream_unref(&filter);
	i_stream_unref(&istream);

	test_end();
}

static void ATTR_NULL(3)
edit_callback(struct header_filter_istream *input,
	      struct message_header_line *hdr,
	      bool *matched, void *context ATTR_UNUSED)
{
	if (hdr == NULL)
		return;
	if (hdr->eoh) {
		/* add a new header */
		const char *new_hdr = "Added: header\n\n";
		i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
		*matched = TRUE;
	} else if (strcasecmp(hdr->name, "To") == 0) {
		/* modify To header */
		const char *new_to = "To: 123\n";
		*matched = TRUE;
		i_stream_header_filter_add(input, new_to, strlen(new_to));
	}
}

static void test_istream_edit(void)
{
	const char *input = "From: foo\nTo: bar\n\nhello world\n";
	const char *output = "From: foo\nTo: 123\nAdded: header\n\nhello world\n";
	struct istream *istream;

	test_begin("i_stream_create_header_filter: edit headers");
	istream = test_istream_create(input);
	test_istream_run(istream, strlen(input), output,
			 HEADER_FILTER_EXCLUDE |
			 HEADER_FILTER_NO_CR,
			 edit_callback);
	i_stream_unref(&istream);

	test_end();
}

static void test_istream_end_body_with_lf(void)
{
	const char *input = "From: foo\n\nhello world";
	const char *output = "From: foo\n\nhello world\n";
	const struct stat *st;
	struct istream *istream, *filter;
	unsigned int i;
	size_t input_len = strlen(input);
	size_t output_len = strlen(output);
	const unsigned char *data;
	string_t *str = t_str_new(64);
	size_t size;

	test_begin("i_stream_create_header_filter: end_body_with_lf");
	istream = test_istream_create(input);
	filter = i_stream_create_header_filter(istream,
					       HEADER_FILTER_EXCLUDE |
					       HEADER_FILTER_NO_CR |
					       HEADER_FILTER_END_BODY_WITH_LF,
					       NULL, 0,
					       *null_header_filter_callback,
					       NULL);

	for (i = 1; i < input_len; i++) {
		test_istream_set_size(istream, i);
		test_assert(i_stream_read(filter) >= 0);
	}
	test_istream_set_size(istream, input_len);
	test_assert(i_stream_read(filter) > 0);
	test_assert(i_stream_read(filter) > 0);
	test_assert(i_stream_read(filter) == -1);

	data = i_stream_get_data(filter, &size);
	test_assert(size == output_len && memcmp(data, output, size) == 0);
	test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
		    (uoff_t)st->st_size == filter->v_offset + size);

	i_stream_skip(filter, size);
	i_stream_seek(filter, 0);
	for (i = 1; i < input_len; i++) {
		test_istream_set_size(istream, i);
		test_assert(i_stream_read(filter) >= 0);

		data = i_stream_get_data(filter, &size);
		if (size > 0)
			str_append_data(str, data, size);
		i_stream_skip(filter, size);
	}
	test_istream_set_size(istream, input_len);
	test_assert(i_stream_read(filter) == 1);
	test_assert(i_stream_read(filter) == 1);
	test_assert(i_stream_read(filter) == -1);

	data = i_stream_get_data(filter, &size);
	str_append_data(str, data, size);
	test_assert(strcmp(str_c(str), output) == 0);

	i_stream_unref(&filter);
	i_stream_unref(&istream);

	test_end();
}

static void test_istream_add_missing_eoh(void)
{
	static const struct {
		const char *input;
		const char *output;
		unsigned int extra;
	} tests[] = {
		{ "", "\n", 0 },
		{ "From: foo", "From: foo\n\n", 1 },
		{ "From: foo\n", "From: foo\n\n", 1 },
		{ "From: foo\n\n", "From: foo\n\n", 1 },
		{ "From: foo\n\nbar", "From: foo\n\nbar", 0 },
		{ "From: foo\r\n", "From: foo\r\n\r\n", 1 },
		{ "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
		{ "From: foo\r\n\r\nbar", "From: foo\r\n\r\nbar", 0 }
	};
	struct istream *istream;
	unsigned int i;

	test_begin("i_stream_create_header_filter: add missing EOH");
	for (i = 0; i < N_ELEMENTS(tests); i++) {
		istream = test_istream_create(tests[i].input);
		test_istream_run(istream,
				 strlen(tests[i].input) + tests[i].extra,
				 tests[i].output,
				 HEADER_FILTER_EXCLUDE |
				 HEADER_FILTER_CRLF_PRESERVE |
				 HEADER_FILTER_ADD_MISSING_EOH,
				 *null_header_filter_callback);
		i_stream_unref(&istream);
	}
	test_end();
}

static void test_istream_add_missing_eoh_and_edit(void)
{
	static const struct {
		const char *input;
		const char *output;
	} tests[] = {
		{ "From: foo\nTo: bar\n",
		  "From: foo\nTo: 123\nAdded: header\n\n" },
		{ "From: foo\nTo: bar\n\n",
		  "From: foo\nTo: 123\nAdded: header\n\n" },
		{ "From: foo\nTo: bar\n\nbody\n",
		  "From: foo\nTo: 123\nAdded: header\n\nbody\n" },
	};
	struct istream *istream;
	unsigned int i;

	test_begin("i_stream_create_header_filter: add missing EOH and edit headers");
	for (i = 0; i < N_ELEMENTS(tests); i++) {
		istream = test_istream_create(tests[i].input);
		test_istream_run(istream, strlen(tests[i].input), tests[i].output,
				 HEADER_FILTER_EXCLUDE |
				 HEADER_FILTER_ADD_MISSING_EOH |
				 HEADER_FILTER_NO_CR,
				 edit_callback);
		i_stream_unref(&istream);
	}
	test_end();
}

static void test_istream_hide_body(void)
{
	static const struct {
		const char *input;
		const char *output;
		int extra;
	} tests[] = {
		{ "From: foo", "From: foo", 0 },
		{ "From: foo\n", "From: foo\n", 0 },
		{ "From: foo\n\n", "From: foo\n\n", 1 },
		{ "From: foo\n\nbar", "From: foo\n\n", -2 },
		{ "From: foo\r\n", "From: foo\r\n", 0 },
		{ "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
		{ "From: foo\r\n\r\nbar", "From: foo\r\n\r\n", -3 }
	};
	struct istream *istream;
	unsigned int i;

	test_begin("i_stream_create_header_filter: hide body");
	for (i = 0; i < N_ELEMENTS(tests); i++) {
		istream = test_istream_create(tests[i].input);
		test_istream_run(istream,
				 (int)strlen(tests[i].input) + tests[i].extra,
				 tests[i].output,
				 HEADER_FILTER_EXCLUDE |
				 HEADER_FILTER_CRLF_PRESERVE |
				 HEADER_FILTER_HIDE_BODY,
				 *null_header_filter_callback);
		i_stream_unref(&istream);
	}
	test_end();
}

static void ATTR_NULL(3)
strip_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
		   struct message_header_line *hdr,
		   bool *matched, void *context ATTR_UNUSED)
{
	if (hdr != NULL && hdr->eoh)
		*matched = TRUE;
}

static void test_istream_strip_eoh(void)
{
	const char *input = "From: foo\nTo: bar\n\nhello world\n";
	const char *output = "From: foo\nTo: bar\nhello world\n";
	struct istream *istream;

	test_begin("i_stream_create_header_filter: strip_eoh");
	istream = test_istream_create(input);
	test_istream_run(istream, strlen(input), output,
			 HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
			 strip_eoh_callback);
	i_stream_unref(&istream);

	test_end();
}

static void ATTR_NULL(3)
missing_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
		     struct message_header_line *hdr,
		     bool *matched ATTR_UNUSED, void *context ATTR_UNUSED)
{
	if (hdr == NULL) {
		const char *new_hdr = "Subject: added\n\n";
		i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
	}
}

static void test_istream_missing_eoh_callback(void)
{
	const char *input = "From: foo\nTo: bar\n";
	const char *output = "From: foo\nTo: bar\nSubject: added\n\n";
	struct istream *istream;

	test_begin("i_stream_create_header_filter: add headers when EOH is missing");
	istream = test_istream_create(input);
	test_istream_run(istream, strlen(input) + 1, output,
			 HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
			 missing_eoh_callback);
	i_stream_unref(&istream);
	test_end();
}

static void test_istream_empty_missing_eoh_callback(void)
{
	const char *input = "";
	const char *output = "Subject: added\n\n";
	struct istream *istream;

	test_begin("i_stream_create_header_filter: add headers when mail is empty");
	istream = test_istream_create(input);
	test_istream_run(istream, strlen(input)+1, output,
			 HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
			 missing_eoh_callback);
	i_stream_unref(&istream);
	test_end();
}

int main(void)
{
	static void (*const test_functions[])(void) = {
		test_istream_filter,
		test_istream_filter_large_buffer,
		test_istream_filter_large_buffer2,
		test_istream_callbacks,
		test_istream_edit,
		test_istream_add_missing_eoh,
		test_istream_add_missing_eoh_and_edit,
		test_istream_end_body_with_lf,
		test_istream_hide_body,
		test_istream_strip_eoh,
		test_istream_missing_eoh_callback,
		test_istream_empty_missing_eoh_callback,
		NULL
	};
	return test_run(test_functions);
}
