/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */

#include "lib.h"

#ifdef HAVE_ZSTD

#include "buffer.h"
#include "istream-private.h"
#include "istream-zlib.h"

#include "zstd.h"
#include "zstd_errors.h"
#include "iostream-zstd-private.h"

#ifndef HAVE_ZSTD_GETERRORCODE
ZSTD_ErrorCode ZSTD_getErrorCode(size_t functionResult)
{
	ssize_t errcode = (ssize_t)functionResult;
	if (errcode < 0)
		return -errcode;
	return ZSTD_error_no_error;
}
#endif

struct zstd_istream {
	struct istream_private istream;

	ZSTD_DStream *dstream;
	ZSTD_inBuffer input;
	ZSTD_outBuffer output;

	struct stat last_parent_statbuf;

	/* ZSTD input size */
	size_t input_size;

	/* storage for frames */
	buffer_t *frame_buffer;

	/* storage for data */
	buffer_t *data_buffer;

	bool hdr_read:1;
	bool marked:1;
	bool zs_closed:1;
	/* is there data remaining */
	bool remain:1;
};

static void i_stream_zstd_init(struct zstd_istream *zstream)
{
	zstream->dstream = ZSTD_createDStream();
	if (zstream->dstream == NULL)
		i_fatal_status(FATAL_OUTOFMEM, "zstd: Out of memory");
	ZSTD_initDStream(zstream->dstream);
	zstream->input_size = ZSTD_DStreamInSize();
	if (zstream->frame_buffer == NULL)
		zstream->frame_buffer = buffer_create_dynamic(default_pool, ZSTD_DStreamInSize());
	else
		buffer_set_used_size(zstream->frame_buffer, 0);
	if (zstream->data_buffer == NULL)
		zstream->data_buffer = buffer_create_dynamic(default_pool, ZSTD_DStreamOutSize());
	else
		buffer_set_used_size(zstream->data_buffer, 0);
	zstream->zs_closed = FALSE;
}

static void i_stream_zstd_deinit(struct zstd_istream *zstream, bool reuse_buffers)
{
	(void)ZSTD_freeDStream(zstream->dstream);
	zstream->dstream = NULL;
	if (!reuse_buffers) {
		buffer_free(&zstream->frame_buffer);
		buffer_free(&zstream->data_buffer);
	}
	zstream->zs_closed = TRUE;
	i_zero(&zstream->input);
}

static void i_stream_zstd_close(struct iostream_private *stream,
				bool close_parent)
{
	struct istream_private *_istream =
		container_of(stream, struct istream_private, iostream);
	struct zstd_istream *zstream =
		container_of(_istream, struct zstd_istream, istream);
	if (!zstream->zs_closed)
		i_stream_zstd_deinit(zstream, FALSE);
	buffer_free(&zstream->frame_buffer);
	if (close_parent)
		i_stream_close(zstream->istream.parent);
}

static void i_stream_zstd_read_error(struct zstd_istream *zstream, size_t err)
{
	ZSTD_ErrorCode errcode = zstd_version_errcode(ZSTD_getErrorCode(err));
	const char *error = ZSTD_getErrorName(err);
	if (errcode == ZSTD_error_memory_allocation)
		i_fatal_status(FATAL_OUTOFMEM, "zstd.read(%s): Out of memory",
			       i_stream_get_name(&zstream->istream.istream));
	else if (errcode == ZSTD_error_prefix_unknown ||
#if HAVE_DECL_ZSTD_ERROR_PARAMETER_UNSUPPORTED == 1
		 errcode == ZSTD_error_parameter_unsupported ||
#endif

		 errcode == ZSTD_error_dictionary_wrong ||
		 errcode == ZSTD_error_init_missing)
		zstream->istream.istream.stream_errno = EINVAL;
	else
		zstream->istream.istream.stream_errno = EIO;

	io_stream_set_error(&zstream->istream.iostream,
			    "zstd.read(%s): %s at %"PRIuUOFF_T,
			    i_stream_get_name(&zstream->istream.istream), error,
			    i_stream_get_absolute_offset(&zstream->istream.istream));
}

static ssize_t i_stream_zstd_read(struct istream_private *stream)
{
	struct zstd_istream *zstream =
		container_of(stream, struct zstd_istream, istream);
	const unsigned char *data;
	size_t size;

	if (stream->istream.eof)
		return -1;

	for (;;) {
		if (zstream->data_buffer->used > 0) {
			if (!i_stream_try_alloc(stream, stream->max_buffer_size, &size))
				return -2;
			size = I_MIN(zstream->data_buffer->used, size);
			memcpy(PTR_OFFSET(stream->w_buffer,stream->pos),
			       zstream->data_buffer->data, size);
			stream->pos += size;
			buffer_delete(zstream->data_buffer, 0, size);
			return size;
		}

		/* see if we can get more */
		if (zstream->input.pos == zstream->input.size) {
			ssize_t ret;
			buffer_set_used_size(zstream->frame_buffer, 0);
			/* need to read more */
			if ((ret = i_stream_read_more(stream->parent, &data, &size)) < 0) {
				stream->istream.stream_errno =
					stream->parent->stream_errno;
				stream->istream.eof = stream->parent->eof;
				if (stream->istream.stream_errno != 0)
					;
				else if (!zstream->hdr_read)
					stream->istream.stream_errno = EINVAL;
				else if (zstream->remain)
					/* truncated data */
					stream->istream.stream_errno = EPIPE;
				return ret;
			}
			if (ret == 0)
				return 0;
			buffer_append(zstream->frame_buffer, data, size);
			/* NOTE: All of the parent stream input is skipped
			   over here. This is why there's no need to call
			   i_stream_set_input_pending() here like with other
			   compression istreams. */
			i_stream_skip(stream->parent, size);
			zstream->input.src = zstream->frame_buffer->data;
			zstream->input.size = zstream->frame_buffer->used;
			zstream->input.pos = 0;
		}

		i_assert(zstream->input.size > 0);
		i_assert(zstream->data_buffer->used == 0);
		zstream->output.dst = buffer_append_space_unsafe(zstream->data_buffer,
								 ZSTD_DStreamOutSize());
		zstream->output.pos = 0;
		zstream->output.size = ZSTD_DStreamOutSize();

		size_t zret = ZSTD_decompressStream(zstream->dstream, &zstream->output,
						    &zstream->input);
		if (ZSTD_isError(zret) != 0) {
			i_stream_zstd_read_error(zstream, zret);
			return -1;
		}
		/* ZSTD magic number is 4 bytes, but it's only defined after v0.8 */
		if (!zstream->hdr_read) {
			i_assert(zstream->istream.parent->v_offset >=
				 zstream->istream.parent_start_offset);
			if ((zstream->istream.parent->v_offset -
			     zstream->istream.parent_start_offset) > 4)
				zstream->hdr_read = TRUE;
		}
		zstream->remain = zret > 0;
		buffer_set_used_size(zstream->data_buffer, zstream->output.pos);
	}
	i_unreached();
}

static void i_stream_zstd_reset(struct zstd_istream *zstream)
{
	struct istream_private *stream = &zstream->istream;

	i_stream_seek(stream->parent, stream->parent_start_offset);
	stream->parent_expected_offset = stream->parent_start_offset;
	stream->skip = stream->pos = 0;
	stream->istream.v_offset = 0;
	stream->high_pos = 0;

	i_stream_zstd_deinit(zstream, TRUE);
	i_stream_zstd_init(zstream);
}

static void
i_stream_zstd_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
{
	struct zstd_istream *zstream =
		container_of(stream, struct zstd_istream, istream);

	if (i_stream_nonseekable_try_seek(stream, v_offset))
		return;

	/* have to seek backwards - reset state and retry */
	i_stream_zstd_reset(zstream);
	if (!i_stream_nonseekable_try_seek(stream, v_offset))
		i_unreached();

	if (mark)
		zstream->marked = TRUE;
}

static void i_stream_zstd_sync(struct istream_private *stream)
{
	struct zstd_istream *zstream =
		container_of(stream, struct zstd_istream, istream);
	const struct stat *st;

	if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
		if (memcmp(&zstream->last_parent_statbuf,
			   st, sizeof(*st)) == 0) {
			/* a compressed file doesn't change unexpectedly,
			   don't clear our caches unnecessarily */
			return;
		}
		zstream->last_parent_statbuf = *st;
	}
	i_stream_zstd_reset(zstream);
}

struct istream *
i_stream_create_zstd(struct istream *input)
{
	struct zstd_istream *zstream;

	zstd_version_check();

	zstream = i_new(struct zstd_istream, 1);

	i_stream_zstd_init(zstream);

	zstream->istream.iostream.close = i_stream_zstd_close;
	zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
	zstream->istream.read = i_stream_zstd_read;
	zstream->istream.seek = i_stream_zstd_seek;
	zstream->istream.sync = i_stream_zstd_sync;

	zstream->istream.istream.readable_fd = FALSE;
	zstream->istream.istream.blocking = input->blocking;
	zstream->istream.istream.seekable = input->seekable;

	return i_stream_create(&zstream->istream, input,
			       i_stream_get_fd(input), 0);
}

#endif
