/* main.c – Tiny demo utility for otezip/zip.h
 * Build:  gcc -std=c99 -DOTEZIP_IMPLEMENTATION main.c -lz -o otezip
 * Usage:  ./mzip -l  archive.zip   # list files
 *         ./mzip -x  archive.zip   # extract into current directory
 *         ./mzip -c  archive.zip file1 file2...  # create new zip archive
 *         ./mzip -a  archive.zip file1 file2...  # add files to existing archive
 *         ./mzip -d  file.gz       # decompress gzip file (gunzip mode)
 *         ./mzip -g  file          # compress file with gzip
 */

#define _POSIX_C_SOURCE 200809L
#if defined(__APPLE__)
/* _POSIX_C_SOURCE alone hides O_NOFOLLOW in macOS <fcntl.h> */
#define _DARWIN_C_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "include/otezip/config.h"
#include "include/otezip/zip.h"
#include "include/otezip/zlib.h" /* deflate/inflate from otezip.c */

#include <limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>

/* O_NOFOLLOW guards overwrites against symlink swaps; only Windows lacks it */
#ifndef O_NOFOLLOW
#if defined(_WIN32) || defined(_WIN64)
#define O_NOFOLLOW 0
#else
#error "O_NOFOLLOW is required to extract safely"
#endif
#endif

/* O_NONBLOCK keeps a forced overwrite from blocking on a FIFO; Windows has none */
#ifndef O_NONBLOCK
#define O_NONBLOCK 0
#endif

/* Ensure O_BINARY is available for binary-safe extraction on Windows. */
#ifndef O_BINARY
#ifdef _O_BINARY
#define O_BINARY _O_BINARY
#else
#define O_BINARY 0
#endif
#endif

#ifdef _WIN32
#include <direct.h>
#include <io.h>
#else
#include <unistd.h>
#endif

#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

/* Force overwrite flag (set via -f / --force) */
static int g_force = 0;

/* Platform compatibility wrappers
 * - mingw/msvc provide mkdir (const char*)/_mkdir and no lstat/S_ISLNK by default.
 * - Provide small wrappers/macros so the rest of the code can use portable names. */
#if defined(_WIN32) || defined(_WIN64)
#define OTEZIP_MKDIR(path, mode) _mkdir (path)
#define OTEZIP_LSTAT(path, buf) stat((path),(buf))
#ifndef OTEZIP_FCHMOD
/* On Windows (MinGW/MSVC) there's no reliable fchmod that maps to POSIX
 * permissions. Setting unix-style permission bits isn't meaningful on NTFS in
 * the same way; failures are non-fatal in extraction code, so make this a
 * no-op that reports success. If a platform provides an fd-based fchmod,
 * callers can override OTEZIP_FCHMOD. */
#define OTEZIP_FCHMOD(fd, mode) (0)
#endif
#ifndef S_ISLNK
#define S_ISLNK(mode) 0
#endif
#ifndef S_ISDIR
#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR)
#endif
#ifndef S_ISREG
#define S_ISREG(mode) (((mode) & _S_IFMT) == _S_IFREG)
#endif
/* MSVC doesn't define mode_t or ssize_t; map POSIX names to CRT equivalents */
#ifdef _MSC_VER
#pragma warning(disable:4996)
typedef unsigned short mode_t;
typedef intptr_t ssize_t;
#define open _open
#define close _close
#define write _write
#endif
#else
#define OTEZIP_MKDIR(path, mode) mkdir((path),(mode))
#define OTEZIP_LSTAT(path, buf) lstat((path),(buf))
#define OTEZIP_FCHMOD(fd, mode) fchmod((fd),(mode))
#endif

/* Clamp size_t to uInt for zlib avail_in/avail_out */
#define UINT_CLAMP(x) ((uInt)(((x) > (size_t)UINT_MAX) ? UINT_MAX : (x)))

static void usage(void) {
	puts ("mzip – minimal ZIP reader/writer (mzip.h demo)\n"
	"Usage: mzip [-l | -x | -c | -a | -v | -d | -g] <archive.zip> [files...] [options]\n"
	"  -l   List contents\n"
	"  -x   Extract all files into current directory\n"
	"  -c   Create new archive with specified files\n"
	"  -a   Add files to existing archive\n"
	"  -d   Decompress gzip/deflate file (gunzip mode)\n"
	"  -g   Compress file with gzip (gzip mode)\n"
	"  -v   Show version number\n\n"
	"Options:");

	/* Show compression options based on what's enabled in config */
	puts ("  -z <method>, -z<method>  Use compression method (default: deflate if available, else store)");
#ifdef OTEZIP_ENABLE_STORE
	puts ("      store     Store files without compression");
#endif
#ifdef OTEZIP_ENABLE_DEFLATE
	puts ("      deflate   Use deflate compression");
#endif
#ifdef OTEZIP_ENABLE_ZSTD
	puts ("      zstd      Use zstd compression");
#endif
#ifdef OTEZIP_ENABLE_LZMA
	puts ("      lzma      Use LZMA compression");
#endif
#ifdef OTEZIP_ENABLE_LZ4
	puts ("      lz4       Use LZ4 compression");
#endif
#ifdef OTEZIP_ENABLE_BROTLI
	puts ("      brotli    Use Brotli compression");
#endif
#ifdef OTEZIP_ENABLE_LZFSE
	puts ("      lzfse     Use LZFSE compression");
#endif
	puts ("  -P<policy>, --policy=<policy>  Extraction policy for suspicious entries\n"
	"      reject (default)  - reject entries with absolute paths, empty names, '..' that escape, or symlink parents\n"
	"      strip             - remove leading '..' components that would escape (e.g., '../../a' -> 'a')\n"
	"      allow             - allow unsafe extraction (use with caution)\n");
	puts ("  --verify-crc    Verify CRC32 when extracting and fail on mismatch\n");
	puts ("  --ignore-zipbomb  Ignore zipbomb expansion checks and allow large claimed uncompressed sizes (dangerous)\n");
	puts ("  -f, --force     Overwrite existing files when extracting\n"
	"  --              Treat all following arguments as file names\n");
}

static int list_files(const char *path) {
	int err = 0;
	zip_t *za = zip_open (path, ZIP_RDONLY, &err);
	if (!za) {
		fprintf (stderr, "Failed to open %s (err=%d)\n", path, err);
		return 1;
	}

	zip_uint64_t n = zip_get_num_files (za);
	for (zip_uint64_t i = 0; i < n; ++i) {
		const char *name = NULL; /* we only have names in directory entries */
		/* mzip stores names inside entries array – expose via zip_name_locate */
		name = ((struct otezip_entry *)za->entries)[i].name; /* hack – internal */
		printf ("%3llu  %s\n", (unsigned long long)i, name? name: "<unknown>");
	}

	zip_close (za);
	return 0;
}

/* Function to create a new ZIP archive or add files to existing one */
static int create_or_add_files(const char *path, char **files, int num_files, int create_mode, int compression_method) {
	int err = 0;
	int flags = create_mode? (ZIP_CREATE | ZIP_TRUNCATE): (ZIP_CREATE);

	zip_t *za = zip_open (path, flags, &err);
	if (!za) {
		fprintf (stderr, "Failed to %s %s (err=%d)\n", create_mode? "create": "open", path, err);
		return 1;
	}

	/* Modify next entry to add to use specified compression method */
	if (compression_method != 0) {
		/* For this mzip structure, store the compression method in the archive */
		((struct zip *)za)->default_method = compression_method;
	}

	int failures = 0;
	for (int i = 0; i < num_files; i++) {
		const char *filename = files[i];

		/* Open file to read content */
		FILE *fp = fopen (filename, "rb");
		if (!fp) {
			fprintf (stderr, "Cannot open file: %s\n", filename);
			failures++;
			continue;
		}

		/* Get file size */
		fseek (fp, 0, SEEK_END);
		long file_size = ftell (fp);
		fseek (fp, 0, SEEK_SET);

		if (file_size < 0) {
			fprintf (stderr, "Error determining file size: %s\n", filename);
			fclose (fp);
			failures++;
			continue;
		}

		/* Read file content */
		void *buffer = malloc (file_size);
		if (!buffer) {
			fprintf (stderr, "Out of memory for file: %s\n", filename);
			fclose (fp);
			failures++;
			continue;
		}

		if (fread (buffer, 1, file_size, fp) != (size_t)file_size) {
			fprintf (stderr, "Error reading file: %s\n", filename);
			fclose (fp);
			free (buffer);
			failures++;
			continue;
		}
		fclose (fp);

		/* Extract just the base filename */
		const char *base_name = filename;
		const char *slash = strrchr (filename, '/');
		if (slash) {
			base_name = slash + 1;
		}

		/* Create source and add to archive */
		zip_source_t *src = zip_source_buffer (za, buffer, file_size, 1); /* 1 = free buffer when done */
		if (!src) {
			fprintf (stderr, "Failed to create source for file: %s\n", filename);
			free (buffer);
			failures++;
			continue;
		}

		/* Add file to archive */
		zip_int64_t idx = zip_file_add (za, base_name, src, 0);
		if (idx < 0) {
			fprintf (stderr, "Failed to add file to archive: %s\n", filename);
			/* Source buffer is already freed in case of failure */
			failures++;
			continue;
		}

		/* Compression method is already applied via default_method before add.
		 * Avoid changing per-entry method post-facto to prevent header mismatch */

		printf ("Added: %s (%ld bytes)\n", base_name, file_size);
	}

	/* Close and finalize the zip file */
	if (zip_close (za) != 0) {
		fprintf (stderr, "Failed to write %s\n", path);
		failures++;
	}
	return failures? 1: 0;
}

/* Normalize a zip entry name into 'out'. Return 0 on success, -1 on invalid path. */
/* Extraction policies */
#define POLICY_REJECT 0 /* default: reject suspicious entries */
#define POLICY_STRIP 1 /* strip leading '..' components */
#define POLICY_ALLOW 2 /* allow unsafe extraction(use with caution) */

static int g_extract_policy = POLICY_REJECT;

/* Only the allow policy writes through symlinks; strip only rewrites '..' */
static int follow_symlinks(void) {
	return g_extract_policy == POLICY_ALLOW;
}

static int sanitize_extract_path(const char *name, char *out, size_t outlen) {
	if (!name || !*name) {
		return -1;
	}
	size_t nlen = strlen (name);
	if (nlen >= (size_t)PATH_MAX) {
		return -1;
	}

	char tmp[PATH_MAX];
	/* Normalize backslashes to forward slashes */
	for (size_t i = 0; i < nlen; ++i) {
		tmp[i] = (name[i] == '\\')? '/': name[i];
	}
	tmp[nlen] = '\0';

	/* Reject absolute paths */
	if (tmp[0] == '/' || tmp[0] == '\\') {
		return -1;
	}
	/* Reject Windows drive absolute like C:/ or C:\ */
	if (nlen >= 2 && ((tmp[1] == ':' && ((tmp[0] >= 'A' && tmp[0] <= 'Z') || (tmp[0] >= 'a' && tmp[0] <= 'z'))))) {
		return -1;
	}

	/* Tokenize and resolve '.' and '..' without touching the filesystem */
	char *segments[PATH_MAX / 2];
	int segc = 0;
	/* Manual tokenization to avoid dependency on strtok_r feature macros */
	char *p = tmp;
	while (*p) {
		/* skip multiple slashes */
		while (*p == '/') {
			p++;
		}
		if (!*p) {
			break;
		}
		char *start = p;
		while (*p && *p != '/') {
			p++;
		}
		/* terminate segment */
		char saved = *p;
		*p = '\0';
		if (strcmp (start, "") == 0 || strcmp (start, ".") == 0) {
			/* skip */
		} else if (strcmp (start, "..") == 0) {
			if (segc == 0) {
				if (g_extract_policy == POLICY_REJECT) {
					return -1;
				} else {
					/* POLICY_STRIP: drop leading .., POLICY_ALLOW: treat as no-op */
				}
			} else {
				segc--;
			}
		} else {
			if (segc < (int) (PATH_MAX / 2)) {
				segments[segc++] = start;
			}
		}
		if (saved == '\0') {
			break;
		}
		/* keep the segment terminated (segments[] points into tmp), move past '/' */
		p++;
	}

	if (segc == 0) {
		return -1; /* empty or only dots */
	}

	/* Build normalized path */
	size_t pos = 0;
	for (int i = 0; i < segc; ++i) {
		size_t need = strlen (segments[i]);
		if (i) {
			if (pos + 1 >= outlen) {
				return -1;
			}
			out[pos++] = '/';
		}
		if (pos + need >= outlen) {
			return -1;
		}
		memcpy (out + pos, segments[i], need);
		pos += need;
	}
	out[pos] = '\0';
	return 0;
}

/* Ensure parent directories exist and are not symlinks. Return 0 on success. */
static int ensure_parent_dirs(const char *path) {
	char tmp[PATH_MAX];
	strncpy (tmp, path, sizeof (tmp));
	tmp[sizeof (tmp) - 1] = '\0';

	size_t len = strlen (tmp);
	for (size_t i = 0; i < len; ++i) {
		if (tmp[i] == '/') {
			tmp[i] = '\0';
			struct stat st;
			if (OTEZIP_LSTAT (tmp, &st) == 0) {
				/* If the path exists, reject symlinks when policy says so */
				if (S_ISLNK (st.st_mode) && !follow_symlinks ()) {
					tmp[i] = '/';
					return -1;
				}
				if (!S_ISDIR (st.st_mode) && !S_ISLNK (st.st_mode)) {
					tmp[i] = '/';
					return -1;
				}
			} else {
				if (errno == ENOENT) {
					/* Try to create directory. If another thread/process created it
					 * concurrently, handle EEXIST by re-checking via lstat to avoid TOCTOU. */
					if (OTEZIP_MKDIR (tmp, 0755) != 0) {
						if (errno == EEXIST) {
							/* Re-check what exists */
							if (OTEZIP_LSTAT (tmp, &st) != 0) {
								tmp[i] = '/';
								return -1;
							}
							if (S_ISLNK (st.st_mode) && !follow_symlinks ()) {
								tmp[i] = '/';
								return -1;
							}
							if (!S_ISDIR (st.st_mode)) {
								tmp[i] = '/';
								return -1;
							}
						} else {
							tmp[i] = '/';
							return -1;
						}
					}
				} else {
					tmp[i] = '/';
					return -1;
				}
			}
			tmp[i] = '/';
		}
	}
	return 0;
}

/* Open path for writing an extracted file: create it exclusively, or with -f
 * truncate an existing regular file (never through a symlink unless the
 * policy allows it). Returns the descriptor or -1 after printing why. */
static int open_output(const char *path, mode_t mode) {
	int fd = open (path, O_WRONLY | O_CREAT | O_EXCL | O_BINARY, mode);
	if (fd < 0) {
		if (errno != EEXIST) {
			fprintf (stderr, "Cannot create %s: %s\n", path, strerror (errno));
			return -1;
		}
		if (!g_force) {
			fprintf (stderr, "Skipping existing file (use -f to overwrite): %s\n", path);
			return -1;
		}
		struct stat pst;
		if (OTEZIP_LSTAT (path, &pst) == 0 && S_ISLNK (pst.st_mode) && !follow_symlinks ()) {
			fprintf (stderr, "Refusing to overwrite symlink: %s\n", path);
			return -1;
		}
		/* O_NONBLOCK: opening a FIFO without a reader would block forever */
		fd = open (path, O_WRONLY | O_TRUNC | O_NOFOLLOW | O_NONBLOCK | O_BINARY);
		if (fd < 0) {
			fprintf (stderr, "Cannot open for overwrite %s: %s\n", path, strerror (errno));
			return -1;
		}
	}
	/* Ensure we didn't end up writing to a special file */
	struct stat st;
	if (fstat (fd, &st) != 0) {
		fprintf (stderr, "Failed to stat %s\n", path);
		close (fd);
		return -1;
	}
	if (!S_ISREG (st.st_mode)) {
		fprintf (stderr, "Refusing to write non-regular file %s\n", path);
		close (fd);
		return -1;
	}
	/* Apply safe permissions (masking out SUID/SGID/sticky by using 0777 mask) */
	if (OTEZIP_FCHMOD (fd, mode & 0777) != 0) {
		/* Non-fatal: warn but continue */
		fprintf (stderr, "Warning: failed to set permissions on %s: %s\n", path, strerror (errno));
	}
	return fd;
}

/* Write all of data to fd; returns 0 on success */
static int write_all(int fd, const uint8_t *data, size_t size, const char *path) {
	while (size > 0) {
		unsigned int len = (size > UINT_MAX)? UINT_MAX: (unsigned int)size;
		ssize_t n = write (fd, data, len);
		if (n < 0) {
			if (errno == EINTR) {
				continue;
			}
			fprintf (stderr, "Write error for %s: %s\n", path, strerror (errno));
			return -1;
		}
		data += n;
		size -= (size_t)n;
	}
	return 0;
}

/* Extract entry i (already opened as zf); returns 1 on failure */
static int extract_entry(zip_t *za, zip_uint64_t i, zip_file_t *zf) {
	const struct otezip_entry *entry = &((struct otezip_entry *)za->entries)[i]; /* internal */
	const char *raw_name = entry->name;
	char path[PATH_MAX];
	if (sanitize_extract_path (raw_name, path, sizeof (path)) != 0) {
		fprintf (stderr, "Skipping suspicious entry: %s\n", raw_name? raw_name: "(null)");
		return 1;
	}

	/* If entry denotes a directory (name ends with '/'), create it */
	size_t rlen = raw_name? strlen (raw_name): 0;
	if (rlen > 0 && raw_name[rlen - 1] == '/') {
		if (ensure_parent_dirs (path) != 0) {
			fprintf (stderr, "Failed to create directory for %s\n", path);
			return 1;
		}
		if (OTEZIP_MKDIR (path, 0755) != 0 && errno != EEXIST) {
			fprintf (stderr, "Failed to create directory %s\n", path);
			return 1;
		}
		return 0;
	}

	if (ensure_parent_dirs (path) != 0) {
		fprintf (stderr, "Cannot ensure parent dirs for %s\n", path);
		return 1;
	}

	/* Mode from the central directory external attributes, masked to 0777
	 * to avoid applying SUID/SGID/sticky from the archive */
	mode_t mode = (mode_t) ((entry->external_attr >> 16) & 0777);
	if (mode == 0) {
		mode = 0644; /* fallback */
	}
	int fd = open_output (path, mode);
	if (fd < 0) {
		return 1;
	}
	/* Stream the entry; decoding and CRC errors surface from zip_fread */
	uint8_t chunk[65536];
	unsigned long total = 0;
	int ok = 1;
	int read_failed = 0;
	for (;;) {
		zip_int64_t n = zip_fread (zf, chunk, sizeof (chunk));
		if (n < 0) {
			fprintf (stderr, "Could not read entry %s\n", path);
			ok = 0;
			read_failed = 1;
			break;
		}
		if (n == 0) {
			break;
		}
		if (write_all (fd, chunk, (size_t)n, path) != 0) {
			ok = 0;
			break;
		}
		total += (unsigned long)n;
	}
	if (close (fd) != 0 && ok) {
		fprintf (stderr, "Write error for %s: %s\n", path, strerror (errno));
		ok = 0;
	}
	if (!ok) {
		/* Don't leave a truncated file behind */
		unlink (path);
		if (!read_failed) {
			fprintf (stderr, "Failed to fully write %s\n", path);
		}
		return 1;
	}
	printf ("Extracted %s (%lu bytes)\n", path, total);
	return 0;
}

static int extract_all(const char *path) {
	int err = 0;
	zip_t *za = zip_open (path, ZIP_RDONLY, &err);
	if (!za) {
		fprintf (stderr, "Failed to open %s (err=%d)\n", path, err);
		return 1;
	}

	int failures = 0;
	zip_uint64_t n = zip_get_num_files (za);
	for (zip_uint64_t i = 0; i < n; ++i) {
		zip_file_t *zf = zip_fopen_index (za, i, 0);
		if (!zf) {
			fprintf (stderr, "Could not read entry %llu\n", (unsigned long long)i);
			failures++;
			continue;
		}
		failures += extract_entry (za, i, zf);
		zip_fclose (zf);
	}

	zip_close (za);
	return failures? 1: 0;
}

/* Inflate a gzip/zlib buffer (header auto-detected) into fout in fixed
 * chunks, so memory use does not depend on the decompressed size. The input
 * stays one buffer: inflate cannot resume a symbol split across calls.
 * Returns 0 on success and sets *out_size to the decompressed size. */
static int inflate_to_file(const uint8_t *input_data, size_t input_size, FILE *fout, size_t *out_size) {
	z_stream strm;
	memset (&strm, 0, sizeof (strm));
	strm.next_in = (uint8_t *)input_data;
	strm.avail_in = UINT_CLAMP (input_size);

	if (inflateInit2 (&strm, MAX_WBITS + 32) != Z_OK) {
		fprintf (stderr, "inflateInit2 failed\n");
		return -1;
	}

	uint8_t chunk[65536];
	int ok = 0;
	for (;;) {
		/* Feed the rest of inputs larger than avail_in can hold */
		if (strm.avail_in == 0) {
			size_t consumed = (size_t)(strm.next_in - input_data);
			strm.avail_in = UINT_CLAMP (input_size - consumed);
		}
		strm.next_out = chunk;
		strm.avail_out = sizeof (chunk);
		uLong in_before = strm.total_in;
		int ret = inflate (&strm, Z_NO_FLUSH);
		size_t have = sizeof (chunk) - strm.avail_out;
		if (have > 0 && fwrite (chunk, 1, have, fout) != have) {
			fprintf (stderr, "Failed to write output file\n");
			break;
		}
		if (ret == Z_STREAM_END) {
			ok = 1;
			break;
		}
		if (ret != Z_OK && ret != Z_BUF_ERROR) {
			fprintf (stderr, "inflate failed: %d (avail_in=%u total_out=%lu)\n",
				ret, strm.avail_in, (unsigned long)strm.total_out);
			break;
		}
		if (have == 0 && strm.total_in == in_before) {
			fprintf (stderr, "inflate failed: truncated input\n");
			break;
		}
	}

	*out_size = (size_t)strm.total_out;
	inflateEnd (&strm);
	return ok? 0: -1;
}

/* Deflate (compress) a buffer with gzip framing.
 * Returns a malloc'd buffer on success (caller frees), NULL on failure.
 * Sets *out_size to the compressed size on success. */
static uint8_t *deflate_to_gzip(const uint8_t *input_data, size_t input_size, size_t *out_size) {
	z_stream strm;
	memset (&strm, 0, sizeof (strm));

	if (deflateInit2 (&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
		fprintf (stderr, "deflateInit2 failed\n");
		return NULL;
	}

	size_t alloc = input_size + (input_size / 8) + 128;
	if (alloc < 256) {
		alloc = 256;
	}
	uint8_t *buf = (uint8_t *)malloc (alloc);
	if (!buf) {
		fprintf (stderr, "Out of memory for output buffer\n");
		deflateEnd (&strm);
		return NULL;
	}

	strm.next_in = (uint8_t *)input_data;
	strm.avail_in = UINT_CLAMP (input_size);
	strm.next_out = buf;
	strm.avail_out = UINT_CLAMP (alloc);

	int ret = deflate (&strm, Z_FINISH);
	*out_size = (size_t)(strm.next_out - buf);
	deflateEnd (&strm);
	if (ret != Z_STREAM_END) {
		fprintf (stderr, "deflate failed: %d\n", ret);
		free (buf);
		return NULL;
	}
	return buf;
}

/* Decompress a gzip file (gunzip mode) */
static int gunzip_file(const char *input_path, const char *output_path) {
	FILE *fin = fopen (input_path, "rb");
	if (!fin) {
		fprintf (stderr, "Cannot open input file: %s\n", input_path);
		return 1;
	}

	fseek (fin, 0, SEEK_END);
	long fsize = ftell (fin);
	fseek (fin, 0, SEEK_SET);

	if (fsize <= 0) {
		fprintf (stderr, "Invalid input file size\n");
		fclose (fin);
		return 1;
	}
	size_t input_size = (size_t)fsize;

	uint8_t *input_data = (uint8_t *)malloc (input_size);
	if (!input_data) {
		fprintf (stderr, "Out of memory\n");
		fclose (fin);
		return 1;
	}

	if (fread (input_data, 1, input_size, fin) != input_size) {
		fprintf (stderr, "Failed to read input file\n");
		free (input_data);
		fclose (fin);
		return 1;
	}
	fclose (fin);

	FILE *fout = fopen (output_path, "wb");
	if (!fout) {
		fprintf (stderr, "Cannot create output file: %s\n", output_path);
		free (input_data);
		return 1;
	}

	size_t output_size = 0;
	int ret = inflate_to_file (input_data, input_size, fout, &output_size);
	free (input_data);
	if (fclose (fout) != 0 && ret == 0) {
		fprintf (stderr, "Failed to write output file\n");
		ret = -1;
	}
	if (ret != 0) {
		/* Don't leave a partial file behind */
		unlink (output_path);
		return 1;
	}
	printf ("Decompressed %s -> %s (%ld -> %lu bytes)\n",
		input_path, output_path, fsize, (unsigned long)output_size);
	return 0;
}

/* Compress a file with gzip */
static int gzip_file(const char *input_path, const char *output_path) {
	FILE *fin = fopen (input_path, "rb");
	if (!fin) {
		fprintf (stderr, "Cannot open input file: %s\n", input_path);
		return 1;
	}

	fseek (fin, 0, SEEK_END);
	long fsize = ftell (fin);
	fseek (fin, 0, SEEK_SET);

	if (fsize < 0) {
		fprintf (stderr, "Invalid input file size\n");
		fclose (fin);
		return 1;
	}
	size_t input_size = (size_t)fsize;

	uint8_t *input_data = NULL;
	if (input_size > 0) {
		input_data = (uint8_t *)malloc (input_size);
		if (!input_data) {
			fprintf (stderr, "Out of memory\n");
			fclose (fin);
			return 1;
		}
		if (fread (input_data, 1, input_size, fin) != input_size) {
			fprintf (stderr, "Failed to read input file\n");
			free (input_data);
			fclose (fin);
			return 1;
		}
	}
	fclose (fin);

	size_t output_size = 0;
	uint8_t *output_data = deflate_to_gzip (input_data, input_size, &output_size);
	free (input_data);
	if (!output_data) {
		return 1;
	}

	FILE *fout = fopen (output_path, "wb");
	if (!fout) {
		fprintf (stderr, "Cannot create output file: %s\n", output_path);
		free (output_data);
		return 1;
	}

	if (fwrite (output_data, 1, output_size, fout) != output_size) {
		fprintf (stderr, "Failed to write output file\n");
		fclose (fout);
		free (output_data);
		return 1;
	}

	fclose (fout);
	free (output_data);
	printf ("Compressed %s -> %s (%ld -> %lu bytes)\n",
		input_path, output_path, fsize, (unsigned long)output_size);
	return 0;
}

int main(int argc, char **argv) {
	if (argc < 2) {
		usage ();
		return 1;
	}

	if (strcmp (argv[1], "-v") == 0) {
		printf ("mzip version %s\n", OTEZIP_VERSION);
		return 0;
	}

	/* Treat -h/--help as a successful request for usage information */
	if (strcmp (argv[1], "-h") == 0 || strcmp (argv[1], "--help") == 0) {
		usage ();
		return 0;
	}

	/* Handle gzip/gunzip modes early - they only need input file */
	if (strcmp (argv[1], "-d") == 0) {
		/* Decompress (gunzip) mode */
		if (argc < 3) {
			fprintf (stderr, "Error: No input file specified for decompression\n");
			usage ();
			return 1;
		}
		const char *input_path = argv[2];
		char output_path[PATH_MAX];
		if (argc >= 4) {
			strncpy (output_path, argv[3], sizeof (output_path) - 1);
			output_path[sizeof (output_path) - 1] = '\0';
		} else {
			/* Remove .gz extension if present, else add .out */
			size_t len = strlen (input_path);
			if (len > 3 && strcmp (input_path + len - 3, ".gz") == 0) {
				snprintf (output_path, sizeof (output_path), "%.*s", (int) (len - 3), input_path);
			} else {
				snprintf (output_path, sizeof (output_path), "%s.out", input_path);
			}
		}
		return gunzip_file (input_path, output_path);
	}

	if (strcmp (argv[1], "-g") == 0) {
		/* Compress (gzip) mode */
		if (argc < 3) {
			fprintf (stderr, "Error: No input file specified for compression\n");
			usage ();
			return 1;
		}
		const char *input_path = argv[2];
		char output_path[PATH_MAX];
		if (argc >= 4) {
			strncpy (output_path, argv[3], sizeof (output_path) - 1);
			output_path[sizeof (output_path) - 1] = '\0';
		} else {
			snprintf (output_path, sizeof (output_path), "%s.gz", input_path);
		}
		return gzip_file (input_path, output_path);
	}

	if (argc < 3) {
		usage ();
		return 1;
	}

	int mode_list = 0, mode_extract = 0, mode_create = 0, mode_append = 0;

	/* Set default compression method based on available algorithms */
	int compression_method = 0; /* Default to store */
#ifdef OTEZIP_ENABLE_DEFLATE
	compression_method = OTEZIP_METHOD_DEFLATE; /* Default to deflate if available */
#endif

	if (strcmp (argv[1], "-l") == 0) {
		mode_list = 1;
	} else if (strcmp (argv[1], "-x") == 0) {
		mode_extract = 1;
	} else if (strcmp (argv[1], "-c") == 0) {
		mode_create = 1;
	} else if (strcmp (argv[1], "-a") == 0) {
		mode_append = 1;
	} else {
		usage ();
		return 1;
	}

	const char *zip_path = argv[2];

	/* Parse options and collect file arguments in a single pass, so options
	 * may appear anywhere after the archive name. "--" ends option parsing. */
	char **files_to_add = (char **)calloc (argc, sizeof (char *));
	int num_files = 0;
	if (!files_to_add) {
		fprintf (stderr, "Error: out of memory\n");
		return 1;
	}
	int i;
	int only_files = 0;
	for (i = 3; i < argc; i++) {
		const char *arg = argv[i];
		if (only_files || arg[0] != '-' || arg[1] == '\0') {
			files_to_add[num_files++] = argv[i];
			continue;
		}
		if (strcmp (arg, "--") == 0) {
			only_files = 1;
		} else if (strncmp (arg, "-z", 2) == 0) {
			/* -z <method> or -z<method> */
			const char *name = arg + 2;
			if (*name == '\0') {
				if (i + 1 >= argc) {
					fprintf (stderr, "Error: -z requires a method argument\n");
					free (files_to_add);
					return 1;
				}
				name = argv[++i];
			}
			int method = otezip_method_from_string (name);
			if (method == -1) {
				fprintf (stderr, "Error: unknown compression method '%s'\n", name);
				free (files_to_add);
				return 1;
			}
			compression_method = method;
		} else if (strncmp (arg, "-P", 2) == 0 || strncmp (arg, "--policy=", 9) == 0) {
			/* -P<policy>, -P <policy> or --policy=<policy> */
			const char *val = (arg[1] == 'P')? arg + 2: arg + 9;
			if (*val == '\0' && arg[1] == 'P' && i + 1 < argc) {
				val = argv[++i];
			}
			if (strcmp (val, "reject") == 0 || strcmp (val, "0") == 0) {
				g_extract_policy = POLICY_REJECT;
			} else if (strcmp (val, "strip") == 0 || strcmp (val, "1") == 0) {
				g_extract_policy = POLICY_STRIP;
			} else if (strcmp (val, "allow") == 0 || strcmp (val, "2") == 0) {
				g_extract_policy = POLICY_ALLOW;
			} else {
				fprintf (stderr, "Unknown policy: %s\n", val);
				free (files_to_add);
				return 1;
			}
		} else if (strcmp (arg, "--verify-crc") == 0) {
			/* enable strict CRC verification in the mzip backend */
			otezip_verify_crc = 1;
		} else if (strcmp (arg, "--ignore-zipbomb") == 0) {
			/* explicit, dangerous override: allow archives that claim huge
			 * uncompressed sizes. This bypasses internal safety checks. */
			otezip_ignore_zipbomb = 1;
		} else if (strcmp (arg, "-f") == 0 || strcmp (arg, "--force") == 0) {
			/* allow overwriting existing files */
			g_force = 1;
		} else {
			fprintf (stderr, "Error: unknown option '%s'\n", arg);
			free (files_to_add);
			return 1;
		}
	}

	if ((mode_list || mode_extract) && num_files > 0) {
		fprintf (stderr, "Error: unexpected argument '%s'\n", files_to_add[0]);
		free (files_to_add);
		return 1;
	}

	int ret = 1;
	if (mode_list) {
		ret = list_files (zip_path);
	} else if (mode_extract) {
		ret = extract_all (zip_path);
	} else if (mode_create || mode_append) {
		if (num_files == 0) {
			fprintf (stderr, "Error: No files specified to %s.\n", mode_create? "create archive with": "add to archive");
			usage ();
		} else {
			ret = create_or_add_files (zip_path, files_to_add, num_files, mode_create, compression_method);
		}
	}
	free (files_to_add);
	return ret;
}
