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

#include "lib.h"
#include "istream.h"
#include "istream-zlib.h"
#include "ostream-zlib.h"
#include "iostream-lz4.h"
#include "compression.h"

#ifndef HAVE_BZLIB
#  define i_stream_create_bz2 NULL
#  define o_stream_create_bz2_auto NULL
#endif
#ifndef HAVE_LZ4
#  define i_stream_create_lz4 NULL
#  define o_stream_create_lz4_auto NULL
#endif
#ifndef HAVE_ZSTD
#  define i_stream_create_zstd NULL
#  define o_stream_create_zstd_auto NULL
#endif

static bool is_compressed_zlib(struct istream *input)
{
	const unsigned char *data;
	size_t size;

	/* Peek into the stream and see if it looks like it's compressed
	   (based on its header). This also means that users can try to exploit
	   security holes in the uncompression library by APPENDing a specially
	   crafted mail. So let's hope zlib is free of holes. */
	data = i_stream_get_data(input, &size);
	if (size < 2)
		return FALSE;

	return data[0] == 31 && data[1] == 139;
}

static bool is_compressed_bzlib(struct istream *input)
{
	const unsigned char *data;
	size_t size;

	data = i_stream_get_data(input, &size);
	if (size < 4)
		return FALSE;
	if (memcmp(data, "BZh", 3) != 0)
		return FALSE;
	if (data[3] < '1' || data[3] > '9')
		return FALSE;
	/* The above is enough to be considered as the bzlib magic.
	   Normally it's followed by data header beginning with 0x31. However,
	   with empty compressed files it's followed by 0x17. */
	return TRUE;
}

static bool is_compressed_lz4(struct istream *input)
{
	const unsigned char *data;
	size_t size;

	data = i_stream_get_data(input, &size);
	if (size < IOSTREAM_LZ4_MAGIC_LEN)
		return FALSE;
	/* there is no standard LZ4 header, so we've created our own */
	return memcmp(data, IOSTREAM_LZ4_MAGIC, IOSTREAM_LZ4_MAGIC_LEN) == 0;
}

#define ZSTD_MAGICNUMBER            0xFD2FB528    /* valid since v0.8.0 */
static bool is_compressed_zstd(struct istream *input)
{
	const unsigned char *data;
	size_t size = 0;

	data = i_stream_get_data(input, &size);
	if (size < sizeof(uint32_t))
	        return FALSE;
	i_assert(size >= sizeof(uint32_t));

	return le32_to_cpu_unaligned(data) == ZSTD_MAGICNUMBER;
}

int compression_lookup_handler(const char *name,
			       const struct compression_handler **handler_r)
{
	unsigned int i;

	for (i = 0; compression_handlers[i].name != NULL; i++) {
		if (strcmp(name, compression_handlers[i].name) == 0) {
			if (compression_handlers[i].create_istream == NULL ||
			    compression_handlers[i].create_ostream_auto == NULL) {
				/* Handler is known but not compiled in */
				return 0;
			}
			(*handler_r) = &compression_handlers[i];
			return 1;
		}
	}
	return -1;
}

int compression_detect_handler(struct istream *input,
			       const struct compression_handler **handler_r)
{
	unsigned int i;
	ssize_t ret = 1;

	*handler_r = NULL;

	if (i_stream_get_data_size(input) == 0)
		ret = i_stream_read(input);
	while (input->stream_errno == 0) {
		for (i = 0; compression_handlers[i].name != NULL; i++) {
			if (compression_handlers[i].is_compressed != NULL &&
			    compression_handlers[i].is_compressed(input)) {
				*handler_r = &compression_handlers[i];
				return 1;
			}
		}
		if (ret >= 0)
			ret = i_stream_read(input);
		if (ret <= 0)
			break;
	};
	if (ret == 0)
		return 0;
	if (ret == -2)
		return -2;
	return -1;
}

int compression_lookup_handler_from_ext(const char *path,
					const struct compression_handler **handler_r)
{
	unsigned int i;
	size_t len, path_len = strlen(path);

	for (i = 0; compression_handlers[i].name != NULL; i++) {
		if (compression_handlers[i].ext == NULL)
			continue;

		len = strlen(compression_handlers[i].ext);
		if (path_len > len &&
		    strcmp(path + path_len - len, compression_handlers[i].ext) == 0) {
			if (compression_handlers[i].create_istream == NULL ||
			    compression_handlers[i].create_ostream_auto == NULL) {
				/* Handler is known but not compiled in */
				return 0;
			}
			(*handler_r) = &compression_handlers[i];
			return 1;
		}
	}
	return -1;
}

const struct compression_handler compression_handlers[] = {
	{
		.name = "gz",
		.ext = ".gz",
		.is_compressed = is_compressed_zlib,
		.create_istream = i_stream_create_gz,
		.create_ostream_auto = o_stream_create_gz_auto,
	},
	{
		.name = "bz2",
		.ext = ".bz2",
		.is_compressed = is_compressed_bzlib,
		.create_istream = i_stream_create_bz2,
		.create_ostream_auto = o_stream_create_bz2_auto,
	},
	{
		.name = "deflate",
		.ext = NULL,
		.is_compressed = NULL,
		.create_istream = i_stream_create_deflate,
		.create_ostream_auto = o_stream_create_deflate_auto,
	},
	{
		.name = "lz4",
		.ext = ".lz4",
		.is_compressed = is_compressed_lz4,
		.create_istream = i_stream_create_lz4,
		.create_ostream_auto = o_stream_create_lz4_auto,
	},
	{
		.name = "zstd",
		.ext = ".zstd",
		.is_compressed = is_compressed_zstd,
		.create_istream = i_stream_create_zstd,
		.create_ostream_auto = o_stream_create_zstd_auto,
	},
	{
		.name = "unsupported",
	},
	{
		.name = NULL,
	}
};
