;-------------------------------------------------------------------------------
; EXC -- Convert EXE file to COM file
;
; (c) Copyright 1998 by K. Heidenstrom.
;
; This program is free software.  You may redistribute it and/or
; modify it under the terms of the GNU General Public License as
; published by the Free Software Foundation; either version 2 of
; the License, or (at your option) any later version.
;
; 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.	In no
; event will the author be liable for any damages of any kind
; related to the use of this program.  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., 675 Mass Ave, Cambridge, MA 02139, USA.
;
; If you find any bugs or make significant improvements, please
; consider sending the details to the author so that other users
; may benefit.	Contact details are:
;
; Email:  kheidens@clear.net.nz
; Web:	  http://home.clear.net.nz/pages/kheidens
; Snail:  K. Heidenstrom c/- P.O. Box 27-103, Wellington, New Zealand
;
;-------------------------------------------------------------------------------
;
; Modified:
;
; KH.19980817.001  0.0.0  Started
; KH.19980818.002  Fixup stub written
; KH.19980823.003  1.0.0  First working version, various optimisations in stub

Ver		EQU	1
SubVer		EQU	0
ModVer		EQU	0
VerDate		EQU	"19980823"		; YYYYMMDD format

;-------------------------------------------------------------------------------
;
; This program must be assembled with Borland's TASM.  The syntax is:
;
;	tasm /ml /t /w2 exc;
;	tlink /t exc, exc, nul
;
;-------------------------------------------------------------------------------

		PAGE	,132
		IDEAL
		%TITLE	"EXC -- Convert EXE file to COM file"
		NAME	EXC

; Documentation ----------------------------------------------------------------
;
; This program converts an MS-DOS EXE file to a COM file.  It is mainly for use
; with my ARK program, which allows you to create executable archives of COM
; files.  ARK cannot archive EXE files directly, so EXE files must be converted
; to COM files for use with ARK.
;
; EXC cannot convert Windows 3.x, Windows 95/98, Windows NT or OS/2 executables
; to COM format.  Those types of executables cannot exist in COM format at all.
;
; There are several other restrictions on what files can be converted from EXE
; format to COM format by EXC.	Programs with overlays will not work properly
; if converted, and there is a size limit of slightly less than 64K on the
; resulting COM file, which includes the load image, the unpacker stub and the
; packed relocation data, so if the EXE file is larger than about 60K it
; probably cannot be converted (this program issues a message in this case).
;
; Programs which check and/or modify their image on disk will not work properly
; if converted to COM files, because the program's image on disk is not what the
; program expects to see.  This applies to programs which validate their disk
; images as a crude protection against viruses (these programs will report that
; they have been tampered with, which of course is true), programs which contain
; overlays (mentioned in the previous paragraph) and programs which are able to
; update default settings by writing new settings to the executable (these will
; fail or possibly even corrupt themselves if they try to do this).
;
; EXC is very similar in function to Microsoft's old EXEPACK utility, which was
; used to compress the relocation table in some large programs.  The differences
; are that EXEPACK leaves the file as an EXE file and does not have a 64K limit,
; whereas EXC converts the EXE file to a COM file, and does have a 64K limit.
;
; EXC is _not_ like EXE2BIN (or EXE2COM, Chris Dunford's EXE2BIN replacement).
; EXE2BIN and EXE2COM are used to convert a program which is temporarily in EXE
; format, to its proper COM format, as part of the program compilation sequence.
; EXC converts a functional program which is supposed to be an EXE file, into a
; COM file.
;
; You could also compare EXC to executable file compressors such as PKLITE and
; LZEXE, but EXC does not compress the load image, so typically the converted
; file is a similar size to the original EXE file.  The converted file may be
; slightly larger or somewhat smaller, depending mainly on the number of entries
; in the relocation table in the EXE header.
;
; The main reason you would use EXC is to enable you to include EXE files in ARK
; archives (and also in archives using XEQ, the program that ARK was based on).
;
; Using EXC with EXEPACKed files and compressed executables
; ---------------------------------------------------------
;
; Running EXC on a file that has been packed by Microsoft's old EXEPACK utility
; or compressed by an executable compressor such as PKLITE, LZEXE or DIET will
; produce a COM file that is slightly larger than the EXE file.  EXC gets best
; performance on an unpacked, uncompressed executable.
;
; For EXEPACKed files, it is better to unpack the file before converting it.
; This will usually produce a COM file that is smaller than the EXEPACKed EXE
; file.  I believe there are some freeware/shareware EXEPACK unpackers around.
; V Communications also used to sell one.
;
; For executable-compressed files, it is also better to unpack the file before
; converting it.  You can then run the executable compressor on the COM file,
; or if you are going to include the file in an ARK archive, leave it in its
; uncompressed state, and run the executable compressor on the ARK file.  This
; will give better compression of the ARK file because the executable compressor
; will not be trying to compress its own output.  If you use this method, and
; you later want to extract the file, you will extract an uncompressed file,
; which could then be compressed by an executable compressor if desired.
;
; The first 28 (1C hex) bytes of an EXE header are formatted as follows.
;
; Offset  Description
;    00h  Signature (always "MZ")
;    02h  Number of bytes in last 512-byte page
;    04h  Total number of 512-byte pages required to hold file
;    06h  Number of entries in relocation table
;    08h  Size of the EXE header, in paragraphs
;    0Ah  Minimum memory required above load image, in paragraphs
;    0Ch  Maximum memory to allocate (in paragraphs, often 0FFFFh)
;    0Eh  Pre-relocation SS register value
;    10h  Initial SP register value (normally equal to stack size)
;    12h  Negative checksum (ignored by DOS)
;    14h  Initial IP register value
;    16h  Pre-relocation CS register value
;    18h  Relocation table offset into header
;    1Ah  Overlay number
;
; The layout of the converted file is:
;
; 0100-0102	Three-byte JMP to fixup stub
; 0103-xxxx	Original load image, minus the first three bytes
; xxxx-xxxx	Fixup stub code
; xxxx-xxxx	Packed fixup (relocation) data
;
; Program requirements
;
; CPU:	   8086 or later
; Memory:  64K free system memory
; DOS:	   2.0 or later
;
; At various places I have written instructions using DBs and opcode values.
; This is because Turbo Assembler (version 3.1) has some weird ideas about
; possible maximum instruction sizes, and sometimes thinks an 'add reg,nnnn'
; instruction might be four bytes long (!) then generates an 'instruction can
; be compacted with override' warning when it discovers it isn't.  The only
; way I know of to avoid these warnings is to construct the instruction using
; DBs and DWs.
;
; ------------------------------------------------------------------------------

; Equates

AppName		EQU	"EXC"

MinRunningSP	=	0FFF0h		; Must have almost 64K of memory
MaxHdrParas	=	63*1024/16	; Max total paragraphs in header
MaxRelocEnts	=	60*1024/4	; Maximum relocation entries to load
MaxImageSize	=	60*1024		; Maximum load image size
MemLimit	=	0FE00h		; Maximum memory usable by this program
					; Allows about 512 bytes stack at top
StubStack	=	512		; Minimum stack bytes required by stub

; Offsets into memory allocation arena header

ARNA_Type	=	0		; Byte - 'M' (not last) or 'Z' (last)
ARNA_Para	=	1		; Word - paragraph of controlled block
ARNA_Size	=	3		; Word - number of paragraphs in block

;-------------------------------------------------------------------------------

		SEGMENT	Code
		ASSUME	cs:Code,ds:Code,es:nothing,ss:nothing

		ORG	100h		; This program is a COM file

PROC	Main0		near
		jmp	Main1
ENDP	Main0

Signature	DB	13,AppName," V",Ver+"0",".",SubVer+"0","."
		DB	ModVer+"0",", ",VerDate,"; "
;---------------
;!! Change the following line if you create a derivative version of this program
		DB	"Original"
;---------------
		DB	26		; Ctrl-Z

; Transient messages -----------------------------------------------------------

DOSVersMsg	DB	AppName,": Requires DOS 2.0 or later",13,10,"$"
MemoryEM	DB	162,AppName,": Insufficient memory",13,10,0
UsageEM		DB	255,13,10,AppName," --",9,"EXE to COM file converter  Version ",Ver+"0",".",SubVer+"0",".",ModVer+"0",", ",VerDate
		DB	13,10,9,"(c) Copyright 1998 by K. Heidenstrom (kheidens@clear.net.nz)"
		DB	13,10
		DB	13,10,"Usage:",9,AppName," Pathname[.EXE]"
		DB	13,10,0
CantOpenInEM	DB	17,AppName,": Unable to open input file",13,10,0
ReadErrEM	DB	20,AppName,": Read error on input file",13,10,0
NotAnExeEM	DB	22,AppName,": Input file is not a valid EXE file",13,10,0
TooLargeEM	DB	19,AppName,": Input file is too large to be converted",13,10,0
OverlayM	DB	AppName,": Warning: EXE file may contain overlays",13,10,0
CantOpenOutEM	DB	33,AppName,": Unable to open output file",13,10,0
WriteErrEM	DB	37,AppName,": Write error on output file",13,10,0
ConvertedEM	DB	0,AppName,": Program has been converted",13,10,0

; Transient tables -------------------------------------------------------------

		ALIGN	2

; First 26 bytes of EXE header from file to be converted

ExeHeader	= $
ExeSig		DB	0,0		; Header signature - should be "MZ"
LastPgSize	DW	0		; Bytes in last page (512-byte) of file
FilePages	DW	0		; Number of pages required to hold file
RelocEntries	DW	0		; Number of relocation table entries
HeaderParas	DW	0		; Header size (in paragraphs)
MinAlloc	DW	0		; Min mem (paras) req'd above load image
MaxAlloc	DW	0		; Max memory (paras) to allocate above
StackSegVal	DW	0		; Pre-relocation SS register contents
StackPtrVal	DW	0		; Initial SP register value
		DW	0		; Negative checksum
InstrPtrVal	DW	0		; Initial IP register value
CodeSegVal	DW	0		; Pre-relocation CS register contents
RelocOff	DW	0		; Relocation table offset into header
ExeHdrLen	=	$ - ExeHeader

; Variables --------------------------------------------------------------------

HeaderBytes	DW	0		; Number of bytes in header
ImageSize	DW	0		; Number of bytes in load image
EndReloc	DW	0		; Offset past end of packed reloc data
RelocDataSize	DW	0		; Number of bytes in packed reloc data

; Transient code ===============================================================

PROC	Main1		near

Main2:		mov	ah,30h
		int	21h
		cmp	al,2		; Expect DOS 2.0 or later
		jae	DOS_Ok
		mov	dx,OFFSET DOSVersMsg
		mov	ah,9
		int	21h
		int	20h

; Errexit code - aborts the program with a specified message and errorlevel.
; On entry, BX points to a control string within the current code segment,
; which consists of a one-byte errorlevel followed by a null-terminated error
; message which may be blank.  This code assumes DOS version 2.0 handle
; functions are supported.  After writing the error message to StdErr, DOS
; function 4Ch (terminate with return code) is used to terminate the program.
; This code assumes only that CS is equal to the current code segment.

ErrExit:	push	[WORD cs:bx]	; Errorlevel onto stack
		inc	bx		; Point to error message
		call	ErrWriteMsg	; Display
		pop	ax		; Errorlevel
ErrExitQ:	mov	ah,4Ch
		int	21h		; Terminate with errorlevel in AL

ErrWriteMsg:	mov	cx,2		; Handle of standard error device
WriteMsg:	push	cs
		pop	ds		; Make DS valid
		mov	dx,bx		; Point DX for later
Err_Parse1:	inc	bx		; Bump pointer
		cmp	[BYTE ds:bx-1],0 ; Hit null terminator yet?
		jnz	Err_Parse1	; If not, loop
		xchg	cx,bx		; Get address of null terminator
		sub	cx,dx		; Subtract offset of start of message
		dec	cx		; Adjust to correct number of chars
		jz	Err_Parse2	; If no text present
		mov	ah,40h		; Function to write to file or device
		int	21h		; Write error message to StdErr
Err_Parse2:	ret			; Return to exit code or whoever

; Check we have enough memory

DOS_Ok:		cmp	sp,MinRunningSP	; Check we have enough working memory
		mov	bx,OFFSET MemoryEM ; Prepare for error
		jb	ErrExit		; If error

; Parse command tail -----------------------------------------------------------

		cld			; Upwards string direction
		mov	si,81h
Parse1:		lodsb
		cmp	al,13
		je	BadUsage
		cmp	al," "
		jbe	Parse1		; Skip leading whitespace

		mov	di,OFFSET InFilename ; Point to filename
		push	di		; Keep for later
		xor	dx,dx		; Flag no dot found
CopyName:	cmp	al,"."
		jne	NotDot
		mov	dx,di		; If it's a dot, point DX to it
NotDot:		cmp	al,"\"
		jne	NotBackslash
		xor	dx,dx		; If '\', clear last-dot pointer
NotBackslash:	DB	88h,45h,OutFilename-InFilename
					; MOV [OutFilename+DI-InFilename],AL
					; Store to output file name
		stosb			; Store into input file name
		lodsb			; Next character
		cmp	al," "		; Check for end of line or whitespace
		ja	CopyName	; Loop

		mov	[BYTE di],0	; Null-terminate filename temporarily
		test	dx,dx		; Did we find a dot in the filename?
		jz	NoDot		; If not
		mov	di,dx		; Point to the last dot in InFilename
		jmp	SHORT AddCOM	; Add '.COM' to OutFilename

BadUsage:	mov	bx,OFFSET UsageEM ; Incorrect usage
GoErrExit1:	jmp	ErrExit

ReadBlock:	mov	ah,3Fh
		call	FileFunc	; Load BX and issue int 21h
		jc	ReadError	; If error
		cmp	ax,cx
		jne	ReadError	; If we didn't read as much as we wanted
		ret

ReadError:	call	CloseFile
		mov	bx,OFFSET ReadErrEM
		jmp	ErrExit

NoDot:		mov	[BYTE di],"."	; If not, add '.exe',0
		mov	[WORD di+1],"xe"
		mov	[WORD di+3],"e"
		DB	198,69,OutFilename-InFilename+0,"."
					; MOV [BYTE DI+OutFilename-InFilename+0],"."
					; Add '.com',0
AddCOM:		DB	199,69,OutFilename-InFilename+1,"co"
		DB	199,69,OutFilename-InFilename+3,"m",0

; Open input file

		pop	dx		; Point to filename
		mov	ax,3D00h
		int	21h		; Open input file for read
		mov	bx,OFFSET CantOpenInEM ; Prepare for error
		jc	GoErrExit1	; If error
		mov	[Handle],ax	; Store input file handle

; Read input file EXE header

		mov	cx,ExeHdrLen	; Get length
		mov	dx,OFFSET ExeHeader ; Point to memory area
		call	ReadBlock	; Read first part of EXE header

; Check input file header signature

		cmp	[WORD ExeSig],"ZM" ; Check for 'MZ' signature
		je	IsAnExe		; If a valid EXE file
		call	CloseFile
		mov	bx,OFFSET NotAnExeEM
		jmp	SHORT GoErrExit1

TooLarge:	call	CloseFile
		mov	bx,OFFSET TooLargeEM
		jmp	ErrExit

; Calculate header size in bytes

IsAnExe:	mov	ax,[HeaderParas]
		cmp	ax,MaxHdrParas
		jae	TooLarge
		mov	cl,4
		shl	ax,cl
		mov	[HeaderBytes],ax

; Calculate load image size

		mov	ax,[FilePages]
		mov	si,[LastPgSize]
		cmp	si,1		; Set carry if no partial page
		cmc			; Reverse carry
		sbb	ax,0		; If partial page, decrement page count
		mov	cl,9		; Get shift count (CH was already 0)
		cwd			; Zero DX
SizeLoop:	shl	ax,1
		rcl	dx,1		; Shift up
		loop	SizeLoop
		add	ax,si
		adc	dx,cx		; Carry into high word
		sub	ax,[HeaderBytes]
		sbb	dx,cx		; Now have load image size in DX|AX
		jnz	TooLarge	; If over 64K
		cmp	ax,MaxImageSize
		jae	TooLarge	; If too big
		mov	[ImageSize],ax	; Total bytes in load image

; Check for no relocation entries; read relocation table into buffer

		mov	dx,[RelocOff]	; CX is already 0
		mov	ax,4200h
		int	21h		; Seek to start of relocation table
		mov	di,OFFSET Buffer ; Point to start of packed reloc data
		xor	dx,dx		; Prepare for no relocations - end is 0
		mov	cx,[RelocEntries]
		test	cx,cx
		jnz	HaveRelocs
		jmp	NoRelocations	; If no relocations
HaveRelocs:	cmp	cx,MaxRelocEnts	; Validate
		jae	TooLarge
		shl	cx,1
		shl	cx,1		; Shift to get a byte count
		mov	dx,OFFSET Buffer+2 ; Point to raw relocation data area
		mov	si,dx
		mov	di,dx		; Prepare SI and DI for later
		call	ReadBlock	; Read relocation table into memory

; Convert unsorted relocation data into unsorted 16-bit offsets
; Check for relocations out of range (i.e. past end of load image)

		mov	bx,[RelocEntries] ; Get count of relocation entries
		mov	cx,1004h	; Get shift count and comparison value
Convert1:	lodsw			; Get offset
		xchg	ax,dx
		lodsw			; Get segment
		cmp	ah,ch		; Check for invalid segment
		jae	GoTooLarge
		shl	ax,cl		; Shift segment up by four bits
		add	ax,dx		; Add offset
		jc	GoTooLarge	; If overflow
		cmp	ax,[ImageSize]	; Past end of image?
		jae	GoTooLarge	; If so
		stosw			; Store 16-bit relocation offset
		dec	bx
		jnz	Convert1	; Loop

; Bubblesort the relocation data into ascending order - yes I know it could be
; slow if the relocation entries are disorganised, but it's small and it works!

Bubble1:	mov	si,OFFSET Buffer+2 ; Get pointer to start
		mov	cx,[RelocEntries]
		dec	cx		; Do a maximum of n-1 swaps
		jz	NoBubble	; If there's only one entry
		xor	bx,bx		; Clear 'some swapped' flag
Bubble2:	lodsw			; Get value
		cmp	ax,[si]		; Lower than next value?
		jbe	Bubble3		; If so, don't swap them
		xchg	ax,[si]		; If not, swap them
		mov	[si-2],ax
		inc	bx
Bubble3:	loop	Bubble2
		test	bx,bx
		jnz	Bubble1		; Loop until we bubble no more

; Convert relocation offsets to 16-bit deltas

NoBubble:	mov	si,OFFSET Buffer+2 ; Point to buffer again
		mov	di,si
		mov	cx,[RelocEntries]
		mov	dx,-100h	; Initialise relocation pointer
Convert2:	lodsw			; Get relocation offset
		sub	ax,dx		; Calculate delta
		stosw			; Store delta
		add	dx,ax		; Update current position
		loop	Convert2	; Do for all

; Convert relocation data to packed encoded form
; The unpacker stub starts with a relocation offset of 0.  The first relocation
; will be at an address of at least 100h since that is where the load image
; starts (from the stub's point of view).  Each entry in the list is given as
; an offset from the address of the previous relocation address.  When three or
; more offsets of 255 or less are found, this code outputs a high value which
; represents 0FFFFh minus the number of 8-bit deltas which are to follow, then
; outputs the 8-bit deltas as bytes.  This saves space in the packed data.
; The end of the packed relocation data is marked by a word of 0FFFFh, which is
; followed by the address of the last relocation (or 0 if there were no
; relocations) as a check against corruption for the unpacker stub.

		mov	ax,-1
		stosw			; Store a known non-8-bit delta
		mov	si,OFFSET Buffer+2 ; Point to buffer again
		mov	di,OFFSET Buffer ; Output data goes at start of buffer
		mov	cx,[RelocEntries]
Convert3:	lodsw			; Get delta
		test	ah,ah		; Is it an 8-bit delta?
		jz	Is8BitDelta	; If so
		stosw			; If not, just write it back
		jmp	SHORT ConvertNext

GoTooLarge:	jmp	TooLarge

Is8BitDelta:	dec	si		; Point to hibyte of first 8-bit reloc
		xor	bx,bx		; Init counter
Is8BitLoop:	inc	bx
		inc	bx
		cmp	bx,((65536-MaxImageSize)*2)-2 ; Limit count to values
		jae	GotConsec	;   that will give NOTted values equal
					;   to or greater than MaxImageSize
		cmp	[BYTE si+bx],0	; Check for 8-bit relocation
		jz	Is8BitLoop
GotConsec:	dec	si		; Point back to first 8-bit relocation
		shr	bx,1		; Number of consecutive 8-bit deltas
		cmp	bx,3		; Is it worth compressing them?
		jae	Do8Bit		; If so
		movsw			; If not, just copy 16-bit relocation
		jmp	SHORT ConvertNext
Do8Bit:		sub	cx,bx		; Adjust count of remaining entries
		inc	cx		; Adjust for LOOP at end of loop
		mov	ax,bx		; Get count to AX
		not	ax		; Convert to 0FFFFh minus count
		stosw			; Store it
Convert4:	lodsw			; Get delta
		stosb			; Store 8-bit delta value
		dec	bx
		jnz	Convert4	; Loop
ConvertNext:	loop	Convert3	; Continue
		add	dx,100h		; Decompressor uses 100h base

; DI points just past the packed relocation data (if any) in Buffer, and DX
; contains the last relocation address to be used by the unpacker (i.e. it is
; 100h-based), or 0 if there are no relocations.

NoRelocations:	mov	ax,-1		; 0FFFFh word marks end
		stosw
		xchg	ax,dx		; Get final relocation offset
		stosw
		mov	[EndReloc],di	; Store end of packed relocation data
		sub	di,OFFSET Buffer ; Calculate relocation data length
		mov	[RelocDataSize],di ; Store for later

; Seek to load image and read load image into memory buffer

		mov	dx,[HeaderBytes]
		xor	cx,cx		; Zero hiword
		mov	ax,4200h
		call	FileFunc	; Seek to start of load image
		mov	dx,[EndReloc]	; Point to buffer area
		mov	di,dx		; To DI for later
		mov	cx,[ImageSize]	; Get image size
		mov	ax,dx
		add	ax,cx
JCTooLarge:	jc	GoTooLarge
		cmp	ax,MemLimit	; Check we won't go too high
		jae	GoTooLarge
		call	ReadBlock	; Read image into memory
		xchg	ax,si		; Keep image size in SI for later

; Check for extra data past load image

		mov	dx,OFFSET CrapBuffer
		mov	cx,16		; Expect up to 15 padding bytes
		mov	ah,3Fh
		int	21h
		jc	NoOverlay
		cmp	ax,cx		; Did we get more than 15 bytes?
		jne	NoOverlay
		mov	bx,OFFSET OverlayM
		call	ErrWriteMsg

; Get input file date/time stamp

NoOverlay:	mov	ax,5700h
		call	FileFunc	; Get date/time
		push	dx
		push	cx		; Keep for later

; Close input file

		call	CloseFile

; Patch first three bytes into a JMP instruction; store original three bytes
; at Value100 (in stub image)

		mov	dl,0E9h		; JMP nnnn (relative)
		xchg	dl,[di+0]	; Replace first byte
		mov	[Value100+0],dl	; Keep it to be replaced by stub
		sub	si,3		; Calculate jump distance
		xchg	si,[di+1]	; Place operand, get old values
		mov	[WORD Value100+1],si ; Keep it for later too

; Set up other values in stub image

		mov	cl,4		; Prepare shift count

		mov	ax,[ImageSize]	; Get image size in bytes
		add	ax,10Fh		; Add PSP plus 15
		shr	ax,cl		; Calculate paragraphs to hold image
		push	ax		; Keep it a moment
		add	ax,[MinAlloc]	; Add minimum required paras above
		mov	[MinParas],ax	; Store into stub

		pop	ax		; Get paras required to hold image
		push	ax		; Keep it again
		add	ax,[MaxAlloc]	; Add maximum extra memory wanted
		jnc	GotMaxParas	; If didn't wrap around
		mov	ax,0FFFFh	; If wrapped, limit it to 0FFFFh paras
GotMaxParas:	mov	[MaxParas],ax	; Store into stub

		pop	ax		; Get image size in paragraphs again
		shl	ax,cl		; Back to bytes
		add	ax,[RelocDataSize] ; Add relocation data size
		DB	5		; ADD AX,StubLength+StubStack
		DW	StubLength+StubStack ; Add stub and stack space
		jc	JCTooLarge	; If too large
		mov	[SafeStack],ax	; Store as minimum SP for stub

		mov	ax,[StackSegVal]
		mov	[SSValue],ax
		mov	ax,[StackPtrVal]
		mov	[SPValue],ax
		mov	ax,[InstrPtrVal]
		mov	[IPValue],ax
		mov	ax,[CodeSegVal]
		mov	[CSValue],ax

; Open output file

		mov	dx,OFFSET OutFilename
		mov	cx,00100000b	; Archive bit set, others clear
		mov	ah,3Ch
		int	21h		; Try to open it
		mov	bx,OFFSET CantOpenOutEM ; Prepare for error
		jc	GoErrExit2	; If error
		mov	[Handle],ax	; Store output file handle

; Write load image (with patched first three bytes) to output file

		mov	dx,[EndReloc]	; Point to load image
		mov	cx,[ImageSize]
		call	WriteBlock	; Write to output file

; Write stub

		mov	dx,OFFSET Fixup0
		DB	185		; MOV CX,StubLength
		DW	StubLength
		call	WriteBlock	; Write to output file

; Write packed relocation data

		mov	dx,OFFSET Buffer
		mov	cx,[RelocDataSize]
		call	WriteBlock	; Write to output file

		pop	cx
		pop	dx		; Restore date/time values from infile
		mov	ax,5701h
		int	21h		; Set date/time to same

		call	CloseFile
		mov	bx,OFFSET ConvertedEM
		jmp	SHORT GoErrExit2

WriteBlock:	mov	ah,40h
		call	FileFunc
		jc	WriteError	; If DOS reported an error
		cmp	ax,cx		; Check we wrote as much as requested
		je	ARet1

WriteError:	call	CloseFile
		mov	bx,OFFSET WriteErrEM
GoErrExit2:	jmp	ErrExit

CloseFile:	mov	ah,3Eh
FileFunc:	DB	187		; MOV BX,nnnn
Handle		DW	0		; File handle to input or output file
		int	21h		; Close file
ARet1:		ret

ENDP	Main1

; Fixup stub code --------------------------------------------------------------

Fixup0:		call	Fixup1		; Skip data area, put address on stack

DIP		= $			; DI will point here when popped
		DB	AppName,Ver+"0",SubVer+"0",ModVer+"0","/KH" ; Signature
MemErrMsg	DB	13,10,"Not enough memory"
MemErrMsLen	=	$ - MemErrMsg + 2
CorruptMsg	DB	13,10,"Packed file corrupt",13,10
CorruptMsLen	=	$ - CorruptMsg
IPValue		DW	0		; IP value from EXE header
Value100	DB	0,0,0		; Values to be reinstated at 100h

Fixup1:		pop	di		; Point to local data

		DB	129,252		; CMP SP,nnnn
SafeStack	DW	0		; Min allowable SP for COM
					; Check stack is far enough away
		jb	StubMemError	; If not, stack might overwrite fixups

		xchg	ax,[di+Value100-DIP] ; Get first word value, save AX
		mov	[ds:100h],ax
		mov	al,[di+Value100-DIP+2]
		mov	[ds:102h],al	; Reinstate first three bytes of image

; Apply fixups to load image

		cld
		lea	si,[di+EndFixCode-DIP] ; Point to fixup data
		mov	bp,MaxImageSize	; Set up comparison value for later
		mov	dx,cs		; Get PSP segment
		add	dx,10h		; Get effective image load segment
		xor	bx,bx		; Initialise fixup address
Fixup2:		lodsw			; Get word from fixup data
		cmp	ax,bp		; Test for 1 or 0
		jae	Fixup3		; If a special code
		add	bx,ax		; Bump pointer
		jc	Corrupted	; If it wraps around - error!
		add	[ds:bx],dx	; Apply fixup
		jmp	SHORT Fixup2	; Next item
Fixup3:		xor	ax,0FFFFh	; Make +ve (NOT AX doesn't set flags!)
		jz	Fixup5		; If zero - finished
		xchg	ax,cx		; Count to CX
		xor	ax,ax		; Zero hibyte of AX
Fixup4:		lodsb
		add	bx,ax		; Bump pointer
		jc	Corrupted	; If it wraps around - error!
		add	[ds:bx],dx	; Apply fixup
		loop	Fixup4		; Next item
		jmp	SHORT Fixup2	; Restart from normal point

; Error exit points - these assume DS = PSP and DI -> DIP

StubMemError:	lea	dx,[di+MemErrMsg-DIP]
		mov	cx,MemErrMsLen
		jmp	SHORT StubDisplay
Corrupted:	lea	dx,[di+CorruptMsg-DIP]
		mov	cx,CorruptMsLen
StubDisplay:	mov	bx,2		; STDERR
		mov	ah,40h
		int	21h		; Display message
		mov	ax,4C00h+254	; Get error code and function
		int	21h		; Exit with appropriate errorlevel
		int	20h		; (just in case DOS 1.x!)

; Check memory allocation and adjust block size if necessary

Fixup5:		lodsw			; Get expected final offset
		cmp	ax,bx		; Check it
		jne	Corrupted
		push	dx		; Keep for later
		sub	dx,11h		; Arena hdr should be just below PSP
		mov	ds,dx		; Address arena header with DS
		inc	dx		; Back to this paragraph
		cmp	dx,[ds:ARNA_Para] ; Check for valid arena header
		mov	ax,[ds:ARNA_Size] ; Get size of block
		push	cs
		pop	ds
		jne	StubMemError	; If arena is not valid

		DB	61		; CMP AX,nnnn
MinParas	DW	0		; Min paras required (from PSP seg)
					; Check we have enough memory
		jb	StubMemError	; If not enough memory
		DB	187		; MOV BX,nnnn
MaxParas	DW	0		; Max paras to allocate (from PSP seg)
		cmp	ax,bx		; Should we resize the block?
		jbe	NoResize	; If not
		mov	ah,4Ah
		int	21h		; Resize block to smaller size

; Relocate stack and create return address and flags

NoResize:	pop	bx		; Get EXE load module paragraph again
		DB	141,135		; LEA AX,[BX+nnnn]
SSValue		DW	0		; SS value from EXE header
		cli			; Add initial SS (unrelocated)
		mov	ss,ax
		DB	188		; MOV SP,nnnn - set SP to top of stack
SPValue		DW	0		; SP value from EXE header
		pushf			; Push flags for IRET
		mov	bp,sp		; Address the stack
		or	[BYTE ss:bp+1],2 ; Enable interrupts in stacked flags
		DB	141,135		; LEA AX,[BX+nnnn]
CSValue		DW	0		; CS value from EXE header
		push	ax		; Image segment plus initial CS
		push	[WORD di+IPValue-DIP]

; Set up registers and IRET to the program start

		mov	ax,[di+Value100-DIP] ; Get AX as provided by DOS
		xor	bx,bx
		xor	cx,cx
		xor	dx,dx
		xor	si,si
		xor	di,di
		xor	bp,bp
		iret

EndFixCode	=	$		; End of fixup code
StubLength	=	EndFixCode - Fixup0 ; Length of fixup code stub

; Buffers ----------------------------------------------------------------------

		ALIGN	16		; Paragraph-align for no good reason

EndCode		= $

CrapBuffer	DB	16 DUP(?)	; Buffer for check for overlay data

InFilename	DB	112 DUP(?)	; Input (EXE) file pathname
OutFilename	DB	112 DUP(?)	; Output (COM) file pathname

Buffer		= $			; Buffer for fixup data and load image

		ORG	MemLimit

EndUsedMem	= $			; End of memory we are allowed to use

		MASM

prexp		MACRO	Text1,Exp,Text2		; Invoke in MASM mode only
		%OUT	Text1 &Exp Text2
		ENDM

		prexp	<Program size  :> %(EndCode - @curseg - 100h) < bytes>
		prexp	<Stub size     :> %(StubLength) < bytes>
		prexp	<Buffer memory :> %(EndUsedMem - Buffer) < bytes>

		IDEAL

		ENDS	Code
		END	Main0

;-------------------------------------------------------------------------------
