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

#include "lib.h"
#include "base64.h"
#include "buffer.h"
#include "str.h"
#include "sha1.h"
#include "istream.h"
#include "istream-crlf.h"
#include "istream-binary-converter.h"
#include "test-common.h"

#include <stdio.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[] =
"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: binary\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
BINARY_TEXT_LONG
"\r\n--bound\r\n"
"Content-Type: text/plain\r\n"
"Content-Transfer-Encoding: binary\r\n"
"\r\n"
BINARY_TEXT_SHORT
"\n--bound\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"hello world\r\n"
"\r\n--bound--\r\n";

static const char mail_output_mime[] =
"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
"\n--bound\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"hello world\r\n"
"\r\n--bound--\r\n";

static const char mail_input_root_hdr[] =
"MIME-Version: 1.0\r\n"
"Content-Transfer-Encoding: binary\r\n"
"Content-Type: text/plain\r\n"
"\r\n";

static const char mail_output_root_hdr[] =
"MIME-Version: 1.0\r\n"
"Content-Transfer-Encoding: base64\r\n"
"Content-Type: text/plain\r\n"
"\r\n";

static const char mail_root_nonbinary[] =
"MIME-Version: 1.0\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"hello\n\n";

static void
test_istream_binary_converter_test(const char *mail_input, unsigned int mail_input_len,
				   const char *mail_output, unsigned int mail_output_len,
				   unsigned int idx)
{
	struct istream *datainput, *input;
	const unsigned char *data;
	size_t i, size;
	int ret;

	datainput = test_istream_create_data(mail_input, mail_input_len);
	test_istream_set_allow_eof(datainput, FALSE);
	input = i_stream_create_binary_converter(datainput);

	for (i = 1; i <= mail_input_len; i++) {
		test_istream_set_size(datainput, i);
		while ((ret = i_stream_read(input)) > 0) ;
		test_assert_idx(ret == 0, idx);
	}
	test_istream_set_allow_eof(datainput, TRUE);
	while ((ret = i_stream_read(input)) > 0) ;
	test_assert_idx(ret == -1, idx);

	data = i_stream_get_data(input, &size);
	test_assert_idx(size == mail_output_len &&
			memcmp(data, mail_output, size) == 0, idx);
	i_stream_unref(&input);
	i_stream_unref(&datainput);
}

static void test_istream_binary_converter_mime(void)
{
	test_begin("istream binary converter in mime parts");
	test_istream_binary_converter_test(mail_input_mime, sizeof(mail_input_mime)-1,
					   mail_output_mime, sizeof(mail_output_mime)-1, 0);
	test_end();
}

static void test_istream_binary_converter_root(void)
{
	buffer_t *inbuf = t_buffer_create(512);
	buffer_t *outbuf = t_buffer_create(512);
	const char *const suffixes[] = { "\n", "\r\n", "\n\r\n\n\n" };
	unsigned int i;
	unsigned int input_hdr_len = sizeof(mail_input_root_hdr)-1;

	test_begin("istream binary converter in root");
	buffer_append(inbuf, mail_input_root_hdr, input_hdr_len);
	buffer_append(outbuf, mail_output_root_hdr, sizeof(mail_output_root_hdr)-1);
	for (i = 0; i < N_ELEMENTS(suffixes); i++) {
		buffer_set_used_size(inbuf, input_hdr_len);
		buffer_set_used_size(outbuf, sizeof(mail_output_root_hdr)-1);
		buffer_append(inbuf, BINARY_TEXT_SHORT, sizeof(BINARY_TEXT_SHORT)-1);
		buffer_append(inbuf, suffixes[i], strlen(suffixes[i]));
		base64_encode(CONST_PTR_OFFSET(inbuf->data, input_hdr_len),
			      inbuf->used - input_hdr_len, outbuf);
		test_istream_binary_converter_test(inbuf->data, inbuf->used,
						   outbuf->data, outbuf->used, i);
	}
	test_end();
}

static void test_istream_binary_converter_root_nonbinary(void)
{
	test_begin("istream binary converter in root having non-binary");
	test_istream_binary_converter_test(mail_root_nonbinary, sizeof(mail_root_nonbinary)-1,
					   mail_root_nonbinary, sizeof(mail_root_nonbinary)-1, 0);
	test_end();
}

static void test_istream_binary_converter_bodyless_part(void)
{
	/* Bodyless MIME part with Content-Transfer-Encoding: binary and
	   no Content-Type: boundary follows headers without a blank line,
	   so end-of-headers fires as block.size == 0 with hdr_buf still
	   live. Triggered a heap-use-after-free when flushing the buffer
	   into stream_add_data() self-aliased on realloc grow. The padding
	   is needed to force a buffer realloc on the self-append. */
	static const char input[] =
		"Content-Type: multipart/mixed; boundary=\"b\"\r\n"
		"\r\n"
		"\r\n--b\r\n"
		"Content-Transfer-Encoding: binary\r\n"
		"X-Padding: "
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		"\r\n"
		"--b--\r\n";
	struct istream *datainput, *input_stream;
	const unsigned char *data;
	size_t size;
	int ret;

	test_begin("istream binary converter body-less MIME part flush");
	datainput = test_istream_create_data(input, sizeof(input)-1);
	input_stream = i_stream_create_binary_converter(datainput);

	while ((ret = i_stream_read_more(input_stream, &data, &size)) > 0)
		i_stream_skip(input_stream, size);
	test_assert(ret == -1);

	i_stream_unref(&input_stream);
	i_stream_unref(&datainput);
	test_end();
}

static int test_input_file(const char *path)
{
	struct istream *file_input, *input, *input2;
	const unsigned char *data;
	size_t size;
	struct sha1_ctxt hash;
	unsigned char hash_file[SHA1_RESULTLEN], hash_converter[SHA1_RESULTLEN];
	int ret = 0;

	lib_init();

	file_input = i_stream_create_file(path, 64);

	/* 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);
	i_stream_unref(&input);

	/* get hash when going through converter */
	i_stream_seek(file_input, 0);
	input = i_stream_create_crlf(file_input);
	input2 = i_stream_create_binary_converter(input);
	sha1_init(&hash);
	while (i_stream_read_more(input2, &data, &size) > 0) {
		sha1_loop(&hash, data, size);
		i_stream_skip(input2, size);
	}
	sha1_result(&hash, hash_converter);
	i_stream_unref(&input2);
	i_stream_unref(&input);

	if (memcmp(hash_file, hash_converter, SHA1_RESULTLEN) != 0) {
		fprintf(stderr, "istream-binary-converter: 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_binary_converter_mime,
		test_istream_binary_converter_root,
		test_istream_binary_converter_root_nonbinary,
		test_istream_binary_converter_bodyless_part,
		NULL
	};
	if (argc > 1)
		return test_input_file(argv[1]);
	else
		return test_run(test_functions);
}
