/*  This file is part of "reprepro"
 *  Copyright (C) 2008 Bernhard R. Link
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02111-1301  USA
 */

#include <config.h>

#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <fcntl.h>
#include <archive.h>
#include <archive_entry.h>

#include "globals.h"
#include "error.h"
#include "mprintf.h"
#include "filecntl.h"
#include "uncompression.h"

const char * const uncompression_suffix[c_COUNT] = {
	"", ".gz", ".bz2", ".lzma", ".xz", ".lz", ".zst"};

/* how those are called in the config file */
const char * const uncompression_config[c_COUNT] = {
	".", ".gz", ".bz2", ".lzma", ".xz", ".lz", ".zst" };

/* All decompression is now synchronous via libarchive; no child processes. */
retvalue uncompress_checkpid(pid_t UNUSED(pid), int UNUSED(status)) {
	return RET_NOTHING;
}

bool uncompress_running(void) {
	return false;
}

/* ---------- struct compressedfile, backed by libarchive ---------- */

struct compressedfile {
	/* for error messages; NULL when opened from an fd */
	char *filename;
	enum compression compression;
	struct archive *a;
	/* I/O state shared with the libarchive read callback */
	struct {
		int fd;
		/* bytes remaining to read from fd; -1 means unlimited */
		off_t remaining;
		char buf[65536];
	} ctx;
	/* whether to close ctx.fd when this file is freed */
	bool closefd;
	bool had_eof;
	bool had_error;
	/* saved error details (archive_error_string becomes invalid after
	 * archive_read_free, so we copy them on the first error) */
	int saved_errno;
	char errbuf[256];
};

static ssize_t myread_cb(struct archive *a, void *data, const void **buf) {
	struct compressedfile *f = data;
	ssize_t n;
	size_t want = sizeof(f->ctx.buf);

	if (f->ctx.remaining == 0)
		return 0; /* signal EOF to libarchive */

	if (f->ctx.remaining > 0 && (off_t)want > f->ctx.remaining)
		want = (size_t)f->ctx.remaining;

	do {
		if (interrupted()) {
			archive_set_error(a, EINTR, "Interrupted");
			return ARCHIVE_FATAL;
		}
		n = read(f->ctx.fd, f->ctx.buf, want);
	} while (n < 0 && errno == EINTR);

	if (n < 0) {
		archive_set_error(a, errno, "read error: %s", strerror(errno));
		return ARCHIVE_FATAL;
	}
	if (f->ctx.remaining > 0)
		f->ctx.remaining -= n;
	*buf = f->ctx.buf;
	return n;
}

static void save_archive_error(struct compressedfile *f) {
	const char *msg;

	if (f->had_error)
		return; /* already saved */
	f->had_error = true;
	f->saved_errno = archive_errno(f->a);
	msg = archive_error_string(f->a);
	if (msg != NULL)
		snprintf(f->errbuf, sizeof(f->errbuf), "%s", msg);
	else
		snprintf(f->errbuf, sizeof(f->errbuf), "uncompression error");
}

static retvalue setup_archive(struct compressedfile *f,
		int *errno_p, const char **msg_p) {
	struct archive *a;
	struct archive_entry *entry;
	int ret;

	a = archive_read_new();
	if (a == NULL) {
		if (errno_p != NULL)
			*errno_p = ENOMEM;
		if (msg_p != NULL)
			*msg_p = "Out of memory allocating archive";
		return RET_ERROR_OOM;
	}
	f->a = a;

	switch (f->compression) {
		case c_none:
			ret = archive_read_support_filter_none(a);
			break;
		case c_gzip:
			ret = archive_read_support_filter_gzip(a);
			break;
		case c_bzip2:
			ret = archive_read_support_filter_bzip2(a);
			break;
		case c_lzma:
			ret = archive_read_support_filter_lzma(a);
			break;
		case c_xz:
			ret = archive_read_support_filter_xz(a);
			break;
		case c_lunzip:
			ret = archive_read_support_filter_lzip(a);
			break;
		case c_zstd:
			ret = archive_read_support_filter_zstd(a);
			break;
		default:
			archive_read_free(a);
			f->a = NULL;
			if (errno_p != NULL)
				*errno_p = EINVAL;
			if (msg_p != NULL)
				*msg_p = "Unknown compression format";
			return RET_ERROR;
	}
	if (ret != ARCHIVE_OK) {
		if (errno_p != NULL)
			*errno_p = archive_errno(a);
		if (msg_p != NULL)
			*msg_p = archive_error_string(a);
		archive_read_free(a);
		f->a = NULL;
		return RET_ERROR;
	}

	ret = archive_read_support_format_raw(a);
	if (ret != ARCHIVE_OK) {
		if (errno_p != NULL)
			*errno_p = archive_errno(a);
		if (msg_p != NULL)
			*msg_p = archive_error_string(a);
		archive_read_free(a);
		f->a = NULL;
		return RET_ERROR;
	}

	ret = archive_read_open2(a, f, NULL, myread_cb, NULL, NULL);
	if (ret != ARCHIVE_OK) {
		if (errno_p != NULL)
			*errno_p = archive_errno(a);
		if (msg_p != NULL)
			*msg_p = archive_error_string(a);
		archive_read_free(a);
		f->a = NULL;
		return RET_ERROR;
	}

	/* Advance to the single entry that format_raw presents */
	ret = archive_read_next_header(a, &entry);
	if (ret == ARCHIVE_EOF) {
		f->had_eof = true;
		return RET_OK;
	}
	if (ret != ARCHIVE_OK) {
		if (errno_p != NULL)
			*errno_p = archive_errno(a);
		if (msg_p != NULL)
			*msg_p = archive_error_string(a);
		archive_read_free(a);
		f->a = NULL;
		return RET_ERROR;
	}
	return RET_OK;
}

retvalue uncompress_open(/*@out@*/struct compressedfile **file_p,
		const char *filename, enum compression compression) {
	struct compressedfile *f;
	int fd, e;
	retvalue r;

	f = zNEW(struct compressedfile);
	if (FAILEDTOALLOC(f))
		return RET_ERROR_OOM;

	f->filename = strdup(filename);
	if (FAILEDTOALLOC(f->filename)) {
		free(f);
		return RET_ERROR_OOM;
	}
	f->compression = compression;
	f->ctx.remaining = -1; /* unlimited */
	f->closefd = true;

	fd = open(filename, O_RDONLY | O_NOCTTY);
	if (fd < 0) {
		e = errno;
		fprintf(stderr, "Error %d opening '%s': %s!\n",
				e, filename, strerror(e));
		free(f->filename);
		free(f);
		return RET_ERRNO(e);
	}
	f->ctx.fd = fd;

	r = setup_archive(f, &e, NULL);
	if (RET_WAS_ERROR(r)) {
		(void)close(fd);
		if (e > 0)
			fprintf(stderr,
"Error %d starting unpacking '%s': %s\n",
				e, filename, strerror(e));
		else
			fprintf(stderr,
"Error starting unpacking '%s'\n", filename);
		free(f->filename);
		free(f);
		return r;
	}
	*file_p = f;
	return RET_OK;
}

retvalue uncompress_fdopen(struct compressedfile **file_p,
		int fd, off_t len, enum compression compression,
		int *errno_p, const char **msg_p) {
	struct compressedfile *f;
	retvalue r;

	f = zNEW(struct compressedfile);
	if (FAILEDTOALLOC(f)) {
		if (errno_p != NULL)
			*errno_p = ENOMEM;
		if (msg_p != NULL)
			*msg_p = "Out of memory";
		return RET_ERROR_OOM;
	}
	f->filename = NULL;
	f->compression = compression;
	f->ctx.fd = fd;
	f->ctx.remaining = len; /* may be -1 for unlimited */
	f->closefd = false;

	r = setup_archive(f, errno_p, msg_p);
	if (RET_WAS_ERROR(r)) {
		free(f);
		return r;
	}
	*file_p = f;
	return RET_OK;
}

int uncompress_read(struct compressedfile *file, void *buffer, int size) {
	ssize_t n;

	if (file->had_error)
		return -1;
	if (file->had_eof)
		return 0;

	n = archive_read_data(file->a, buffer, (size_t)size);
	if (n == 0) {
		file->had_eof = true;
		return 0;
	}
	if (n < 0) {
		save_archive_error(file);
		return -1;
	}
	return (int)n;
}

retvalue uncompress_error(struct compressedfile *file) {
	if (file == NULL)
		return RET_NOTHING;

	if (!file->had_error)
		return RET_OK;

	/* errbuf was filled when had_error was first set */
	if (file->filename != NULL)
		fprintf(stderr,
"Error reading from '%s': %s\n", file->filename, file->errbuf);
	else
		fprintf(stderr,
"Error reading compressed data: %s\n", file->errbuf);
	return RET_ERROR;
}

void uncompress_abort(struct compressedfile *file) {
	if (file == NULL)
		return;

	archive_read_free(file->a);
	if (file->closefd && file->ctx.fd >= 0)
		(void)close(file->ctx.fd);
	free(file->filename);
	free(file);
}

retvalue uncompress_fdclose(struct compressedfile *file,
		int *errno_p, const char **msg_p) {
	retvalue r;

	if (file == NULL)
		return RET_OK;

	assert(file->closefd == false);

	if (file->had_error) {
		if (errno_p != NULL)
			*errno_p = file->saved_errno;
		if (msg_p != NULL)
			*msg_p = file->errbuf;
		r = RET_ERROR;
	} else {
		r = RET_OK;
	}

	archive_read_free(file->a);
	free(file);
	return r;
}

retvalue uncompress_close(struct compressedfile *file) {
	retvalue r;

	if (file == NULL)
		return RET_OK;

	if (file->had_error) {
		r = RET_ERROR;
	} else {
		r = RET_OK;
	}

	archive_read_free(file->a);

	if (RET_IS_OK(r) && file->closefd && file->ctx.fd >= 0
			&& close(file->ctx.fd) != 0) {
		int e = errno;
		fprintf(stderr,
"Error %d reading from %s: %s!\n",
			e, file->filename, strerror(e));
		free(file->filename);
		free(file);
		return RET_ERRNO(e);
	}
	if (!RET_IS_OK(r)) {
		if (file->closefd && file->ctx.fd >= 0)
			(void)close(file->ctx.fd);
		if (file->filename != NULL) {
			if (file->saved_errno > 0)
				fprintf(stderr,
"Error %d reading from %s: %s!\n",
					file->saved_errno,
					file->filename, file->errbuf);
			else
				fprintf(stderr,
"Error reading from %s: %s!\n",
					file->filename, file->errbuf);
		}
	}
	free(file->filename);
	free(file);
	return r;
}

/* ---------- file-to-file decompression ---------- */

static retvalue builtin_uncompress(const char *compressed,
		const char *destination, enum compression compression) {
	struct compressedfile *f;
	char buffer[4096];
	int bytes_read, bytes_written, written;
	int destfd;
	int e;
	retvalue r;

	r = uncompress_open(&f, compressed, compression);
	if (!RET_IS_OK(r))
		return r;

	destfd = open(destination,
			O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666);
	if (destfd < 0) {
		e = errno;
		fprintf(stderr, "Error %d creating '%s': %s\n",
				e, destination, strerror(e));
		uncompress_abort(f);
		return RET_ERRNO(e);
	}
	do {
		bytes_read = uncompress_read(f, buffer, sizeof(buffer));
		if (bytes_read <= 0)
			break;

		bytes_written = 0;
		while (bytes_written < bytes_read) {
			written = write(destfd,
					buffer + bytes_written,
					bytes_read - bytes_written);
			if (written < 0) {
				e = errno;
				fprintf(stderr,
"Error %d writing to '%s': %s\n",
					e, destination, strerror(e));
				(void)close(destfd);
				uncompress_abort(f);
				return RET_ERRNO(e);
			}
			bytes_written += written;
		}
	} while (true);

	r = uncompress_close(f);
	if (RET_WAS_ERROR(r)) {
		(void)close(destfd);
		return r;
	}
	if (close(destfd) != 0) {
		e = errno;
		fprintf(stderr, "Error %d writing to '%s': %s!\n",
				e, destination, strerror(e));
		return RET_ERROR;
	}
	return RET_OK;
}

retvalue uncompress_queue_file(const char *compressed,
		const char *destination, enum compression compression,
		finishaction *action, void *privdata) {
	retvalue r;
	bool failed;

	(void)unlink(destination);

	if (verbose > 1)
		fprintf(stderr, "Uncompress '%s' into '%s'...\n",
				compressed, destination);

	r = builtin_uncompress(compressed, destination, compression);
	failed = RET_WAS_ERROR(r);
	if (failed)
		(void)unlink(destination);

	/* invoke the callback as the caller expects it */
	return action(privdata, compressed, failed);
}

retvalue uncompress_file(const char *compressed,
		const char *destination, enum compression compression) {
	retvalue r;

	(void)unlink(destination);

	if (verbose > 1)
		fprintf(stderr, "Uncompress '%s' into '%s'...\n",
				compressed, destination);

	r = builtin_uncompress(compressed, destination, compression);
	if (RET_WAS_ERROR(r)) {
		(void)unlink(destination);
		return r;
	}
	return RET_OK;
}

enum compression compression_by_suffix(const char *name, size_t *len_p) {
	enum compression c;
	size_t len = *len_p;

	for (c = c_COUNT - 1 ; c > c_none ; c--) {
		size_t l = strlen(uncompression_suffix[c]);

		if (len <= l)
			continue;
		if (strncmp(name + len - l, uncompression_suffix[c], l) == 0) {
			*len_p -= l;
			return c;
		}
	}
	return c_none;
}
