// SPDX-License-Identifier: 0BSD

///////////////////////////////////////////////////////////////////////////////
//
/// \file       test_lzip_decoder.c
/// \brief      Tests decoding lzip data
//
//  Author:     Jia Tan
//
///////////////////////////////////////////////////////////////////////////////

#include "tests.h"

#ifdef HAVE_LZIP_DECODER

// Memlimit large enough to pass all of the test files
#define MEMLIMIT (1U << 20)
#define DECODE_CHUNK_SIZE 1024


// The uncompressed data in the test files are short US-ASCII strings.
// The tests check if the decompressed output is what it is expected to be.
// Storing the strings here as text would break the tests on EBCDIC systems
// and storing the strings as an array of hex values is inconvenient, so
// store the CRC32 values of the expected data instead.
//
// CRC32 value of "Hello\nWorld\n"
static const uint32_t hello_world_crc = 0x15A2A343;

// CRC32 value of "Trailing garbage\n"
static const uint32_t trailing_garbage_crc = 0x87081A60;


// Helper function to decode a good file with no flags and plenty high memlimit
static void
basic_lzip_decode(const char *src, const uint32_t expected_crc)
{
	size_t file_size;
	uint8_t *data = tuktest_file_from_srcdir(src, &file_size);
	uint32_t checksum = 0;

	lzma_stream strm = LZMA_STREAM_INIT;
	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT, 0), LZMA_OK);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	strm.next_in = data;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	// Feed 1 byte at a time to the decoder to look for any bugs
	// when switching between decoding sequences
	lzma_ret ret = LZMA_OK;
	while (ret == LZMA_OK) {
		strm.avail_in = 1;
		ret = lzma_code(&strm, LZMA_RUN);
		if (strm.avail_out == 0) {
			checksum = lzma_crc32(output_buffer,
				(size_t)(strm.next_out - output_buffer),
				checksum);
			strm.next_out = output_buffer;
			strm.avail_out = sizeof(output_buffer);
		}
	}

	assert_lzma_ret(ret, LZMA_STREAM_END);
	assert_uint_eq(strm.total_in, file_size);

	checksum = lzma_crc32(output_buffer,
			(size_t)(strm.next_out - output_buffer),
			checksum);
	assert_uint_eq(checksum, expected_crc);

	lzma_end(&strm);
}


static void
test_options(void)
{
	// Test NULL stream
	assert_lzma_ret(lzma_lzip_decoder(NULL, MEMLIMIT, 0),
			LZMA_PROG_ERROR);

	// Test invalid flags
	lzma_stream strm = LZMA_STREAM_INIT;
	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT, UINT32_MAX),
			LZMA_OPTIONS_ERROR);
	// Memlimit tests are done elsewhere
}


static void
test_v0_decode(void)
{
	// This tests if liblzma can decode lzip version 0 files.
	// lzip 1.17 and older can decompress this, but lzip 1.18
	// and newer can no longer decode these files.
	basic_lzip_decode("files/good-1-v0.lz", hello_world_crc);
}


static void
test_v1_decode(void)
{
	// This tests decoding a basic lzip v1 file
	basic_lzip_decode("files/good-1-v1.lz", hello_world_crc);
}


// Helper function to decode a good file with trailing bytes after
// the lzip stream
static void
trailing_helper(const char *src, const uint32_t expected_data_checksum,
		const uint32_t expected_trailing_checksum)
{
	size_t file_size;
	uint32_t checksum = 0;
	uint8_t *data = tuktest_file_from_srcdir(src, &file_size);
	lzma_stream strm = LZMA_STREAM_INIT;
	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	strm.next_in = data;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	lzma_ret ret = LZMA_OK;
	while (ret == LZMA_OK) {
		ret = lzma_code(&strm, LZMA_RUN);
		if (strm.avail_out == 0) {
			checksum = lzma_crc32(output_buffer,
				(size_t)(strm.next_out - output_buffer),
				checksum);
			strm.next_out = output_buffer;
			strm.avail_out = sizeof(output_buffer);
		}
	}

	assert_lzma_ret(ret, LZMA_STREAM_END);
	assert_uint(strm.total_in, <, file_size);

	checksum = lzma_crc32(output_buffer,
			(size_t)(strm.next_out - output_buffer),
			checksum);

	assert_uint_eq(checksum, expected_data_checksum);

	// Trailing data should be readable from strm.next_in
	checksum = lzma_crc32(strm.next_in, strm.avail_in, 0);
	assert_uint_eq(checksum, expected_trailing_checksum);

	lzma_end(&strm);
}


// Helper function to decode a bad file and compare to returned error to
// what the caller expects
static void
decode_expect_error(const char *src, lzma_ret expected_error)
{
	lzma_stream strm = LZMA_STREAM_INIT;
	size_t file_size;
	uint8_t *data = tuktest_file_from_srcdir(src, &file_size);

	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	strm.avail_in = file_size;
	strm.next_in = data;

	lzma_ret ret = LZMA_OK;

	do {
		// Discard output since we are only looking for errors
		strm.next_out = output_buffer;
		strm.avail_out = sizeof(output_buffer);
		if (strm.avail_in == 0)
			ret = lzma_code(&strm, LZMA_FINISH);
		else
			ret = lzma_code(&strm, LZMA_RUN);
	} while (ret == LZMA_OK);

	assert_lzma_ret(ret, expected_error);
	lzma_end(&strm);
}


static void
test_v0_trailing(void)
{
	trailing_helper("files/good-1-v0-trailing-1.lz", hello_world_crc,
			trailing_garbage_crc);
}


static void
test_v1_trailing(void)
{
	trailing_helper("files/good-1-v1-trailing-1.lz", hello_world_crc,
			trailing_garbage_crc);

	// The second files/good-1-v1-trailing-2.lz will have the same
	// expected output and trailing output as
	// files/good-1-v1-trailing-1.lz, but this tests if the prefix
	// to the trailing data contains lzip magic bytes.
	// When this happens, the expected behavior is to silently ignore
	// the magic byte prefix and consume it from the input file.
	trailing_helper("files/good-1-v1-trailing-2.lz", hello_world_crc,
			trailing_garbage_crc);

	// Expect LZMA_BUF error if a file ends with the lzip magic bytes
	// but does not contain any data after
	decode_expect_error("files/bad-1-v1-trailing-magic.lz",
			LZMA_BUF_ERROR);
}


static void
test_concatenated(void)
{
	// First test a file with one v0 member and one v1 member
	// The first member should contain "Hello\n" and
	// the second member should contain "World!\n"
	lzma_stream strm = LZMA_STREAM_INIT;
	size_t file_size;
	uint8_t *v0_v1 = tuktest_file_from_srcdir("files/good-2-v0-v1.lz",
			&file_size);

	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	strm.next_in = v0_v1;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_STREAM_END);

	assert_uint_eq(strm.total_in, file_size);

	uint32_t checksum = lzma_crc32(output_buffer, strm.total_out, 0);
	assert_uint_eq(checksum, hello_world_crc);

	// The second file contains one v1 member and one v2 member
	uint8_t *v1_v0 = tuktest_file_from_srcdir("files/good-2-v1-v0.lz",
			&file_size);

	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	strm.next_in = v1_v0;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_STREAM_END);

	assert_uint_eq(strm.total_in, file_size);
	checksum = lzma_crc32(output_buffer, strm.total_out, 0);
	assert_uint_eq(checksum, hello_world_crc);

	// The third file contains 2 v1 members
	uint8_t *v1_v1 = tuktest_file_from_srcdir("files/good-2-v1-v1.lz",
			&file_size);

	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	strm.next_in = v1_v1;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_STREAM_END);

	assert_uint_eq(strm.total_in, file_size);
	checksum = lzma_crc32(output_buffer, strm.total_out, 0);
	assert_uint_eq(checksum, hello_world_crc);

	lzma_end(&strm);
}


static void
test_crc(void)
{
	// Test invalid checksum
	lzma_stream strm = LZMA_STREAM_INIT;
	size_t file_size;
	uint8_t *data = tuktest_file_from_srcdir("files/bad-1-v1-crc32.lz",
			&file_size);

	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED), LZMA_OK);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	strm.next_in = data;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_DATA_ERROR);

	// Test ignoring the checksum value - should decode successfully
	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED | LZMA_IGNORE_CHECK), LZMA_OK);

	strm.next_in = data;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_STREAM_END);
	assert_uint_eq(strm.total_in, file_size);

	// Test tell check
	assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT,
			LZMA_CONCATENATED | LZMA_TELL_ANY_CHECK), LZMA_OK);

	strm.next_in = data;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_GET_CHECK);
	assert_uint_eq(lzma_get_check(&strm), LZMA_CHECK_CRC32);
	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_DATA_ERROR);
	lzma_end(&strm);
}


static void
test_invalid_magic_bytes(void)
{
	uint8_t lzip_id_string[] = { 0x4C, 0x5A, 0x49, 0x50 };
	lzma_stream strm = LZMA_STREAM_INIT;

	for (uint32_t i = 0; i < ARRAY_SIZE(lzip_id_string); i++) {
		// Corrupt magic bytes
		lzip_id_string[i] ^= 1;
		uint8_t output_buffer[DECODE_CHUNK_SIZE];

		assert_lzma_ret(lzma_lzip_decoder(&strm, MEMLIMIT, 0),
				LZMA_OK);

		strm.next_in = lzip_id_string;
		strm.avail_in = sizeof(lzip_id_string);
		strm.next_out = output_buffer;
		strm.avail_out = sizeof(output_buffer);

		assert_lzma_ret(lzma_code(&strm, LZMA_RUN),
				LZMA_FORMAT_ERROR);

		// Reset magic bytes
		lzip_id_string[i] ^= 1;
	}

	lzma_end(&strm);
}


static void
test_invalid_version(void)
{
	// The file contains a version number that is not 0 or 1,
	// so it should cause an error
	decode_expect_error("files/unsupported-1-v234.lz",
			LZMA_OPTIONS_ERROR);
}


static void
test_invalid_dictionary_size(void)
{
	// The first file has a too small dictionary size field.
	decode_expect_error("files/bad-1-v1-dict-1.lz", LZMA_DATA_ERROR);

	// The second file has a too large dictionary size field.
	decode_expect_error("files/bad-1-v1-dict-2.lz", LZMA_DATA_ERROR);
}


static void
test_invalid_uncomp_size(void)
{
	// Test invalid v0 lzip file uncomp size
	decode_expect_error("files/bad-1-v0-uncomp-size.lz",
			LZMA_DATA_ERROR);

	// Test invalid v1 lzip file uncomp size
	decode_expect_error("files/bad-1-v1-uncomp-size.lz",
			LZMA_DATA_ERROR);
}


static void
test_invalid_member_size(void)
{
	decode_expect_error("files/bad-1-v1-member-size.lz",
			LZMA_DATA_ERROR);
}


static void
test_invalid_memlimit(void)
{
	// A very low memlimit should prevent decoding.
	// It should be possible to update the memlimit after the error.
	size_t file_size;
	uint8_t *data = tuktest_file_from_srcdir("files/good-1-v1.lz",
			&file_size);

	uint8_t output_buffer[DECODE_CHUNK_SIZE];

	lzma_stream strm = LZMA_STREAM_INIT;

	assert_lzma_ret(lzma_lzip_decoder(&strm, 1, 0), LZMA_OK);

	strm.next_in = data;
	strm.avail_in = file_size;
	strm.next_out = output_buffer;
	strm.avail_out = sizeof(output_buffer);

	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_MEMLIMIT_ERROR);

	// Up the memlimit so that decoding can continue.
	// First only increase by a small amount and expect an error.
	assert_lzma_ret(lzma_memlimit_set(&strm, 100), LZMA_MEMLIMIT_ERROR);
	assert_lzma_ret(lzma_memlimit_set(&strm, MEMLIMIT), LZMA_OK);

	// Finish decoding
	assert_lzma_ret(lzma_code(&strm, LZMA_FINISH), LZMA_STREAM_END);

	assert_uint_eq(strm.total_in, file_size);
	uint32_t checksum = lzma_crc32(output_buffer, strm.total_out, 0);
	assert_uint_eq(checksum, hello_world_crc);

	lzma_end(&strm);
}
#endif


extern int
main(int argc, char **argv)
{
	tuktest_start(argc, argv);

#ifndef HAVE_LZIP_DECODER
	tuktest_early_skip("lzip decoder disabled");
#else
	tuktest_run(test_options);
	tuktest_run(test_v0_decode);
	tuktest_run(test_v1_decode);
	tuktest_run(test_v0_trailing);
	tuktest_run(test_v1_trailing);
	tuktest_run(test_concatenated);
	tuktest_run(test_crc);
	tuktest_run(test_invalid_magic_bytes);
	tuktest_run(test_invalid_version);
	tuktest_run(test_invalid_dictionary_size);
	tuktest_run(test_invalid_uncomp_size);
	tuktest_run(test_invalid_member_size);
	tuktest_run(test_invalid_memlimit);
	return tuktest_end();
#endif
}
