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

#include "lib.h"
#include "array.h"
#include "str.h"
#include "sha1.h"
#include "hash-format.h"
#include "istream.h"
#include "istream-crlf.h"
#include "istream-attachment-extractor.h"
#include "istream-attachment-connector.h"
#include "ostream.h"
#include "test-common.h"

#include <stdio.h>
#include <unistd.h>

#define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
"b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
"jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
#define BINARY_TEXT_LONG_BASE64 \
"d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
"aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
"Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="

#define BINARY_TEXT_SHORT "eh"
#define BINARY_TEXT_SHORT_BASE64 "ZWg="

static const char mail_input[] =
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
"\r\n"
"mime header\r\n"
"\r\n--bound\r\n"
"Content-Transfer-Encoding: base64\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
BINARY_TEXT_LONG_BASE64
"\r\n--bound\r\n"
"Content-Type: text/plain\r\n"
"Content-Transfer-Encoding: base64\r\n"
"\r\n"
BINARY_TEXT_SHORT_BASE64
"\r\n--bound--\r\n";

static const char mail_output[] =
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
"\r\n"
"mime header\r\n"
"\r\n--bound\r\n"
"Content-Transfer-Encoding: base64\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"\r\n--bound\r\n"
"Content-Type: text/plain\r\n"
"Content-Transfer-Encoding: base64\r\n"
"\r\n"
"\r\n--bound--\r\n";

static const char *mail_broken_input_body_prefix =
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
"\r\n"
"--bound\r\n"
"Content-Transfer-Encoding: base64\r\n"
"Content-Type: text/plain\r\n"
"\r\n";

static const char *mail_broken_input_bodies[] = {
	/* broken base64 input */
	"Zm9vCg=\n",
	"Zm9vCg\n",
	"Zm9vC\n",
	/* extra whitespace */
	"Zm9v\n Zm9v\n",
	"Zm9v \nZm9v\n",
	/* mixed LF vs CR LF */
	"Zm9vYmFy\r\nZm9vYmFy\n",
	"Zm9vYmFy\nZm9vYmFy\r\n",
	/* line length increases */
	"Zm9v\nZm9vYmFy\n",
	"Zm9v\nZm9vCg==",
	"Zm9v\nZm9vYgo="
};

static const char *mail_nonbroken_input_bodies[] = {
	/* suffixes with explicit '=' end */
	"Zm9vCg==",
	"Zm9vCg==\n",
	"Zm9vCg==\r\n",
	"Zm9vCg==\nfoo\n",
	"Zm9vCg==\r\nfoo\n",
	"Zm9vCg==  \t\t\n\n",
	/* suffixes with shorter line length */
	"Zm9vYmFy\nZm9v\n",
	"Zm9vYmFy\r\nZm9v\r\n",
	"Zm9vYmFy\nZm9v\nfoo\n",
	"Zm9vYmFy\r\nZm9v\r\nfoo\n",
	"Zm9vYmFy\nZm9v\n  \t\t\n\n",
	/* suffixes with empty line */
	"Zm9v\n\n",
	"Zm9v\r\n\r\n",
	"Zm9v\n\nfoo\n"
	"Zm9v\r\n\nfoo\n"
	"Zm9v\r\n\r\nfoo\n"
#if 0
	/* the whitespace here could be handled as suffixes, but for now
	   they're not: */
	"Zm9v ",
	"Zm9v \n"
#endif
};

struct attachment {
	size_t buffer_offset;
	uoff_t start_offset;
	uoff_t encoded_size, decoded_size;
	unsigned int base64_blocks_per_line;
};

static buffer_t *attachment_data;
static ARRAY(struct attachment) attachments;

static int test_open_temp_fd_callback(void *context ATTR_UNUSED)
{
	return test_create_temp_fd();
}

static int test_open_attachment_ostream(struct istream_attachment_info *info,
					struct ostream **output_r,
					const char **error_r ATTR_UNUSED,
					void *context ATTR_UNUSED)
{
	struct attachment *a;

	if (attachment_data == NULL)
		attachment_data = buffer_create_dynamic(default_pool, 1024);
	if (!array_is_created(&attachments))
		i_array_init(&attachments, 8);
	a = array_append_space(&attachments);
	a->buffer_offset = attachment_data->used;
	a->start_offset = info->start_offset;
	a->encoded_size = info->encoded_size;
	a->base64_blocks_per_line = info->base64_blocks_per_line;
	test_assert(strlen(info->hash) == 160/8*2); /* sha1 size */

	*output_r = o_stream_create_buffer(attachment_data);
	if (o_stream_seek(*output_r, a->buffer_offset) < 0)
		i_unreached();
	return 0;
}

static int
test_open_attachment_ostream_error(struct istream_attachment_info *info ATTR_UNUSED,
				   struct ostream **output_r ATTR_UNUSED,
				   const char **error_r,
				   void *context ATTR_UNUSED)
{
	*error_r = "test open error";
	return -1;
}

static int test_close_attachment_ostream(struct ostream *output, bool success,
					 const char **error_r ATTR_UNUSED,
					 void *context ATTR_UNUSED)
{
	struct attachment *a;

	i_assert(success);

	a = array_back_modifiable(&attachments);
	a->decoded_size = output->offset - a->buffer_offset;

	if (o_stream_finish(output) < 0)
		i_unreached();
	o_stream_destroy(&output);
	return 0;
}

static int
test_close_attachment_ostream_error(struct ostream *output,
				    bool success, const char **error,
				    void *context ATTR_UNUSED)
{
	if (success)
		*error = "test output error";
	o_stream_abort(output);
	o_stream_destroy(&output);
	return -1;
}

static struct istream *
test_build_original_istream(struct istream *base_input, uoff_t msg_size)
{
	struct istream_attachment_connector *conn;
	const unsigned char *data = attachment_data->data;
	const struct attachment *a;
	struct istream *input;
	uoff_t data_size = attachment_data->used;
	const char *error;

	conn = istream_attachment_connector_begin(base_input, msg_size);
	array_foreach(&attachments, a) {
		input = i_stream_create_from_data(data, a->decoded_size);
		if (istream_attachment_connector_add(conn, input,
				a->start_offset, a->encoded_size,
				a->base64_blocks_per_line, TRUE, &error) < 0)
			i_unreached();
		i_stream_unref(&input);

		i_assert(a->decoded_size <= data_size);
		data += a->decoded_size;
		data_size -= a->decoded_size;
	}
	i_assert(data_size == 0);
	return istream_attachment_connector_finish(&conn);
}

static void
get_istream_attachment_settings(struct istream_attachment_settings *set_r)
{
	const char *error;

	i_zero(set_r);
	set_r->min_size = 1;
	set_r->drain_parent_input = TRUE;
	set_r->open_temp_fd = test_open_temp_fd_callback;
	set_r->open_attachment_ostream = test_open_attachment_ostream;
	set_r->close_attachment_ostream= test_close_attachment_ostream;
	if (hash_format_init("%{sha1}", &set_r->hash_format, &error) < 0)
		i_unreached();
}

static int test_input_stream(struct istream *file_input)
{
	struct istream_attachment_settings set;
	struct istream *input, *input2;
	const unsigned char *data;
	size_t size;
	struct sha1_ctxt hash;
	uoff_t msg_size, orig_msg_size;
	buffer_t *base_buf;
	unsigned char hash_file[SHA1_RESULTLEN], hash_attached[SHA1_RESULTLEN];
	int ret = 0;

	/* get hash when directly reading input */
	input = i_stream_create_crlf(file_input);
	sha1_init(&hash);
	while (i_stream_read_more(input, &data, &size) > 0) {
		sha1_loop(&hash, data, size);
		i_stream_skip(input, size);
	}
	sha1_result(&hash, hash_file);
	msg_size = orig_msg_size = input->v_offset;
	i_stream_unref(&input);

	/* read through attachment extractor */
	get_istream_attachment_settings(&set);

	i_stream_seek(file_input, 0);
	input = i_stream_create_crlf(file_input);
	input2 = i_stream_create_attachment_extractor(input, &set, NULL);
	i_stream_unref(&input);
	base_buf = buffer_create_dynamic(default_pool, 1024);
	while (i_stream_read_more(input2, &data, &size) > 0) {
		buffer_append(base_buf, data, size);
		i_stream_skip(input2, size);
	}
	i_stream_unref(&input2);

	/* rebuild the original stream and see if the hash matches */
	for (unsigned int i = 0; i < 2; i++) {
		input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
		input = test_build_original_istream(input2, msg_size);
		i_stream_unref(&input2);

		sha1_init(&hash);
		while (i_stream_read_more(input, &data, &size) > 0) {
			sha1_loop(&hash, data, size);
			i_stream_skip(input, size);
		}
		test_assert_idx(input->eof && input->stream_errno == 0, i);
		sha1_result(&hash, hash_attached);
		i_stream_unref(&input);

		if (memcmp(hash_file, hash_attached, SHA1_RESULTLEN) != 0)
			ret = -1;

		/* try again without knowing the message's size */
		msg_size = UOFF_T_MAX;
	}

	/* try with a wrong message size */
	for (int i = 0; i < 2; i++) {
		input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
		input = test_build_original_istream(input2,
				i == 0 ? orig_msg_size + 1 : orig_msg_size - 1);
		i_stream_unref(&input2);
		while (i_stream_read_more(input, &data, &size) > 0)
			i_stream_skip(input, size);
		test_assert(input->stream_errno == (i == 0 ? EPIPE : EINVAL));
		i_stream_unref(&input);
	}

	buffer_free(&base_buf);
	buffer_free(&attachment_data);
	if (array_is_created(&attachments))
		array_free(&attachments);
	return ret;
}

static void test_istream_attachment(void)
{
	struct istream_attachment_settings set;
	struct istream *datainput, *input;
	const unsigned char *data;
	size_t i, size;
	int ret;

	test_begin("istream attachment");
	datainput = test_istream_create_data(mail_input, sizeof(mail_input));
	test_istream_set_allow_eof(datainput, FALSE);

	get_istream_attachment_settings(&set);
	input = i_stream_create_attachment_extractor(datainput, &set, NULL);

	for (i = 1; i <= sizeof(mail_input); i++) {
		test_istream_set_size(datainput, i);
		while ((ret = i_stream_read(input)) > 0) ;
		test_assert(ret == 0);
	}
	test_istream_set_allow_eof(datainput, TRUE);
	while ((ret = i_stream_read(input)) > 0) ;
	test_assert(ret == -1);

	data = i_stream_get_data(input, &size);
	test_assert(size == sizeof(mail_output) &&
		    memcmp(data, mail_output, size) == 0);

	data = attachment_data->data;
	test_assert(attachment_data->used ==
		    sizeof(BINARY_TEXT_LONG)-1 + strlen(BINARY_TEXT_SHORT));
	test_assert(memcmp(data, BINARY_TEXT_LONG, sizeof(BINARY_TEXT_LONG)-1) == 0);
	test_assert(memcmp(data + sizeof(BINARY_TEXT_LONG)-1,
			   BINARY_TEXT_SHORT, strlen(BINARY_TEXT_SHORT)) == 0);
	i_stream_unref(&input);
	i_stream_unref(&datainput);

	buffer_free(&attachment_data);
	if (array_is_created(&attachments))
		array_free(&attachments);
	test_end();
}

static bool test_istream_attachment_extractor_one(const char *body, int err_type)
{
	const size_t prefix_len = strlen(mail_broken_input_body_prefix);
	struct istream_attachment_settings set;
	struct istream *datainput, *input;
	char *mail_text;
	const unsigned char *data;
	size_t size;
	int ret;
	bool unchanged;

	mail_text = i_strconcat(mail_broken_input_body_prefix, body, NULL);
	datainput = test_istream_create_data(mail_text, strlen(mail_text));

	get_istream_attachment_settings(&set);
	if (err_type == 1)
		set.open_attachment_ostream = test_open_attachment_ostream_error;
	else if (err_type == 2)
		set.close_attachment_ostream = test_close_attachment_ostream_error;
	input = i_stream_create_attachment_extractor(datainput, &set, NULL);

	while ((ret = i_stream_read(input)) > 0) ;
	if (err_type != 0) {
		test_assert(ret == -1 && input->stream_errno == EIO);
		unchanged = FALSE;
		goto cleanup;
	}
	test_assert(ret == -1 && input->stream_errno == 0);

	data = i_stream_get_data(input, &size);
	i_assert(size >= prefix_len &&
		 memcmp(data, mail_broken_input_body_prefix, prefix_len) == 0);
	data += prefix_len;
	size -= prefix_len;

	i_assert(attachment_data != NULL);
	unchanged = attachment_data->used <= strlen(body) &&
		memcmp(attachment_data->data, body, attachment_data->used) == 0 &&
		strlen(body) - attachment_data->used == size &&
		memcmp(data, body + attachment_data->used, size) == 0;

cleanup:
	buffer_free(&attachment_data);
	if (array_is_created(&attachments))
		array_free(&attachments);

	i_stream_unref(&input);
	i_stream_unref(&datainput);
	i_free(mail_text);
	return unchanged;
}

static void test_istream_attachment_extractor(void)
{
	unsigned int i;

	test_begin("istream attachment extractor");
	for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
		test_assert(test_istream_attachment_extractor_one(mail_broken_input_bodies[i], 0));
	for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
		test_assert(!test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], 0));
	test_end();
}

static void test_istream_attachment_extractor_error(void)
{
	unsigned int i;

	test_begin("istream attachment extractor error");
	for (int err_type = 1; err_type <= 2; err_type++) {
		for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
			test_istream_attachment_extractor_one(mail_broken_input_bodies[i], err_type);
		for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
			test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], err_type);
	}
	test_end();
}

static void test_istream_attachment_connector(void)
{
	struct istream *input;

	test_begin("istream attachment connector");
	input = i_stream_create_from_data(mail_input, sizeof(mail_input));
	test_assert(test_input_stream(input) == 0);
	i_stream_unref(&input);
	test_end();
}

static int test_input_file(const char *path)
{
	struct istream *file_input;
	int ret = 0;

	lib_init();

	file_input = i_stream_create_file(path, 64);
	if (test_input_stream(file_input) < 0) {
		fprintf(stderr, "istream-attachment-extractor: mismatch on file %s\n",
			path);
		ret = -1;
	}
	i_stream_unref(&file_input);

	lib_deinit();
	return ret;
}

int main(int argc, char *argv[])
{
	static void (*const test_functions[])(void) = {
		test_istream_attachment,
		test_istream_attachment_extractor,
		test_istream_attachment_extractor_error,
		test_istream_attachment_connector,
		NULL
	};
	if (argc > 1)
		return test_input_file(argv[1]) < 0 ? 1 : 0;
	else
		return test_run(test_functions);
}
