/* OpenAmiga.org
 *  This file is part of the OpenAmiga "xfdmaster library project"
 *
 * For more information look at:
 *   http://openamiga.org/?function=viewproject&projectid=67
 */

#include "utils.h"
#include <dos/obsolete.h> /* Needed for FIB_IS_#? macros */
#include <proto/xpkmaster.h>
#include <string.h>
#include <stdarg.h>
#include "../memio.h"

static const char USED version[] = "\0$VER: xfdDecrunch 53.2 (11.9.2014)";

static const char template[] = "SOURCE/A,DEST,ALL/S,FORCE/S,CLONE/S,ND=NODIRS/S,NA=NOASK/S,"
	"LS=LINKSIMPLE/S,LP=LINKPRO/S,CA=COPYALL/S,SH=STRIPHUNKS/S,NE=NOEXTERN/S,NX=NOXPKPWD/S";

enum {
	ARG_SOURCE,
	ARG_DEST,
	ARG_ALL,
	ARG_FORCE,
	ARG_CLONE,
	ARG_NODIRS,
	ARG_NOASK,
	ARG_LINKSIMPLE,
	ARG_LINKPRO,
	ARG_COPYALL,
	ARG_STRIPHUNKS,
	ARG_NOEXTERN,
	ARG_NOXPKPWD,
	ARG_MAX
};

struct global_data {
	struct xfdMasterIFace *ixfd;
	struct xfdBufferInfo *xbi;
	struct AnchorPath *ap;
	APTR buffer;
	ULONG buflen;
	LONG args[ARG_MAX];
	BOOL onefile;
	BOOL destfile;
};

static BOOL decrunch_dir(struct global_data *gd, const char *source, const char *dest);
static BOOL read_file(const char *fn, APTR buffer, ULONG buflen);
static BOOL decrunch_file(struct global_data *gd, const char *fn);
static BOOL get_password(struct global_data *gd);
static void text_prompt(const char *prompt, char *buffer, LONG buflen);
static BOOL parse_key(const char *keystr, ULONG *result);
static BOOL write_file(struct global_data *gd, const char *fn, APTR buffer, ULONG buflen);
static BOOL yesno_prompt(const char *prompt);

int main(void) {
	struct global_data gd;
	struct xfdMasterBase *XfdBase;
	struct xfdMasterIFace *IXfd = NULL;
	struct RDArgs *rda;
	int rc = RETURN_ERROR;

	signal(SIGINT, SIG_IGN);

	memset(&gd, 0, sizeof(gd));

	IDOS->Printf(CSI"1m%s"CSI"0;32m (C) 2014 Fredrik Wikstrom"CSI";31m\n\n",
		version + 7);

	XfdBase = (struct xfdMasterBase *)IExec->OpenLibrary("xfdmaster.library", 53);
	IXfd = (struct xfdMasterIFace *)IExec->GetInterface((struct Library *)XfdBase, "main", 1, NULL);
	if (IXfd == NULL || !LIB_IS_AT_LEAST(&XfdBase->LibNode, 53, 7)) {
		IExec->DropInterface((struct Interface *)IXfd);
		IExec->CloseLibrary((struct Library *)XfdBase);
		IDOS->PutErrStr("You need xfdmaster.library v53.7+\n");
		return RETURN_FAIL;
	}

	rda = IDOS->ReadArgs(template, gd.args, NULL);
	if (rda != NULL) {
		gd.ixfd = IXfd;

		gd.xbi = IXfd->xfdAllocObject(XFDOBJ_BUFFERINFO);
		gd.ap = IDOS->AllocDosObjectTags(DOS_ANCHORPATH,
			ADO_Strlen, PATHMAX,
			TAG_END);

		if (gd.xbi != NULL && gd.ap != NULL) {
			if (decrunch_dir(&gd, (const char *)gd.args[ARG_SOURCE], (const char *)gd.args[ARG_DEST])) {
				rc = RETURN_OK;
			}
		} else {
			IDOS->PrintFault(ERROR_NO_FREE_STORE, NULL);
		}

		IXfd->xfdFreeObject(gd.xbi);
		IDOS->FreeDosObject(DOS_ANCHORPATH, gd.ap);

		IDOS->FreeArgs(rda);
	} else {
		IDOS->PrintFault(IDOS->IoErr(), NULL);
	}

	IExec->DropInterface((struct Interface *)IXfd);
	IExec->CloseLibrary((struct Library *)XfdBase);

	return rc;
}

static BOOL decrunch_dir(struct global_data *gd, const char *source, const char *dest) {
	struct AnchorPath *ap = gd->ap;
	BOOL res = FALSE;
	LONG error;
	BPTR lock;
	char pathbuf[PATHMAX];

	if (source == NULL) source = "";

	if (dest == NULL) dest = "";

	if (strcmp(source, "*") == 0) source = "#?";

	if (gd->args[ARG_LINKSIMPLE] && gd->args[ARG_LINKPRO]) {
		IDOS->PutErrStr("You cannot link several headers.\n");
		return FALSE;
	}

	ap->ap_BreakBits = SIGBREAKF_CTRL_C;
    ap->ap_Flags |= APF_DOWILD;

	if ((error = IDOS->MatchFirst(source, ap)) == 0) {
		if (!(ap->ap_Flags & APF_ITSWILD)) {
			if (FIB_IS_FILE(&ap->ap_Info)) {
				gd->onefile = TRUE;
			} else {
				ap->ap_Flags |= APF_DODIR;
				if ((error = IDOS->MatchNext(ap)) == 0 && (ap->ap_Flags & APF_DIDDIR)) {
					ap->ap_Flags &= ~APF_DIDDIR;
				}
			}
		}
	}

	if (error == 0) {
		strlcpy(pathbuf, dest, sizeof(pathbuf));
		lock = IDOS->Lock(pathbuf, ACCESS_READ);
		if (lock != ZERO) {
			struct ExamineData *exd;
			exd = IDOS->ExamineObjectTags(
				EX_LockInput, lock,
				TAG_END);
			if (exd != NULL) {
				if (EXD_IS_FILE(exd)) {
					if (gd->onefile) {
						gd->destfile = TRUE;
					} else {
						error = ERROR_OBJECT_WRONG_TYPE;
					}
				}
				IDOS->FreeDosObject(DOS_EXAMINEDATA, exd);
			} else {
				error = IDOS->IoErr();
			}
			IDOS->UnLock(lock);
		} else {
			error = IDOS->IoErr();
			if (error == ERROR_OBJECT_NOT_FOUND) {
				error = 0;
				if (gd->onefile) {
					gd->destfile = TRUE;
				} else {
					lock = IDOS->CreateDir(pathbuf);
					if (lock != ZERO) {
						IDOS->UnLock(lock);
					} else {
						error = IDOS->IoErr();
					}
				}
			}
		}
		if (error != 0) {
			IDOS->PrintFault(error, pathbuf);
			goto Error;
		}
	}

	if (error == 0) {
		ap->ap_Flags &= ~APF_DIDDIR;

		do {
			if (ap->ap_Flags & APF_DIDDIR) {
				ap->ap_Flags &= ~APF_DIDDIR;
				if (!gd->args[ARG_NODIRS]) {
					if (gd->args[ARG_CLONE]) {
						IDOS->SetComment(pathbuf, ap->ap_Info.fib_Comment);
						IDOS->SetProtection(pathbuf, ap->ap_Info.fib_Protection);
					}
					IDOS->PathPart(pathbuf)[0] = '\0';
				}
				continue;
			}
			if (FIB_IS_FILE(&ap->ap_Info)) {
				BOOL success = FALSE;

				if (!gd->destfile &&
					!IDOS->AddPart(pathbuf, ap->ap_Info.fib_FileName, sizeof(pathbuf)))
				{
					IDOS->PrintFault(IDOS->IoErr(), NULL);
					goto Error;
				}

				gd->buflen = ap->ap_Info.fib_Size;
				gd->buffer = IExec->AllocVecTags(gd->buflen,
					AVT_Type, MEMF_SHARED,
					AVT_Lock, FALSE,
					TAG_END);
				if (gd->buffer) {
					if (read_file(ap->ap_Buffer, gd->buffer, gd->buflen) &&
						decrunch_file(gd, pathbuf))
					{
						if (gd->args[ARG_CLONE]) {
							IDOS->SetComment(pathbuf, ap->ap_Info.fib_Comment);
							IDOS->SetProtection(pathbuf, ap->ap_Info.fib_Protection);
						}
						success = TRUE;
					}
					IExec->FreeVec(gd->buffer);
				}
				IDOS->PutStr("\n");

				if (!gd->destfile) {
					IDOS->PathPart(pathbuf)[0] = '\0';
				}

				if (!success && !gd->onefile && !gd->args[ARG_NOASK] &&
					!yesno_prompt("Continue after error (Y/N)? "))
				{
					goto Error;
				}
			} else {
				if (gd->args[ARG_ALL]) {
					ap->ap_Flags |= APF_DODIR;
					if (!gd->args[ARG_NODIRS]) {
						if (!IDOS->AddPart(pathbuf, ap->ap_Info.fib_FileName, sizeof(pathbuf))) {
							IDOS->PrintFault(IDOS->IoErr(), NULL);
							goto Error;
						}
						lock = IDOS->CreateDir(pathbuf);
						if (lock == ZERO) {
							IDOS->PrintFault(IDOS->IoErr(), pathbuf);
							goto Error;
						}
						IDOS->UnLock(lock);
					}
				}
			}
		} while ((error = IDOS->MatchNext(ap)) == 0);
	}

	if (error == 0) {
		res = TRUE;
	} else if (error != ERROR_NO_MORE_ENTRIES) {
		IDOS->PrintFault(error, source);
	}

Error:
	IDOS->MatchEnd(ap);

	return res;
}

static BOOL read_file(const char *fn, APTR buffer, ULONG buflen) {
	BOOL res = FALSE;
	BPTR fh;

	IDOS->Printf("Reading \"%s\" (%lu bytes)...\n", fn, buflen);

	fh = IDOS->Open(fn, MODE_OLDFILE);
	if (fh == ZERO) goto Error;

	if (IDOS->Read(fh, buffer, buflen) != buflen) goto Error;

	res = TRUE;

Error:
	IDOS->Close(fh);

	if (!res) IDOS->PrintFault(IDOS->IoErr(), fn);

	return res;
}

static BOOL decrunch_file(struct global_data *gd, const char *fn) {
	struct xfdMasterIFace *IXfd = gd->ixfd;
	struct xfdBufferInfo *xbi = gd->xbi;
	BOOL res = FALSE;

	if (gd->buflen && gd->args[ARG_STRIPHUNKS] && READ_ULONG(gd->buffer, 0) == HUNK_HEADER) {
		IXfd->xfdStripHunks(gd->buffer, gd->buflen, &gd->buflen,
			XFDSHF_NAME|XFDSHF_SYMBOL|XFDSHF_DEBUG);
	}

	xbi->xfdbi_SourceBuffer = gd->buffer;
	xbi->xfdbi_SourceBufLen = gd->buflen;
	xbi->xfdbi_Flags = 0;
	if (!gd->args[ARG_NOEXTERN]) xbi->xfdbi_Flags |= XFDFF_RECOGEXTERN;

	if (IXfd->xfdRecogBuffer(xbi)) {
		IDOS->Printf("Decrunching \"%s\" file...\n", xbi->xfdbi_PackerName);

		if ((xbi->xfdbi_PackerFlags & (XFDPFF_PASSWORD|XFDPFF_KEY16|XFDPFF_KEY32)) &&
			!get_password(gd))
		{
			return FALSE;
		}

		if (IXfd->xfdDecrunchBuffer(xbi)) {
			IExec->FreeVec(gd->buffer); gd->buffer = NULL;

			res = write_file(gd, fn, xbi->xfdbi_TargetBuffer, xbi->xfdbi_TargetBufSaveLen);

			IExec->FreeMem(xbi->xfdbi_TargetBuffer, xbi->xfdbi_TargetBufLen);
		} else {
			IXfd->xfdPrintFault(xbi->xfdbi_Error, "Decrunch failed");
		}
	} else {
		IDOS->PutStr("Not crunched\n");

		if (gd->args[ARG_COPYALL]) {
			res = write_file(gd, fn, gd->buffer, gd->buflen);
		} else {
			res = TRUE; /* Not an error */
		}
	}

	return res;
}

static BOOL get_password(struct global_data *gd) {
	struct xfdBufferInfo *xbi = gd->xbi;
	struct Library *XpkBase = NULL;
	struct XpkIFace *IXpk = NULL;
	static union {
		char password[100];
		UWORD key16;
		ULONG key32;
	} buffer;
	BOOL res = FALSE;

	if (!gd->args[ARG_NOXPKPWD]) {
		XpkBase = IExec->OpenLibrary("xpkmaster.library", 5);
		IXpk = (struct XpkIFace *)IExec->GetInterface(XpkBase, "main", 1, NULL);
	}

	if (IXpk != NULL) {
		LONG error;

		if ((xbi->xfdbi_PackerFlags & XFDPFF_PASSWORD)) {
			error = IXpk->XpkPassRequestTags(
				XPK_PasswordBuf, &buffer,
				XPK_PassBufSize, 100,
				TAG_END);
		} else {
			const ULONG xpktag = (xbi->xfdbi_PackerFlags & XFDPFF_KEY16)
				? XPK_Key16BitPtr : XPK_Key32BitPtr;
			error = IXpk->XpkPassRequestTags(
				xpktag, &buffer,
				TAG_END);
		}

		if (error == XPKERR_OK) {
			xbi->xfdbi_Special = &buffer;
			res = TRUE;
		} else {
			IXpk->XpkPrintFault(error, NULL);
		}
	} else {
		if ((xbi->xfdbi_PackerFlags & XFDPFF_PASSWORD)) {
			text_prompt("Enter password: ", buffer.password, 100);
			xbi->xfdbi_Special = &buffer;
			res = TRUE;
		} else if ((xbi->xfdbi_PackerFlags & XFDPFF_KEY16)) {
			ULONG key;
			while (TRUE) {
				text_prompt("Enter 16 bit key (dec/hex): ", buffer.password, 100);
				if (parse_key(buffer.password, &key)) break;
				IDOS->PutErrStr("Bad key! Try again...\n");
			}
			buffer.key16 = key;
			xbi->xfdbi_Special = &buffer;
			res = TRUE;
		} else {
			ULONG key;
			while (TRUE) {
				text_prompt("Enter 32 bit key (dec/hex): ", buffer.password, 100);
				if (parse_key(buffer.password, &key)) break;
				IDOS->PutErrStr("Bad key! Try again...\n");
			}
			buffer.key32 = key;
			xbi->xfdbi_Special = &buffer;
			res = TRUE;
		}
	}

	IExec->DropInterface((struct Interface *)IXpk);
	IExec->CloseLibrary(XpkBase);

	return res;
}

static void text_prompt(const char *prompt, char *buffer, LONG buflen) {
	LONG promptlen = strlen(prompt);
	LONG inputlen;

	buflen--;

	do {
		IDOS->Write(IDOS->Output(), prompt, promptlen);
		inputlen = IDOS->Read(IDOS->Input(), buffer, buflen);
	} while (inputlen <= 0);

	buffer[inputlen--] = '\0';
	if (buffer[inputlen] == '\n') buffer[inputlen] = '\0';
}

static BOOL parse_key(const char *keystr, ULONG *result) {
	ULONG keyval = 0;
	BOOL sign = FALSE;
	char ch, digit;

	while (TRUE) {
		ch = *keystr++;
		if (ch == '\0') return FALSE;
		if (ch == '\t' || ch == '\x14') continue;
		break;
	}

	if (ch == '-') {
		sign = TRUE;
		ch = *keystr++;
		if (ch == '\0') return FALSE;
	}

	if (ch == '$') {
		ch = *keystr++;
		if (ch == '\0') return FALSE;

		do {
			if (ch >= '0' && ch <= '9') digit = ch - '0';
			else if (ch >= 'A' && ch <= 'F') digit = ch - ('A' - 10);
			else if (ch >= 'a' && ch <= 'f') digit = ch - ('a' - 10);
			else return FALSE;

			keyval = (keyval << 4) | digit;

			ch = *keystr++;
		} while (ch != '\0');
	} else {
		do {
			if (ch >= '0' && ch <= '9') digit = ch - '0';
			else return FALSE;

			keyval = (keyval * 10) + digit;

			ch = *keystr++;
		} while (ch != '\0');
	}

	*result = (sign) ? (-keyval) : (keyval);
	return TRUE;
}

/* To be used for LINKSIMPLE and LINKPRO */
static ULONG link_data[0x1d0] = {
	0x0064006a, 0x00b40000, 0x00000130, 0x000003f3,
	0x00000000, 0x00000002, 0x00000000, 0x00000001,
	0x0000003f, 0x00000000, 0x000003e9, 0x0000003f,
	0x60160000, 0x0000abcd, 0x00000000, 0x00000000,
	0x00000000, 0x00000000, 0x48e7f0c2, 0x43fa00a6,
	0x70212c78, 0x00044eae, 0xfdd841fa, 0xffd42080,
	0x677643f9, 0x12345678, 0x203c1234, 0x56784eae,
	0xff344a80, 0x660841fa, 0x00886164, 0x604e223a,
	0xffb8243c, 0x00000130, 0x76ff2c7a, 0xffa44eae,
	0xffbe223a, 0xffa4243a, 0xffcc263a, 0xffce4eae,
	0xffd6b680, 0x670841fa, 0x006e6134, 0x600e4cdf,
	0x430f4eb9, 0x12345678, 0x4fefffe4, 0x227affa6,
	0x203affa8, 0x2c780004, 0x4eaeff2e, 0x227aff62,
	0x2c780004, 0x4eaefe62, 0x4fef001c, 0x70004e75,
	0x76001618, 0x24082c7a, 0xff484eae, 0xffc42200,
	0x4eeeffd0, 0x646f732e, 0x6c696272, 0x61727900,
	0x154e6f20, 0x6d656d6f, 0x72792061, 0x7661696c,
	0x61626c65, 0x2e0a1445, 0x72726f72, 0x206c6f61,
	0x64696e67, 0x20646174, 0x612e0a00, 0x000003f2,
	0x000003f5, 0x00000000, 0x000003f6, 0x00360062,
	0x00400024, 0x0000000e, 0x0000005c, 0x000003f3,
	0x00000000, 0x00000001, 0x00000000, 0x00000000,
	0x00000000, 0x000003e9, 0x00000000, 0x48e780c0,
	0x43f91234, 0x567841fa, 0x002c203c, 0x12345678,
	0xb1c96714, 0x650822d8, 0x538066fa, 0x600ad1c0,
	0xd3c02320, 0x538066fa, 0x4cdf0301, 0x4ef91234,
	0x56784e71, 0x000003f2, 0x24564552, 0x3a207866,
	0x64446563, 0x72756e63, 0x6820312e, 0x39202830,
	0x392e3039, 0x2e313939, 0x39290d0a, 0x00000000
};

#define PRO_HEADER    0x0
#define SIMPLE_HEADER 0x13c
#define SIMPLE_FOOTER 0x1a4

static BOOL write_file(struct global_data *gd, const char *fn, APTR buffer, ULONG buflen) {
	struct xfdBufferInfo *xbi = gd->xbi;
	BOOL res = FALSE;
	BPTR fh = ZERO;

	IDOS->Printf("Writing \"%s\" (%lu bytes)...\n", fn, buflen);

	if (!gd->args[ARG_FORCE]) {
		fh = IDOS->Open(fn, MODE_OLDFILE);
		if (fh != ZERO) {
			IDOS->Close(fh);

			if (!yesno_prompt("File already exists. Overwrite (Y/N)? ")) {
				IDOS->PutStr("Skipping file...\n");
				return TRUE; /* Not an error */
			}
		} else {
			LONG error = IDOS->IoErr();
			if (error != ERROR_OBJECT_NOT_FOUND) goto Error;
		}
	}

	fh = IDOS->Open(fn, MODE_NEWFILE);
	if (fh == ZERO) goto Error;

	if ((xbi->xfdbi_PackerFlags & XFDPFF_ADDR)) {
		struct xfdMasterIFace *IXfd = gd->ixfd;

		if (READ_ULONG(buffer, 0) == HUNK_HEADER && IXfd->xfdTestHunkStructure(buffer, buflen)) {
			IDOS->PutStr("File seems to be executable...\n");
			if (IDOS->Write(fh, buffer, buflen) != buflen) goto Error;
		} else if (gd->args[ARG_LINKSIMPLE]) {
			APTR data;
			WORD offset;
			ULONG longs;
			LONG len;

			IDOS->PutStr("Linking simple executable header...\n");

			buflen = (buflen + 3) & ~3;

			data = (APTR)link_data + SIMPLE_HEADER;
			offset = READ_WORD_INC(data);
			WRITE_ULONG(data, offset - 2, xbi->xfdbi_DecrAddress);
			offset = READ_WORD_INC(data);
			WRITE_ULONG(data, offset - 4, xbi->xfdbi_JmpAddress);
			offset = READ_WORD_INC(data);
			longs = buflen >> 2;
			WRITE_ULONG(data, offset - 6, longs);
			offset = READ_WORD_INC(data);
			longs += READ_ULONG_INC(data);
			WRITE_ULONG(data, offset - 12, longs);
			WRITE_ULONG(data, offset - 4, longs);
			len = READ_LONG_INC(data);
			if (IDOS->Write(fh, data, len) != len) goto Error;

			if (IDOS->Write(fh, buffer, buflen) != buflen) goto Error;

			data = (APTR)link_data + SIMPLE_FOOTER;
			len = 4;
			if (IDOS->Write(fh, data, len) != len) goto Error;
		} else if (gd->args[ARG_LINKPRO]) {
			APTR data;
			WORD offset;
			LONG len;

			IDOS->PutStr("Linking professional executable header...\n");

			data = (APTR)link_data + PRO_HEADER;
			offset = READ_WORD_INC(data);
			WRITE_ULONG(data, offset - 2, xbi->xfdbi_DecrAddress);
			offset = READ_WORD_INC(data);
			WRITE_ULONG(data, offset - 4, buflen);
			offset = READ_WORD_INC(data);
			WRITE_ULONG(data, offset - 6, xbi->xfdbi_JmpAddress);
			data += 2;
			len = READ_LONG_INC(data);
			if (IDOS->Write(fh, data, len) != len) goto Error;

			if (IDOS->Write(fh, buffer, buflen) != buflen) goto Error;
		} else {
			IDOS->Printf("Saving relocated image (Addr: $%08lx, Jmp: $%08lx)...\n",
				xbi->xfdbi_DecrAddress, xbi->xfdbi_JmpAddress);
			if (IDOS->Write(fh, buffer, buflen) != buflen) goto Error;
		}
	} else {
		if (IDOS->Write(fh, buffer, buflen) != buflen) goto Error;
	}

	res = TRUE;

Error:
	IDOS->Close(fh);

	if (!res) IDOS->PrintFault(IDOS->IoErr(), fn);

	return res;
}

static BOOL yesno_prompt(const char *prompt) {
	char buffer[5], answer;
	while (TRUE) {
		text_prompt(prompt, buffer, 5);
		answer = buffer[0];
		if (answer == 'Y' || answer == 'y') return TRUE;
		if (answer == 'N' || answer == 'n') return FALSE;
	}
}

