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

#include "test-lib.h"
#include "str.h"
#include "istream-private.h"
#include "istream-base64.h"
#include "istream-sized.h"
#include "base64.h"

struct base64_istream_test {
	const char *input;
	const char *output;
	int stream_errno;
};

static const struct base64_istream_test base64_tests[] = {
	{ "", "", 0 },
	{ "aGVsbG8gd29ybGQ=", "hello world", 0 },
	{ "\naGVs\nbG8g\nd29y\nbGQ=\n", "hello world", 0 },
	{ "  aGVs    \r\n bG8g  \r\n   d29y  \t \r\n    bGQ= \r\n\r\n",
	  "hello world", 0 },
	{ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC+INC60YPRgCDQtNC+0Y/MgdGCLg==",
	  "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
	  "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
	  "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
	  "\x81\xd1\x82\x2e", 0 },
	{ "\r", "", 0 },
	{ "\n", "", 0 },
	{ "\r\n", "", 0 },
	{ "  ", "", 0 },
	{ "foo", "\x7e\x8a", EPIPE },
	{ "foo ","\x7e\x8a", EPIPE },
	{ "Zm9vC", "foo", EPIPE },
	{ "Zm9v!", "foo", EINVAL },
	{ "Zm9!v", "fo", EINVAL },
	{ "Zm9 v", "foo", 0 },
	{ "Zm 9v", "foo", 0 },
	{ "Z m9v", "foo", 0 },
};

static const struct base64_istream_test base64url_tests[] = {
	{ "", "", 0 },
	{ "aGVsbG8gd29ybGQ=", "hello world", 0 },
	{ "\naGVs\nbG8g\nd29y\nbGQ=\n", "hello world", 0 },
	{ "  aGVs    \r\n bG8g  \r\n   d29y  \t \r\n    bGQ= \r\n\r\n",
	  "hello world", 0 },
	{ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC-INC60YPRgCDQtNC-0Y_MgdGCLg==",
	  "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
	  "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
	  "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
	  "\x81\xd1\x82\x2e", 0 },
	{ "\r", "", 0 },
	{ "\n", "", 0 },
	{ "\r\n", "", 0 },
	{ "  ", "", 0 },
	{ "foo", "\x7e\x8a", EPIPE },
	{ "foo ","\x7e\x8a", EPIPE },
	{ "Zm9vC", "foo", EPIPE },
	{ "Zm9v!", "foo", EINVAL },
	{ "Zm9!v", "fo", EINVAL },
	{ "Zm9 v", "foo", 0 },
	{ "Zm 9v", "foo", 0 },
	{ "Z m9v", "foo", 0 },
};

static void
decode_test(unsigned int base64_input_len,
	    struct istream *input_data, struct istream *input,
	    const char *output, int stream_errno)
{
	const unsigned char *data;
	size_t i, size;
	int ret = 0;

	for (i = 1; i <= base64_input_len; i++) {
		test_istream_set_size(input_data, i);
		while ((ret = i_stream_read(input)) > 0) ;
		if (ret == -1 && stream_errno != 0)
			break;
		test_assert(ret == 0);
	}
	if (ret == 0) {
		test_istream_set_allow_eof(input_data, TRUE);
		while ((ret = i_stream_read(input)) > 0) ;
	}
	test_assert(ret == -1);
	test_assert(input->stream_errno == stream_errno);

	data = i_stream_get_data(input, &size);
	test_assert(size == strlen(output));
	if (size > 0)
		test_assert(memcmp(data, output, size) == 0);
}

static void
decode_base64_test(const char *base64_input, const char *output,
		   int stream_errno)
{
	unsigned int base64_input_len = strlen(base64_input);
	struct istream *input_data, *input;

	input_data = test_istream_create_data(base64_input, base64_input_len);
	test_istream_set_allow_eof(input_data, FALSE);
	input = i_stream_create_base64_decoder(input_data);

	decode_test(base64_input_len, input_data, input, output, stream_errno);

	i_stream_unref(&input);
	i_stream_unref(&input_data);
}

static void
decode_base64url_test(const char *base64_input, const char *output,
		      int stream_errno)
{
	unsigned int base64_input_len = strlen(base64_input);
	struct istream *input_data, *input;

	input_data = test_istream_create_data(base64_input, base64_input_len);
	test_istream_set_allow_eof(input_data, FALSE);
	input = i_stream_create_base64url_decoder(input_data);

	decode_test(base64_input_len, input_data, input, output, stream_errno);

	i_stream_unref(&input);
	i_stream_unref(&input_data);
}

static void
test_istream_base64_io_random(void)
{
	unsigned char in_buf[2048];
	size_t in_buf_size;
	buffer_t *out_buf;
	unsigned int i, j, loop_count = ON_VALGRIND ? 100 : 4000;
	int ret;

	out_buf = t_buffer_create(sizeof(in_buf));

	test_begin("istream base64 random I/O");

	for (i = 0; !test_has_failed() && i < loop_count; i++) {
		struct istream *input1, *input2, *input3, *input4, *input5;
		struct istream *sinput1, *sinput2, *sinput3, *sinput4;
		struct istream *top_input;
		const unsigned char *data;
		unsigned int chpl1, chpl2;
		unsigned int sized_streams;
		unsigned int crlf_encode;
		size_t size;
		struct base64_encoder b64enc;

		/* Initialize test data */
		in_buf_size = i_rand_limit(sizeof(in_buf));
		for (j = 0; j < in_buf_size; j++)
			in_buf[j] = i_rand_uchar();

		/* Reset final output buffer */
		buffer_set_used_size(out_buf, 0);

		/* Determine line lengths */
		chpl1 = i_rand_limit(30)*4;
		chpl2 = i_rand_limit(30)*4;

		/* Create stream for test data */
		input1 = i_stream_create_from_data(in_buf, in_buf_size);
		i_stream_set_name(input1, "[data]");

		/* Determine which stages have sized streams */
		sized_streams = i_rand_minmax(0x00, 0x0f);
		/* Determine which stages use CRLF */
		crlf_encode = i_rand_minmax(0x00, 0x03);

		/* Create first encoder stream */
		input2 = i_stream_create_base64_encoder(
			input1, chpl1, HAS_ALL_BITS(crlf_encode, BIT(0)));
		i_stream_set_name(input2, "[base64_encoder #1]");

		if (HAS_ALL_BITS(sized_streams, BIT(0))) {
			/* Wrap the first encoder stream in a sized stream to
			   check size and trigger any buffer overflow problems
			 */
			base64_encode_init(&b64enc, &base64_scheme,
					   (HAS_ALL_BITS(crlf_encode, BIT(0)) ?
					    BASE64_ENCODE_FLAG_CRLF : 0),
					   chpl1);
			sinput1 = i_stream_create_sized(input2,
				base64_get_full_encoded_size(&b64enc,
							     in_buf_size));
			i_stream_set_name(sinput1, "[sized #1]");
		} else {
			sinput1 = input2;
			i_stream_ref(sinput1);
		}

		/* Create first decoder stream */
		input3 = i_stream_create_base64_decoder(sinput1);
		i_stream_set_name(input3, "[base64_decoder #1]");

		if (HAS_ALL_BITS(sized_streams, BIT(1))) {
			/* Wrap the first decoder stream in a sized stream to
			   check size and trigger any buffer overflow problems
			 */
			sinput2 = i_stream_create_sized(input3, in_buf_size);
			i_stream_set_name(sinput2, "[sized #2]");
		} else {
			sinput2 = input3;
			i_stream_ref(sinput2);
		}

		/* Create second encoder stream */
		input4 = i_stream_create_base64_encoder(
			sinput2, chpl2, HAS_ALL_BITS(crlf_encode, BIT(1)));
		i_stream_set_name(input4, "[base64_encoder #2]");

		if (HAS_ALL_BITS(sized_streams, BIT(2))) {
			/* Wrap the second encoder stream in a sized stream to
			   check size and trigger any buffer overflow problems
			 */
			base64_encode_init(&b64enc, &base64_scheme,
					   (HAS_ALL_BITS(crlf_encode, BIT(1)) ?
					    BASE64_ENCODE_FLAG_CRLF : 0),
					   chpl2);
			sinput3 = i_stream_create_sized(input4,
				base64_get_full_encoded_size(&b64enc,
							    in_buf_size));
			i_stream_set_name(sinput3, "[sized #3]");
		} else {
			sinput3 = input4;
			i_stream_ref(sinput3);
		}

		/* Create second decoder stream */
		input5 = i_stream_create_base64_decoder(sinput3);
		i_stream_set_name(input5, "[base64_decoder #2]");

		if (HAS_ALL_BITS(sized_streams, BIT(3))) {
			/* Wrap the second decoder stream in a sized stream to
			   check size and trigger any buffer overflow problems
			 */
			sinput4 = i_stream_create_sized(input5, in_buf_size);
			i_stream_set_name(sinput4, "[sized #4]");
		} else {
			sinput4 = input5;
			i_stream_ref(sinput4);
		}


		/* Assign random buffer sizes */
		i_stream_set_max_buffer_size(input5, i_rand_minmax(1, 512));
		i_stream_set_max_buffer_size(input4, i_rand_minmax(1, 512));
		i_stream_set_max_buffer_size(input3, i_rand_minmax(1, 512));
		i_stream_set_max_buffer_size(input2, i_rand_minmax(1, 512));

		/* Read the outer stream in full with random increments. */
		top_input = sinput4;
		while ((ret = i_stream_read_more(
			top_input, &data, &size)) > 0) {
			size_t ch = i_rand_limit(512);

			size = I_MIN(size, ch);
			buffer_append(out_buf, data, size);
			i_stream_skip(top_input, size);
		}
		if (ret < 0 && top_input->stream_errno == 0) {
			data = i_stream_get_data(top_input, &size);
			if (size > 0) {
				buffer_append(out_buf, data, size);
				i_stream_skip(top_input, size);
			}
		}

		/* Assert stream status */
		test_assert_idx(ret < 0 && top_input->stream_errno == 0, i);
		/* Assert input/output equality */
		test_assert_idx(out_buf->used == in_buf_size &&
				memcmp(in_buf, out_buf->data, in_buf_size) == 0,
				i);

		if (top_input->stream_errno != 0) {
			i_error("%s: %s", i_stream_get_name(input1),
			       i_stream_get_error(input1));
			i_error("%s: %s", i_stream_get_name(input2),
			       i_stream_get_error(input2));
			i_error("%s: %s", i_stream_get_name(input3),
			       i_stream_get_error(input3));
			i_error("%s: %s", i_stream_get_name(input4),
			       i_stream_get_error(input4));
			i_error("%s: %s", i_stream_get_name(input5),
			       i_stream_get_error(input5));
		}

		if (test_has_failed()) {
			i_info("Test parameters: size=%zu "
			       "line_length_1=%u line_length_2=%u",
				in_buf_size, chpl1, chpl2);
		}

		/* Clean up */
		i_stream_unref(&input1);
		i_stream_unref(&input2);
		i_stream_unref(&input3);
		i_stream_unref(&input4);
		i_stream_unref(&input5);
		i_stream_unref(&sinput1);
		i_stream_unref(&sinput2);
		i_stream_unref(&sinput3);
		i_stream_unref(&sinput4);
	}
	test_end();
}

void test_istream_base64_decoder(void)
{
	unsigned int i;

	for (i = 0; i < N_ELEMENTS(base64_tests); i++) {
		const struct base64_istream_test *test = &base64_tests[i];

		test_begin(t_strdup_printf("istream base64 decoder %u", i+1));
		decode_base64_test(test->input, test->output,
				   test->stream_errno);
		test_end();
	}

	for (i = 0; i < N_ELEMENTS(base64url_tests); i++) {
		const struct base64_istream_test *test = &base64url_tests[i];

		test_begin(t_strdup_printf("istream base64url decoder %u",
					   i+1));
		decode_base64url_test(test->input, test->output,
				      test->stream_errno);
		test_end();
	}

	test_istream_base64_io_random();
}
