/* pp_patcher v1.0
**
** pp_patcher is a small utility which enables programs to get at
** PowerPacked files, as if these were normal files. This is acomplished
** by patching some vital DOS functions, thus redirecting calls to these to
** my own internal routines. The overall effect is that PowerPacked data-
** files seem to be normal files. You can TYPE them in a CLI window, or
** bring them directly into your favourite editor. Another good way to
** use this proggy is if you crunch your workbench icons. Workbench will
** never know the difference, but it reduces the diskspace occupied by
** icons by some 65-70% (usually). There is a nominal performance reduction
** due to the fact that we have to decrunch the icons before passing them
** on to Workbench, but this doesn't seem too annoying. Especially not if
** you use optimized (B.A.D.) disks, or the CLI command Addbuffers or
** equivalent.
**
** For further info, read the DOC file.
**
** Compiles under Aztec V5.0 using large code/large data/32 bit integers
** Should do fine under Lattice, too, if you fix the #pragma's.
**
** Shareware 1991, copyright (C) 1991 by Michael Berg
**					 Sct. Peders Gade 24A, 2th
**					 8900  Randers
**					 DENMARK
*/

/* That's right -- ALL of these are necessary! */
#include <functions.h>
#include <exec/nodes.h>
#include <exec/lists.h>
#include <exec/memory.h>
#include <libraries/dos.h>
#include <libraries/dosextens.h>
#include <intuition/intuition.h>
#include "ppbase.h"			/* enclosed in this directory */

#asm
DOSLibPatch MACRO
	public	_Real\1
	public	_New\1
	public	_Make\1
	public	_Rest\1
	public	_LVO\1
	public	\2
	cseg

_Make\1				;make a doslib patch
	move.l	a6,-(sp)	;save regs
	move.l	\2,a6		;get at the dosbase
	lea	_LVO\1(a6),a6	;jump vector address
	moveq	#0,d0
	move.w	(a6),Orig\1	;fetch the 'moveq #?,d0' opcode
	move.w	4(a6),d0	;fetch the 'bra' offset
	add.l	a6,d0		;add to find branch target
	addq.l	#4,d0		;pc relative offset compensation
	move.l	d0,Orig\1+4	;we need this so we can jump directly
	move.w	#$4ef9,(a6)	;initiate new sequence: jmp $abs_addrs
	move.l	#_New\1,2(a6)	;jump to our new function, of corz!
	move.l	(sp)+,a6	;restore regs
	rts

	dseg
	ds	0
_Real\1
Orig\1
	dc.w	0		;this will be replaced by a moveq
	dc.w	$4ef9,0,0	;the JMP address will be fixed

	cseg
_Rest\1				;restore a doslib patch
	tst.l	Orig\1+4	;did we make it at all?
	beq.s	1$		;nope, don't do anything
	move.l	a6,-(sp)	;save regs
	move.l	\2,a6		;get at the dosbase
	lea	_LVO\1(a6),a6	;find jump vector address
	move.l	Orig\1+4,d0	;calculate a bra offset to the orig. addrs
	subq.l	#4,d0		;pc relative offset compensation
	sub.l	a6,d0		;subtract base address
	move.w	Orig\1,(a6)	;restore the moveq opcode
	move.w	#$6000,2(a6)	;opcode for 'bra.l'
	move.w	d0,4(a6)	;save the branch offset
	move.l	(sp)+,a6	;restore registers
1$	rts
	ENDM

;DOSLibPatch is a macro which defines three functions. For Open(), they are:
;
;MakeOpen	- Patch the DOS library Open() vector
;RestOpen	- Restore the DOS library to it's original state
;RealOpen	- This calls the original DOS function
;
;Furthermore, you yourself have to create a function called (in this case)
;NewOpen, with a parameter specification like Open(). Future calls to Open
;will be redirected to NewOpen. NewOpen has access to the original DOS code
;through the function RealOpen. RealOpen should also be defined like Open.
;More details follow. For now, you need to know that DOSLibPatch works only
;with the DOS library, which differes in format from other libraries.

	DOSLibPatch Open,_DOSBase
	DOSLibPatch Close,_DOSBase
	DOSLibPatch Examine,_DOSBase
	DOSLibPatch Write,_DOSBase
#endasm

/* Well, the compiler has the inline-code ability -- why not use them? */
#define strlen	_BUILTIN_strlen
#define strcpy	_BUILTIN_strcpy

/* Some defines which make the source code look pretty */
typedef struct IntuitionBase INTUIBASE;
typedef struct GfxBase GFXBASE;
typedef struct PPBase PPBASE;

/* A tiny window enables the user to control the program */
struct NewWindow nw =
{
	0,0,320,10,
	-1,-1,
	CLOSEWINDOW,
	WINDOWDRAG | WINDOWCLOSE | WINDOWDEPTH | ACTIVATE,
	NULL,NULL,
	(UBYTE *)"Powerpacker Patcher V1.0",
	NULL,NULL,
	0,0,0,0,
	WBENCHSCREEN
};

/* powerpacker.library related functions */
extern int ppLoadData(char *, int, int, UBYTE **, int *, int (*)());
#pragma amicall(PPBase, 0x1e, ppLoadData(a0,d0,d1,a1,a2,a3))

/* Open()-patch related functions */
extern void MakeOpen(void), RestOpen(void);
extern BPTR RealOpen(char *, int);
extern BPTR NewOpen(char *, int);
#pragma regcall(RealOpen(d1,d2))	/* All of these PRAGMAs are EXTREMELY */
#pragma regcall(NewOpen(d1,d2))		/* important !!!!!!!!!!		      */

/* Close()-patch related functions */
extern void MakeClose(void), RestClose(void);
extern void RealClose(BPTR);
extern void NewClose(BPTR);
#pragma regcall(RealClose(d1))
#pragma regcall(NewClose(d1))

/* Examine()-patch related functions */
extern void MakeExamine(void), RestExamine(void);
extern int RealExamine(BPTR, struct FileInfoBlock *);
extern int NewExamine(BPTR, struct FileInfoBlock *);
#pragma regcall(RealExamine(d1,d2))
#pragma regcall(NewExamine(d1,d2))

/* Write()-patch related functions */
extern void MakeWrite(void), RestWrite(void);
extern int RealWrite(BPTR, char *, int);
extern int NewWrite(BPTR, char *, int);
#pragma regcall(RealWrite(d1,d2,d3))
#pragma regcall(NewWrite(d1,d2,d3))

/* Define the maximum length of a filename and its path
** 256 is, as far as I can tell, the AmigaDOS limit (due to BSTR's).
*/
#define MAXPATHLEN	256

/* Match tags for PowerPacked files */
#define PP20		(('P' << 24) + ('P' << 16) + ('2' << 8) + '0')
#define PX20		(('P' << 24) + ('X' << 16) + ('2' << 8) + '0')

/* One of these for each opened, powerpacked file, which has not yet been
** closed
*/
struct filenode
{
	struct MinNode mn;

	BPTR filehandle;
	char *ram_filename, *orig_filename;
	short dirty;
};

/* One of these for each caller to NewOpen() */
struct caller
{
	struct MinNode mn;

	struct Task *tc;
};

/* Global data */
struct MinList templist, callers;
struct Window *win;
struct IntuitionBase *IntuitionBase;
struct GfxBase *GfxBase;
struct PPBase *PPBase;
int patched;

/* Exported to detach module */
int _stack = 4000;
int _priority = 5;
char *_procname = "Powerpacker Patcher";
BPTR _BackGroundIO;

/* Universal termination code */
void die(int errcode)
{
	/* Restore the original DOS functions. You will not find these
	** functions in the sourcecode. They are generated by the assembler
	** macro DOSLibPatch -- see above
	*/
	if (patched)
	{
		Forbid();
			RestOpen();
			RestClose();
			RestExamine();
			RestWrite();
		Permit();
	}

	/* Close the window */
	if (win) CloseWindow(win);

	/* Close libraries */
	if (IntuitionBase)	CloseLibrary(IntuitionBase);
	if (GfxBase)		CloseLibrary(GfxBase);
	if (PPBase)		CloseLibrary(PPBase);

	/* Finally! */
	exit(errcode);
}

/* Open up all required libraries */
void openlibs()
{
  if
  (
    !(GfxBase	    =   (GFXBASE *)OpenLibrary("graphics.library",   0)) ||
    !(IntuitionBase = (INTUIBASE *)OpenLibrary("intuition.library",  0)) ||
    !(PPBase	    =    (PPBASE *)OpenLibrary("powerpacker.library",0))
  )
	/* Graphics & Intuition should NEVER fail to open */
	die(10);
}

/* Attempt to open the window */
void openwindow()
{
	if (!(win = OpenWindow(&nw)))
		die(20);
}

/* Install the DOS patches */
void installpatch()
{
	Forbid();
		MakeOpen();
		MakeClose();
		MakeExamine();
		MakeWrite();
	Permit();

	patched = 1;
}

/* Some Lists need to be initialized */
void initlist()
{
	NewList((struct List *)&templist);
	NewList((struct List *)&callers);
}

/* Open up everything */
void openstuff()
{
	openlibs();
	openwindow();
	initlist();
	installpatch();
}

/* Add a new caller to NewOpen() to the list of callers */
addcaller(register struct Task *tc)
{
	register struct caller *memgot;

	if (memgot = AllocMem(sizeof(*memgot),0))
	{
		memgot->tc = tc;
		AddTail((struct List *)&callers, (struct Node *)memgot);
		return(1);
	}
	else
		return(0);
}

/* Add a file node to the list of files which we have created. These
** exist temporarily in RAM: and we need to get rid of these along the way,
** as they are Close()'d.
*/
addfilenode(register BPTR fn, register char *filename, register char *orig)
{
	register struct filenode *memgot;

	if
	(
		(memgot = AllocMem(sizeof(*memgot),MEMF_CLEAR))		 &&
		(memgot->ram_filename  = AllocMem(strlen(filename)+1,0)) &&
		(memgot->orig_filename = AllocMem(strlen(orig)+1,0))
	)
	{
		memgot->filehandle = fn;
		strcpy(memgot->ram_filename,filename);
		strcpy(memgot->orig_filename,orig);
		AddTail((struct List *)&templist, (struct Node *)memgot);
		return(1);
	}
	else
		return(0);
}

/* Find a filenode (keyed by its filehandle) */
struct filenode *findfilenode(register BPTR fn)
{
	register struct filenode *search;

	/* Linear search is employed */
	for
	(
		search = (struct filenode *)(templist.mlh_Head);
		search->mn.mln_Succ;
		search = (struct filenode *)(search->mn.mln_Succ)
	)
		if (search->filehandle == fn)
			return(search);

	return(NULL);
}

/* Find a caller on the callers list */
struct caller *findcaller(register struct Task *tc)
{
	register struct caller *search;

	for
	(
		search = (struct caller *)(callers.mlh_Head);
		search->mn.mln_Succ;
		search = (struct caller *)(search->mn.mln_Succ)
	)
		if (search->tc == tc)
			return(search);

	return(NULL);
}

/* This baby builds a complete filename (including a path) from a BCPL
** pointer to a filehandle. Optimizations are most welcome. All those
** ParentDir() calls take a LONG time.
*/
char *fullname(register BPTR lock, register struct FileInfoBlock *fib)
{
	static char pathandfile[MAXPATHLEN];
	char tmp[MAXPATHLEN];
	register BPTR parentlock, unlocklock;
	register char *co;

	strcpy(pathandfile,fib->fib_FileName);

	parentlock = lock;
	unlocklock = (BPTR)0;
	while (parentlock=ParentDir(parentlock))
	{
		if (unlocklock)
			UnLock(unlocklock);

		if (RealExamine(parentlock,fib))
		{
			strcpy(tmp,fib->fib_FileName);
			strcat(tmp,"/");
			strcat(tmp,pathandfile);
			strcpy(pathandfile,tmp);
		}
		else
		{
			UnLock(parentlock);
			return(NULL);
		}

		unlocklock = parentlock;
	}

	if (unlocklock)
		UnLock(unlocklock);

	if (co = (char *)index(pathandfile,'/'))
		*co = ':';

	/* This fixes a bug in the old RAM disk */
	if (!strcmp(pathandfile,":"))
		strcpy(pathandfile,"RAM:");

	return(pathandfile);
}

/* Is a filename really a file? We need to know this, because it is
** rediculous to try to read in the PowerPacker matchtag from something
** like CON:0/0/544/23/ConWindow.
*/
reallyfile(register char *filename)
{
	/* Don't like to hard-wire it like this, but it seems to
	** be the only realistic approach. Don't worry, you can't
	** do an ASSIGN to any of these. If you could, we would have
	** to check all volumenodes on the DeviceList stored in
	** the RootNode of the DosLibrary. (Got that?!)
	*/
	static char *duds[] =
	{
		"NIL:",	"CON:",
		"RAW:",	"PRT:",
		"PAR:",	"SER:"
	};

	register short i;

	/* A simple, linear search is employed */
	for (i = 0; i < sizeof(duds)/sizeof(char *); i++)
	{
		register short foundit;
		register char *cmp, *cmp2;
		register short j;

		cmp  = duds[i];
		cmp2 = filename;

		for (foundit = 1, j = 0; j < 4; j++)
		{
			if (toupper(*cmp++) != toupper(*cmp2++))
			{
				foundit = 0;
				break;
			}
		}

		if (foundit)
			return(0);
	}

	/* One last check, just to be sure */
	if (!strcmp(filename,"*"))
		return(0);

	return(1);
}

/* When somebody opens a PP file, we decrunch it into a temporary RAM file
** and return a filehandle to that file. When the caller closes the file
** (which it thinks is the original disk file), it will really be closing
** the temporary RAM file. This is a good chance for us to get rid of it,
** so that RAM: won't get crowded in time. HOWEVER! If the caller has
** written new data into the file, we have to rewrite the temporary file
** over the original (disk) file. flushout() does exactly that.
*/
flushout(register struct filenode *fn)
{
	register BPTR orighandle;

	/* First of all, we have to open the original file. We're in
	** trouble if this is not possible...
	*/
	if (orighandle = RealOpen(fn->orig_filename, MODE_NEWFILE))
	{
		char buffer[4096];	/* should suffice when copying  */
		register short readlen;	/* from a fast device like RAM: */

		Seek(fn->filehandle, 0, -1);

		do
		{
			readlen = Read(fn->filehandle, buffer, 4096);
			RealWrite(orighandle, buffer, readlen);
		}
			while (readlen == 4096);

		RealClose(orighandle);
	}
}

/* This is the new Open() functions. All future calls to the DOS Open()
** function will be rerouted through here.
*/
BPTR NewOpen(register char *filename, register mode)
{
	UBYTE *memgot;
	int filelen;
	register BPTR tempfh = (BPTR)0;
	register struct Task *thistask;
	register struct caller *thiscaller;

	/* We only deal with a few of the incoming calls:
	**
	** 1) We can't do anything about new files
	** 2) Equally, we don't care about CON: or NIL: file open requests
	** 3) If we have seen the calling task before, the one who is making
	**    the request must be ppLoadData. It is absolutely vital that
	**    we forward this request to the original DOS code. Otherwise
	**    we would end up in an infinite (recursive) loop, with ppLoadData
	**    calling NewOpen calling ppLoadData ...
	** 4) If we cannot add a caller to the list of callers (see 3), we
	**    ignore the call. If we miss a few in a low memory situation,
	**    so be it.
	*/

	if
	(
		mode == MODE_NEWFILE		   ||
		!reallyfile(filename)		   ||
		findcaller(thistask = FindTask(0)) ||
		!addcaller(thistask)
	)
		return(RealOpen(filename,mode));

	/* Now, ask ppLoadData to bring in the file */
	if (!ppLoadData(filename,DECR_NONE,0,&memgot,&filelen,(int (*)())0))
	{
		char filnambuf[40];
		register char *t, *m;

		/* Generate a name for the temporary file in RAM */
		t = filename;
		if (m = (char *)index(t,':')) t = m+1;
		while (m = (char *)index(t,'/')) t = m+1;

		strcpy(filnambuf,"RAM:");
		strcat(filnambuf,t);
		strcat(filnambuf,".tmp");

		/* We have to ensure that the name is unique on RAM: */
		while (tempfh = RealOpen(filnambuf,MODE_OLDFILE))
		{
			char *xtra = "?";

			/* Pad the name with random characters. This
			** should do the trick
			*/
			RealClose(tempfh);
			*xtra = 'A' + (rand() % 26);
			strcat(filnambuf,xtra);
		}

		/* Now, open the RAM file and flush data we loaded into
		** this file.
		*/
		if (tempfh = RealOpen(filnambuf,MODE_NEWFILE))
		{
			/* Remember that WE created that file */
			if (!addfilenode(tempfh,filnambuf,filename))
			{
				/* Couln't do it. Simulate a "Can't open
				** file" from the real Open().
				*/
				RealClose(tempfh);
				DeleteFile(filnambuf);
				tempfh = (BPTR)0;
			}
			else
			{
				/* Flush out the file. Probably should do
				** a check on RealWrite... Oh well.
				*/
				RealWrite(tempfh,(char *)memgot,filelen);
				Seek(tempfh,0,-1);
			}
		}
		/* Housekeeping */
		FreeMem(memgot,filelen);
	}

	/* We no longer have to worry about this caller */
	thiscaller = findcaller(thistask);
	Remove((struct Node *)thiscaller);
	FreeMem(thiscaller, sizeof(*thiscaller));

	/* Return a filehandle to the file */
	return(tempfh);
}

/* Yep! A new Write() function. We have to know if a process has updated
** the (substitute) file we created for it. If true, mark the file as
** being "dirty", so that we can later save it over the original PP file.
*/
int NewWrite(register BPTR filehandle, register char *buffer, int length)
{
	register struct filenode *fn;
	register wrtret;

	wrtret = RealWrite(filehandle, buffer, length);

	if (fn = findfilenode(filehandle))
		fn->dirty = 1;

	return(wrtret);
}

/* A new Close() function. It removes non-dirty, temporary files from
** RAM, and it keeps track of which files have to be updated back onto
** disk.
*/
void NewClose(register BPTR filehandle)
{
	register struct filenode *fn;

	if (fn = findfilenode(filehandle))
	{
		if (fn->dirty)
			flushout(fn);

		RealClose(filehandle);
		DeleteFile(fn->ram_filename);
		Remove((struct Node *)fn);
		FreeMem(fn,sizeof(*fn));
	}
	else
		RealClose(filehandle);
}

/* A new Examine() function. Often, programs examine a file before opening
** it. This way, they can allocate just enough memory to hold the entire
** file. However, we have to correct Examine() calls to PowerPacked files,
** so that the correct amount of memory will be allocated by the caller.
*/
int NewExamine(BPTR lock, struct FileInfoBlock *fib)
{
	int ppmatchtag;
	int decrunchinfo;
	register examinereturn;
	register BPTR tmpfh;
	struct FileInfoBlock fib_backup;

	/* Start off by examining the lock */
	if (!(examinereturn = RealExamine(lock,fib)))
		return(0);

	/* If it's a directory, or if it's pp_LoadData, never mind */
	if (fib->fib_DirEntryType >= 0 || findcaller(FindTask(0)))
		return(examinereturn);

	/* The lock target was a simple file. Check to see if it's a
	** PP file
	*/
	fib_backup = *fib;
	tmpfh	   = RealOpen(fullname(lock,fib),MODE_OLDFILE);
	*fib	   = fib_backup;

	if (!tmpfh)
		return(examinereturn);

	Seek(tmpfh,0,-1);
	Read(tmpfh,(char *)&ppmatchtag,sizeof(int));

	if (ppmatchtag == PP20 || ppmatchtag == PX20)
	{
		/* It was. Examine decrunchinfo to get at the original
		** filesize, so that programs trying to allocate enough
		** memory to hold a certain file will get the correct
		** filesize (which is, of corz, size of the decrunched
		** file!)
		*/
		Seek(tmpfh,-4,1);
		Read(tmpfh,(char *)&decrunchinfo,sizeof(int));
		fib->fib_Size = decrunchinfo >> 8;
	}

	RealClose(tmpfh);

	return(examinereturn);
}

/* Hangaround() simply waits forever for a message from Intuition. When we
** get one, it can only be a CLOSEWINDOW request.
*/
hangaround()
{
	WaitPort(win->UserPort);
	GetMsg(win->UserPort);
}

/* Entry point */
void main()
{
	openstuff();
	hangaround();
	die(0);
}
