/* zip.cc -- IO on .zip files using zlib
   Version 0.15 beta, Mar 19th, 1998,

   Modified by Ryan Nunn. Nov 9th 2001
   Modified by the Exult Team. 2003-2022
   Read zip.h for more info
*/

/* Added by Ryan Nunn */
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "ignore_unused_variable_warning.h"

#ifdef HAVE_ZIP_SUPPORT

#	include <cstddef>
#	include <cstdio>
#	include <cstdlib>
#	include <cstring>
#	ifdef NO_ERRNO_H
extern int errno;
#	else
#		include <cerrno>
#	endif
using namespace std;

#	include "zip.h"

/* Added by Ryan Nunn to overcome DEF_MEM_LEVEL being undeclared */
#	if MAX_MEM_LEVEL >= 8
#		define DEF_MEM_LEVEL 8
#	else
#		define DEF_MEM_LEVEL MAX_MEM_LEVEL
#	endif

#	ifndef VERSIONMADEBY
#		define VERSIONMADEBY (0x0) /* platform depedent */
#	endif

#	ifndef Z_BUFSIZE
#		define Z_BUFSIZE (16384)
#	endif

#	ifndef Z_MAXFILENAMEINZIP
#		define Z_MAXFILENAMEINZIP (256)
#	endif

/*
#define SIZECENTRALDIRITEM (0x2e)
#define SIZEZIPLOCALHEADER (0x1e)
*/

/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */

#	ifndef SEEK_CUR
#		define SEEK_CUR 1
#	endif

#	ifndef SEEK_END
#		define SEEK_END 2
#	endif

#	ifndef SEEK_SET
#		define SEEK_SET 0
#	endif

// const char zip_copyright[] = " zip 0.15 Copyright 1998 Gilles Vollant ";

#	define SIZEDATA_INDATABLOCK (4096 - (4 * 4))

#	define LOCALHEADERMAGIC   (0x04034b50)
#	define CENTRALHEADERMAGIC (0x02014b50)
#	define ENDHEADERMAGIC     (0x06054b50)

#	define FLAG_LOCALHEADER_OFFSET (0x06)
#	define CRC_LOCALHEADER_OFFSET  (0x0e)

#	define SIZECENTRALHEADER (0x2e) /* 46 */

#	ifdef __GNUC__
#		pragma GCC diagnostic push
#		pragma GCC diagnostic ignored "-Wold-style-cast"
#	endif    // __GNUC__
static int U7deflateInit2(z_stream* stream, int level) {
	return deflateInit2(
			stream, level, Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, 0);
}
#	ifdef __GNUC__
#		pragma GCC diagnostic pop
#	endif    // __GNUC__

struct linkedlist_datablock_internal {
	linkedlist_datablock_internal* next_datablock;
	uLong                          avail_in_this_block;
	uLong                          filled_in_this_block;
	uLong                          unused; /* for future use and alignement */
	unsigned char                  data[SIZEDATA_INDATABLOCK];
};

struct linkedlist_data {
	linkedlist_datablock_internal* first_block;
	linkedlist_datablock_internal* last_block;
};

struct curfile_info {
	z_stream stream;               /* zLib stream structure for inflate */
	int      stream_initialised;   /* 1 is stream is initialised */
	uInt     pos_in_buffered_data; /* last written byte in buffered_data */

	uLong pos_local_header;   /* offset of the local header of the file
								   currenty writing */
	char* central_header;     /* central header data for the current file */
	uLong size_centralheader; /* size of the central header for cur file */
	uLong flag;               /* flag of the file currently writing */

	int  method;                   /* compression method of file currenty wr.*/
	Byte buffered_data[Z_BUFSIZE]; /* buffer contain compressed data to be
									  writ*/
	uLong dosDate;
	uLong crc32;
};

struct zip_internal {
	FILE*           filezip;
	linkedlist_data central_dir; /* datablock with central dir in construction*/
	int in_opened_file_inzip;    /* 1 if a file in the zip is currently writ.*/
	curfile_info ci;             /* info on the file curretly writing */

	uLong begin_pos; /* position of the beginning of the zipfile */
	uLong number_entry;
};

static linkedlist_datablock_internal* allocate_new_datablock() {
	linkedlist_datablock_internal* ldi;
	ldi = new linkedlist_datablock_internal();
	if (ldi != nullptr) {
		ldi->next_datablock       = nullptr;
		ldi->filled_in_this_block = 0;
		ldi->avail_in_this_block  = SIZEDATA_INDATABLOCK;
	}
	return ldi;
}

static void free_datablock(linkedlist_datablock_internal* ldi) {
	while (ldi != nullptr) {
		linkedlist_datablock_internal* ldinext = ldi->next_datablock;
		delete ldi;
		ldi = ldinext;
	}
}

static void init_linkedlist(linkedlist_data* ll) {
	ll->first_block = ll->last_block = nullptr;
}

static int add_data_in_datablock(
		linkedlist_data* ll, const void* buf, uLong len) {
	linkedlist_datablock_internal* ldi;
	const unsigned char*           from_copy;

	if (ll == nullptr) {
		return ZIP_INTERNALERROR;
	}

	if (ll->last_block == nullptr) {
		ll->first_block = ll->last_block = allocate_new_datablock();
		if (ll->first_block == nullptr) {
			return ZIP_INTERNALERROR;
		}
	}

	ldi       = ll->last_block;
	from_copy = static_cast<const unsigned char*>(buf);

	while (len > 0) {
		uInt           copy_this;
		uInt           i;
		unsigned char* to_copy;

		if (ldi->avail_in_this_block == 0) {
			ldi->next_datablock = allocate_new_datablock();
			if (ldi->next_datablock == nullptr) {
				return ZIP_INTERNALERROR;
			}
			ldi            = ldi->next_datablock;
			ll->last_block = ldi;
		}

		if (ldi->avail_in_this_block < len) {
			copy_this = ldi->avail_in_this_block;
		} else {
			copy_this = len;
		}

		to_copy = &(ldi->data[ldi->filled_in_this_block]);

		for (i = 0; i < copy_this; i++) {
			*(to_copy + i) = *(from_copy + i);
		}

		ldi->filled_in_this_block += copy_this;
		ldi->avail_in_this_block -= copy_this;
		from_copy += copy_this;
		len -= copy_this;
	}
	return ZIP_OK;
}

/****************************************************************************/

/* ===========================================================================
   Outputs a long in LSB order to the given file
   nbByte == 1, 2 or 4 (byte, short or long)
*/

static int ziplocal_putValue(FILE* file, uLong x, int nbByte);

static int ziplocal_putValue(FILE* file, uLong x, int nbByte) {
	unsigned char buf[4];
	int           n;
	for (n = 0; n < nbByte; n++) {
		buf[n] = static_cast<unsigned char>(x & 0xff);
		x >>= 8;
	}
	if (fwrite(buf, nbByte, 1, file) != 1) {
		return ZIP_ERRNO;
	} else {
		return ZIP_OK;
	}
}

static void ziplocal_putValue_inmemory(void* dest, uLong x, int nbByte);

static void ziplocal_putValue_inmemory(void* dest, uLong x, int nbByte) {
	auto* buf = static_cast<unsigned char*>(dest);
	int   n;
	for (n = 0; n < nbByte; n++) {
		buf[n] = static_cast<unsigned char>(x & 0xff);
		x >>= 8;
	}
}

/****************************************************************************/

static uLong ziplocal_TmzDateToDosDate(const tm_zip* ptm, uLong uLongdosDate) {
	ignore_unused_variable_warning(uLongdosDate);
	uLong year = ptm->tm_year;
	if (year > 1980) {
		year -= 1980;
	} else if (year > 80) {
		year -= 80;
	}
	return (((ptm->tm_mday) + (32 * (ptm->tm_mon + 1)) + (512 * year)) << 16)
		   | ((ptm->tm_sec / 2) + (32 * ptm->tm_min) + (2048 * ptm->tm_hour));
}

/****************************************************************************/

extern zipFile ZEXPORT zipOpen(const char* pathname, int append) {
	zip_internal  ziinit;
	zip_internal* zi;

	ziinit.filezip = nullptr;

	/* Start changes by Ryan Nunn to fix append mode bug */

	/* If append, use r+b mode, will fail if not exist */
	if (append != 0) {
		ziinit.filezip = fopen(pathname, "r+b");
	}

	/* If not append, or failed, use wb */
	if (ziinit.filezip == nullptr) {
		ziinit.filezip = fopen(pathname, "wb");
	}

	/* Still doesn't exist, means can't create */
	if (ziinit.filezip == nullptr) {
		return nullptr;
	}

	/* Make sure we are at the end of the file */
	fseek(ziinit.filezip, 0, SEEK_END);

	/* End changes by Ryan Nunn to fix append mode bug */

	ziinit.begin_pos             = ftell(ziinit.filezip);
	ziinit.in_opened_file_inzip  = 0;
	ziinit.ci.stream_initialised = 0;
	ziinit.number_entry          = 0;
	init_linkedlist(&(ziinit.central_dir));

	zi = new zip_internal(ziinit);
	return zi;
}

extern int ZEXPORT zipOpenNewFileInZip(
		zipFile file, const char* filename, const zip_fileinfo* zipfi,
		const void* extrafield_local, uInt size_extrafield_local,
		const void* extrafield_global, uInt size_extrafield_global,
		const char* comment, int method, int level) {
	uInt size_filename;
	uInt size_comment;
	uInt i;
	int  err = ZIP_OK;

	if (file == nullptr) {
		return ZIP_PARAMERROR;
	}
	if ((method != 0) && (method != Z_DEFLATED)) {
		return ZIP_PARAMERROR;
	}

	if (file->in_opened_file_inzip == 1) {
		err = zipCloseFileInZip(file);
		if (err != ZIP_OK) {
			return err;
		}
	}

	if (filename == nullptr) {
		filename = "-";
	}

	if (comment == nullptr) {
		size_comment = 0;
	} else {
		size_comment = strlen(comment);
	}

	size_filename = strlen(filename);

	if (zipfi == nullptr) {
		file->ci.dosDate = 0;
	} else {
		if (zipfi->dosDate != 0) {
			file->ci.dosDate = zipfi->dosDate;
		} else {
			file->ci.dosDate = ziplocal_TmzDateToDosDate(
					&zipfi->tmz_date, zipfi->dosDate);
		}
	}

	file->ci.flag = 0;
	if ((level == 8) || (level == 9)) {
		file->ci.flag |= 2;
	}
	if (level == 2) {
		file->ci.flag |= 4;
	}
	if (level == 1) {
		file->ci.flag |= 6;
	}

	file->ci.crc32                = 0;
	file->ci.method               = method;
	file->ci.stream_initialised   = 0;
	file->ci.pos_in_buffered_data = 0;
	file->ci.pos_local_header     = ftell(file->filezip);
	file->ci.size_centralheader   = SIZECENTRALHEADER + size_filename
								  + size_extrafield_global + size_comment;
	file->ci.central_header = new char[file->ci.size_centralheader];

	ziplocal_putValue_inmemory(file->ci.central_header, CENTRALHEADERMAGIC, 4);
	/* version info */
	ziplocal_putValue_inmemory(file->ci.central_header + 4, VERSIONMADEBY, 2);
	ziplocal_putValue_inmemory(file->ci.central_header + 6, 20, 2);
	ziplocal_putValue_inmemory(file->ci.central_header + 8, file->ci.flag, 2);
	ziplocal_putValue_inmemory(
			file->ci.central_header + 10, file->ci.method, 2);
	ziplocal_putValue_inmemory(
			file->ci.central_header + 12, file->ci.dosDate, 4);
	ziplocal_putValue_inmemory(file->ci.central_header + 16, 0, 4); /*crc*/
	ziplocal_putValue_inmemory(
			file->ci.central_header + 20, 0, 4); /*compr size*/
	ziplocal_putValue_inmemory(
			file->ci.central_header + 24, 0, 4); /*uncompr size*/
	ziplocal_putValue_inmemory(file->ci.central_header + 28, size_filename, 2);
	ziplocal_putValue_inmemory(
			file->ci.central_header + 30, size_extrafield_global, 2);
	ziplocal_putValue_inmemory(file->ci.central_header + 32, size_comment, 2);
	ziplocal_putValue_inmemory(
			file->ci.central_header + 34, 0, 2); /*disk nm start*/

	if (zipfi == nullptr) {
		ziplocal_putValue_inmemory(file->ci.central_header + 36, 0, 2);
	} else {
		ziplocal_putValue_inmemory(
				file->ci.central_header + 36, zipfi->internal_fa, 2);
	}

	if (zipfi == nullptr) {
		ziplocal_putValue_inmemory(file->ci.central_header + 38, 0, 4);
	} else {
		ziplocal_putValue_inmemory(
				file->ci.central_header + 38, zipfi->external_fa, 4);
	}

	ziplocal_putValue_inmemory(
			file->ci.central_header + 42, file->ci.pos_local_header, 4);

	for (i = 0; i < size_filename; i++) {
		*(file->ci.central_header + SIZECENTRALHEADER + i) = *(filename + i);
	}

	for (i = 0; i < size_extrafield_global; i++) {
		*(file->ci.central_header + SIZECENTRALHEADER + size_filename + i)
				= *(static_cast<const char*>(extrafield_global) + i);
	}

	for (i = 0; i < size_comment; i++) {
		*(file->ci.central_header + SIZECENTRALHEADER + size_filename
		  + size_extrafield_global + i)
				= *(filename + i);
	}
	if (file->ci.central_header == nullptr) {
		return ZIP_INTERNALERROR;
	}

	/* write the local header */
	err = ziplocal_putValue(file->filezip, LOCALHEADERMAGIC, 4);

	if (err == ZIP_OK) {
		err = ziplocal_putValue(
				file->filezip, 20, 2); /* version needed to extract */
	}
	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, file->ci.flag, 2);
	}

	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, file->ci.method, 2);
	}

	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, file->ci.dosDate, 4);
	}

	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, 0, 4); /* crc 32, unknown */
	}
	if (err == ZIP_OK) {
		err = ziplocal_putValue(
				file->filezip, 0, 4); /* compressed size, unknown */
	}
	if (err == ZIP_OK) {
		err = ziplocal_putValue(
				file->filezip, 0, 4); /* uncompressed size, unknown */
	}

	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, size_filename, 2);
	}

	if (err == ZIP_OK) {
		err = ziplocal_putValue(file->filezip, size_extrafield_local, 2);
	}

	if ((err == ZIP_OK) && (size_filename > 0)) {
		if (fwrite(filename, size_filename, 1, file->filezip) != 1) {
			err = ZIP_ERRNO;
		}
	}

	if ((err == ZIP_OK) && (size_extrafield_local > 0)) {
		if (fwrite(extrafield_local, size_extrafield_local, 1, file->filezip)
			!= 1) {
			err = ZIP_ERRNO;
		}
	}

	file->ci.stream.avail_in  = 0;
	file->ci.stream.avail_out = Z_BUFSIZE;
	file->ci.stream.next_out  = file->ci.buffered_data;
	file->ci.stream.total_in  = 0;
	file->ci.stream.total_out = 0;

	if ((err == ZIP_OK) && (file->ci.method == Z_DEFLATED)) {
		file->ci.stream.zalloc = nullptr;
		file->ci.stream.zfree  = nullptr;
		file->ci.stream.opaque = nullptr;

		err = U7deflateInit2(&file->ci.stream, level);

		if (err == Z_OK) {
			file->ci.stream_initialised = 1;
		}
	}

	if (err == Z_OK) {
		file->in_opened_file_inzip = 1;
	}
	return err;
}

extern int ZEXPORT zipWriteInFileInZip(zipFile file, voidpc buf, unsigned len) {
	int err = ZIP_OK;

	if (file == nullptr) {
		return ZIP_PARAMERROR;
	}

	if (file->in_opened_file_inzip == 0) {
		return ZIP_PARAMERROR;
	}

	file->ci.stream.next_in  = static_cast<const Bytef*>(buf);
	file->ci.stream.avail_in = len;
	file->ci.crc32 = crc32(file->ci.crc32, static_cast<const Bytef*>(buf), len);

	while ((err == ZIP_OK) && (file->ci.stream.avail_in > 0)) {
		if (file->ci.stream.avail_out == 0) {
			if (fwrite(file->ci.buffered_data, file->ci.pos_in_buffered_data, 1,
					   file->filezip)
				!= 1) {
				err = ZIP_ERRNO;
			}
			file->ci.pos_in_buffered_data = 0;
			file->ci.stream.avail_out     = Z_BUFSIZE;
			file->ci.stream.next_out      = file->ci.buffered_data;
		}

		if (file->ci.method == Z_DEFLATED) {
			const uLong uTotalOutBefore = file->ci.stream.total_out;
			err                         = deflate(&file->ci.stream, Z_NO_FLUSH);
			file->ci.pos_in_buffered_data
					+= (file->ci.stream.total_out - uTotalOutBefore);

		} else {
			uInt copy_this;
			if (file->ci.stream.avail_in < file->ci.stream.avail_out) {
				copy_this = file->ci.stream.avail_in;
			} else {
				copy_this = file->ci.stream.avail_out;
			}
			for (uInt i = 0; i < copy_this; i++) {
				file->ci.stream.next_out[i] = file->ci.stream.next_in[i];
			}
			file->ci.stream.avail_in -= copy_this;
			file->ci.stream.avail_out -= copy_this;
			file->ci.stream.next_in += copy_this;
			file->ci.stream.next_out += copy_this;
			file->ci.stream.total_in += copy_this;
			file->ci.stream.total_out += copy_this;
			file->ci.pos_in_buffered_data += copy_this;
		}
	}

	return 0;
}

extern int ZEXPORT zipCloseFileInZip(zipFile file) {
	int err = ZIP_OK;

	if (file == nullptr) {
		return ZIP_PARAMERROR;
	}

	if (file->in_opened_file_inzip == 0) {
		return ZIP_PARAMERROR;
	}
	file->ci.stream.avail_in = 0;

	if (file->ci.method == Z_DEFLATED) {
		while (err == ZIP_OK) {
			uLong uTotalOutBefore;
			if (file->ci.stream.avail_out == 0) {
				if (fwrite(file->ci.buffered_data,
						   file->ci.pos_in_buffered_data, 1, file->filezip)
					!= 1) {
					err = ZIP_ERRNO;
				}
				file->ci.pos_in_buffered_data = 0;
				file->ci.stream.avail_out     = Z_BUFSIZE;
				file->ci.stream.next_out      = file->ci.buffered_data;
			}
			uTotalOutBefore = file->ci.stream.total_out;
			err             = deflate(&file->ci.stream, Z_FINISH);
			file->ci.pos_in_buffered_data
					+= (file->ci.stream.total_out - uTotalOutBefore);
		}
	}

	if (err == Z_STREAM_END) {
		err = ZIP_OK; /* this is normal */
	}

	if ((file->ci.pos_in_buffered_data > 0) && (err == ZIP_OK)) {
		if (fwrite(file->ci.buffered_data, file->ci.pos_in_buffered_data, 1,
				   file->filezip)
			!= 1) {
			err = ZIP_ERRNO;
		}
	}

	if ((file->ci.method == Z_DEFLATED) && (err == ZIP_OK)) {
		err                         = deflateEnd(&file->ci.stream);
		file->ci.stream_initialised = 0;
	}

	ziplocal_putValue_inmemory(
			file->ci.central_header + 16, file->ci.crc32, 4); /*crc*/
	ziplocal_putValue_inmemory(
			file->ci.central_header + 20, file->ci.stream.total_out,
			4); /*compr size*/
	ziplocal_putValue_inmemory(
			file->ci.central_header + 24, file->ci.stream.total_in,
			4); /*uncompr size*/

	if (err == ZIP_OK) {
		err = add_data_in_datablock(
				&file->central_dir, file->ci.central_header,
				file->ci.size_centralheader);
	}
	delete[] file->ci.central_header;

	if (err == ZIP_OK) {
		const long cur_pos_inzip = ftell(file->filezip);
		if (fseek(file->filezip, file->ci.pos_local_header + 14, SEEK_SET)
			!= 0) {
			err = ZIP_ERRNO;
		}

		if (err == ZIP_OK) {
			err = ziplocal_putValue(
					file->filezip, file->ci.crc32, 4); /* crc 32, unknown */
		}

		if (err == ZIP_OK) { /* compressed size, unknown */
			err = ziplocal_putValue(
					file->filezip, file->ci.stream.total_out, 4);
		}

		if (err == ZIP_OK) { /* uncompressed size, unknown */
			err = ziplocal_putValue(file->filezip, file->ci.stream.total_in, 4);
		}

		if (fseek(file->filezip, cur_pos_inzip, SEEK_SET) != 0) {
			err = ZIP_ERRNO;
		}
	}

	file->number_entry++;
	file->in_opened_file_inzip = 0;

	return err;
}

extern int ZEXPORT zipClose(zipFile file, const char* global_comment) {
	int   err             = 0;
	uLong size_centraldir = 0;
	uLong centraldir_pos_inzip;
	uInt  size_global_comment;
	if (file == nullptr) {
		return ZIP_PARAMERROR;
	}

	if (file->in_opened_file_inzip == 1) {
		err = zipCloseFileInZip(file);
	}

	if (global_comment == nullptr) {
		size_global_comment = 0;
	} else {
		size_global_comment = strlen(global_comment);
	}

	centraldir_pos_inzip = ftell(file->filezip);
	if (err == ZIP_OK) {
		linkedlist_datablock_internal* ldi = file->central_dir.first_block;
		while (ldi != nullptr) {
			if ((err == ZIP_OK) && (ldi->filled_in_this_block > 0)) {
				if (fwrite(ldi->data, ldi->filled_in_this_block, 1,
						   file->filezip)
					!= 1) {
					err = ZIP_ERRNO;
				}
			}

			size_centraldir += ldi->filled_in_this_block;
			ldi = ldi->next_datablock;
		}
	}
	free_datablock(file->central_dir.first_block);

	if (err == ZIP_OK) { /* Magic End */
		err = ziplocal_putValue(file->filezip, ENDHEADERMAGIC, 4);
	}

	if (err == ZIP_OK) { /* number of this disk */
		err = ziplocal_putValue(file->filezip, 0, 2);
	}

	if (err == ZIP_OK) { /* number of the disk with the start of the central
							directory */
		err = ziplocal_putValue(file->filezip, 0, 2);
	}

	if (err == ZIP_OK) { /* total number of entries in the central dir on this
							disk */
		err = ziplocal_putValue(file->filezip, file->number_entry, 2);
	}

	if (err == ZIP_OK) { /* total number of entries in the central dir */
		err = ziplocal_putValue(file->filezip, file->number_entry, 2);
	}

	if (err == ZIP_OK) { /* size of the central directory */
		err = ziplocal_putValue(file->filezip, size_centraldir, 4);
	}

	if (err == ZIP_OK) { /* offset of start of central directory with respect to
							the starting disk number */
		err = ziplocal_putValue(file->filezip, centraldir_pos_inzip, 4);
	}

	if (err == ZIP_OK) { /* zipfile comment length */
		err = ziplocal_putValue(file->filezip, size_global_comment, 2);
	}

	if ((err == ZIP_OK) && (size_global_comment > 0)) {
		if (fwrite(global_comment, size_global_comment, 1, file->filezip)
			!= 1) {
			err = ZIP_ERRNO;
		}
	}
	fclose(file->filezip);
	delete file;

	return err;
}

/* Added by Ryan Nunn */
#endif /*HAVE_ZIP_SUPPORT*/
