	IDNT FileCompare
**************************************************************************
*
* FileCompare - Determines if two specified files have identical contents
*
*
* DESCRIPTION:
*   The main purpose of this file is to assist automatic generation of
*   function prototypes files in make scripts.  For this purpose, all
*   that's wanted is a nice, testable exit status, but the program also
*   sends text to the standard output.
*
*   This is a single-module program.  The non-library subroutines used
*   are all local to this module, and use the on-stack local storage
*   allocated by the entry-level sequence's LINK instruction as they like.
*
*   The code produced is PURE, and may be made resident.
*
*   
*   Copyright ©1992 Brian Rhodefer
**
**
**	Tabstops : 8
**	To Assemble: Macro68 FileCompare.asm
**
**
**	July 19, 1992	** written
**
**
**  FUNCTION & USAGE
**
**	FileCompare <file1> <file2> [Q]
**

		INCPATH		"includes:"
		OBJFILE		"FileCompare"
		ERRFILE		"FileCompare.bugs"

		LALL
		PALL
		HALL
		SFCOND
		EXEOBJ
		LIST
		LISTFILE	"ram:FileCompare.lst"
		STRICT
		MC68000




	macfile "AllLibraryOffsets.i"
	macfile "exec/types.i"
	macfile "libraries/dos.i"
	macfile "fcMacros.i"

	section FileCompare,code

;
; Define the template for our local on-stack variables:
;

	STRUCTURE fileMGMT,0
	APTR fHandle
	CPTR fName
	LONG fLength
	LABEL fMGMT_SIZEOF

	STRUCTURE myStackFrame,0
	STRUCT file1,fMGMT_SIZEOF
	STRUCT file2,fMGMT_SIZEOF
	APTR outStream
	STRUCT myFIB,fib_SIZEOF
	
	LABEL SFL			; Stands for "Stack Frame Length"


BUFSIZE	EQU 512


*************************
*	COLD ENTRY	*
*************************


START:
	jmp (realStart,PC)		; Allegedly, tools can
	cstr 'fileCompare V0.1'
	even

realStart:
	link A5,#-SFL			; Reserve our variables

	move.l	D0,D2			; Make copies of command-line length
	movea.l A0,A2			; Initialize pointer to commandline to scan
	
	clr.l (file1+fHandle-SFL,A5)
	clr.l (file2+fHandle-SFL,A5)

	lea (dosName,PC),a1		; Try to open the DOS library:
	moveq.l #33,D0			; Version 1.2 or better
	movea.l (4).w,a6		; Get pointer to Execbase
	SYSCALL OpenLibrary
	movea.l D0,A6
	tst.l D0
	bne.b .dosIsOpen

	moveq #20,D0			; This qualifies as a total "FAIL"ure
	bra.w unlinkAndReturn

.dosIsOpen:		
	SYSCALL Output
	move.l D0,(outStream-SFL,A5)

	subq.w #2,D2		; Prepare DBcc index to omit terminator
	bcs.b .argTrouble	; There should be at least ONE char!

;
	bsr.w getAnArg			; Find a pointer to file1's name
	move.l A0,(file1+fName-SFL,A5) 
	beq.b .argTrouble

	bsr.w getAnArg			; Find a pointer to file2's name
	move.l A0,(file2+fName-SFL,A5)
	bne.b .twoLikelyFilenames

.argTrouble:
	bra.w badArgs

.twoLikelyFilenames:

;
; Now it's time to find out if our files are there
;
	lea (file2-SFL,A5),A3
	bsr.w myFileOpen
	move.l D0,D3		; Save exit status from file 1 attempt
	lea (file1-SFL,A5),A3
	bsr.w myFileOpen
	and.l D3,D0
	beq.b badFiles

	move.l (fLength-SFL,A5),D6	; File length remaining
	cmp.l  (file2+fLength-SFL,A5),D6
	bne.b filesDontMatch
	tst.l D6			; Are they zero-length?
	beq.b allDone			; If so, they match by definition
	bsr.w compareFiles

allDone:
	lea (matchString,PC),A0
	tst.l D6
	beq.b .announceResults
	lea (differString,PC),A0

.announceResults:
	bsr.w myPutString

closeDown:
	move.l (file1+fHandle-SFL,A5),D1
	beq.b .file1IsGone
	SYSCALL Close

.file1IsGone:
	move.l (file2+fHandle-SFL,A5),D1
	beq.b closeDos
	SYSCALL Close

closeDos:
	movea.l A6,A1
	movea.l (4).w,A6
	SYSCALL CloseLibrary
	move.l D6,D0

unlinkAndReturn:
	unlk A5
	rts	

badArgs:
	lea (argsString,PC),A0
	bsr.w myPutString
	moveq #10,D6			; This is an "ERROR"
	bra.b closeDos

filesDontMatch:
	moveq #5,D6			; "WARN" condition
	bra.b allDone

badFiles:
	moveq #10,D6
	bra.b closeDown


*******************************************************************************
*
*  *********************
*  * Local subroutines *
*  *********************
*
*
* getAnArg - scan through the command line, looking for an argument.
*
* DESCRIPTION:
*  Pretty simple, except that arguments with embedded spaces are permitted
*  if they're enclosed in double quotes per standard AmigaDOS practice.
*
* INPUTS:
*   A2 - Pointer to beginning of remaining unscanned portion of command line
*   D2 - Number of remaining characters in command line
*
* RETURNS:
*   A0 - Pointing to first character of name (in other words, what A2 was on entry)
*        or NULL if no name found

getAnArg:
	tst.l D2
	beq.b .badExit
	moveq #' ',D1		;  Character we're gonna ignore

.findFirstChar:
	cmp.b (A2)+,D1
	dbne D2,.findFirstChar
	beq.b .badExit
;
; Well, we've found a non-blank command line character.
; Is it a double-quote?
;
	cmpi.b #'"',-(A2)
	bne.b .foundStart
	move.b (A2)+,D1		; If so, that'll become the terminating char

.foundStart:
	movea.l A2,A0

.findLastChar:
	cmp.b (A2)+,D1
	dbeq D2,.findLastChar
	beq.b .goodExit
	moveq #0,D2

.goodExit:
	clr.b (-1,a2)
	rts

.badExit:
	moveq #0,D2		; Ensure subsequent calls see "No chars left"
	movea.l D2,A0
	rts


*******************************************************************************
* myFileOpen - Attempts to Lock() file designated by the name string of a given
*              "fileMGMT" structure.  If the Lock is obtained, the file's
*              attributes are obtained using a call to Examine(), and then the
*	       file is Open()ed for reading and unlocked.
*              
* DESCRIPTION:
*  
*  
*  The argument string delimiter
*
* INPUTS:
*   A3 - Points to a "fileMGMT" structure
*   A5 - Points to top of local on-stack variables
*   A6 - Pointer to Dos library
*
* RETURNS:
*   D0 - 1 if the file locked and opened successfully, 0 if there was
*        a problem.  An attempt is made to annunciate problems on the
*        standard output
*
*******************************************************************************

myFileOpen:
	move.l (fName,A3),D1
	move.l D1,D6			; Save pointer to filename
	moveq #ACCESS_READ,D2
	SYSCALL Lock
	move.l D0,D5
	beq.b .fileNotFound
;
; Prepare arguments for call to Examine:
;
	move.l D0,D1
	lea (myFIB-SFL,A5),A0
	move.l A0,D2
	SYSCALL Examine
	tst.l D0
	beq.b .errNoExamine			; Pretty unlikely...
	tst.w (myFIB+fib_DirEntryType-SFL,A5)	; Negative if a plain file
	bpl.b .fileIsDir
	move.l (myFIB+fib_Size-SFL,A5),(fLength,A3)

	
	move.l D6,D1			; Recover filename pointer
	move.l #MODE_OLDFILE,D2
	SYSCALL Open
	move.l D0,(A3)
	beq .locksButWontRead

.goodExit:
	move.l D5,D1
	SYSCALL UnLock
	moveq #1,D0
	rts


.errNoExamine:
	lea (examineErrorString,PC),A0
	bra.b .complainAndUnlock

.fileIsDir:
	lea (fileIsDirString,PC),A0
	bra.b .complainAndUnlock

.locksButWontRead:
	lea (openErrorString,PC),A0

.complainAndUnlock:
	bsr.b .complainAboutFile
	bsr.b .goodExit			; re-use the Unlock() call
	bra.b .errorOut


.fileNotFound:
	lea (lockErrorString,PC),A0

.complainAboutFile:
	bsr.b myPutString
	movea.l (fName,A3),A0
	bsr.b myPutString
	lea (closeBracket,PC),A0
	bsr.b myPutString

.errorOut:
	moveq #0,D0
aReturn:
	rts
	

*******************************************************************************
* myPutString - Sends string to standard output via Write()
*              
* DESCRIPTION:
*   Since we're restricting ourselves to AmigaOS V1.3 features, the only
*   function available to us for printing messages to the standard output
*   is to use the Write() call, this subroutine makes things a bit easier
*   by counting the number of characters in a null-terminated string, in
*   order to supply Write() with the necessary "length" information
*  
*  
*
* INPUTS:
*   A0 - Pointer to NULL-terminated string
*   A5 - Points to top of local on-stack variables
*   A6 - Pointer to Dos library
*
* RETURNS:
*   Nothing.  If Write()ing to the standard output doesn't work, we're in
*   big trouble anyway.  Maybe AmigaOS will have kept a an error that the
*   user can discover by using the "Why" program.
*
*******************************************************************************
	
myPutString:
	move.l (outStream-SFL,A5),D1	; Fetch pointer to standard output
	beq.b aReturn
	moveq #-1,D3			; We won't be counting the NULL.
.sizer:
	addq.l #1,D3
	tst.b (0,A0,D3.w)
	bne.b .sizer
	move.l A0,D2			; Write() wants is buffer-pointer here.
	jmp (_LVOWrite,A6)


*******************************************************************************
* compareFiles - See if open files have identical contents
*              
* DESCRIPTION:
*   Allocate buffers for both files, in a contiguous fashion (so only 1 call
*   to AllocMem/FreeMem is needed).  Read & compare blocks of each file until
*   either a difference is found, or the files are exhausted; then deallocate
*   the buffer. If errors are found, print them.
*  
*  
*
* INPUTS:
*   A5 - Points to top of local on-stack variables
*   A6 - Pointer to Dos library
*
* RETURNS:
*   D6 - PASS/WARN/FATAL status
*

compareFiles:
	move.l #BUFSIZE,D7
	move.l D7,D0
	add.l D7,D0			; We'll allocate two contiguous buffers.
	move.l #MEMF_CLEAR,D1
	LIBCALL AllocMem,4
	movea.l D0,A4			; Pointer to our buffer space
	tst.l D0
	beq.b .tooLittleMemory

;
; OK, here's where we do the actual comparison of file contents.
; D7 keeps the buffer size handy.
; D6 keeps track of the number of bytes remaining in the files.
; D5 computes the number of bytes to attempt to read from the files
; A4 holds a pointer to file1's read buffer.  File 2's starts <D7> bytes after.
;

.compareBlock:
	move.l D7,D5
	cmp.l D5,D6		; Have we fewer than a buffer's worth left?
	bcc .useD5
	move.l D6,D5		; If so, just ask for the remaining chars.
.useD5:				; Read from file #1: 
	move.l (file1+fHandle-SFL,A5),D1
	move.l A4,D2
	move.l D5,D3
	SYSCALL Read
	move.l D0,D4		; So we can compare length read in file 2

	lea (BUFSIZE,A4),A2	; Form pointer to buffer for file#2
	move.l (file2+fHandle-SFL,A5),D1
	move.l A2,D2
	move.l D5,D3
	SYSCALL Read

	cmp.l D0,D4		; Were the same # of chars read from both?
	bne.b .readPhaseError
;
; Because we're going to stop as soon as we find the first difference
; between the files, and because the two buffers started out equal
; (i.e., cleared), we're going to compare whole longwords at a time,
; and not even bother with the question of partially-filled longwords
; at the end of an incompletely-filled buffer.  So long as the number of
; characters read from each file is the same, any such "missed" spots in
; the buffer will match.  So, compute a "DBcc loop" termination count for
; the bytes read:
;
	subq.w #1,D0
	lsr.l #2,D0

	movea.l A4,A0		; Pointer to file1's buf.  A2=> file 2's.

.compareLoop:
	cmpm.l (A0)+,(A2)+
	dbne D0,.compareLoop

	bne.b .filesDontMatch
	sub.l D4,D6		; How many characters remain?
	bhi .compareBlock

.closeBuf
	move.l D7,D0
	add.l  D7,D0
	movea.l A4,A1
	move.l A4,D1
	beq.b .bufIsGone
	EXECLIB FreeMem

.bufIsGone:
	rts

.filesDontMatch:
	moveq #5,D6		; This is a "WARN" condition
	bra.b .closeBuf

.readPhaseError:
	lea (badLengthString,PC),A0

.lastWord:
	bsr.w myPutString
	clr.l (outStream-SFL,A5)	; Squelch any further error output
	moveq #10,D6		; This is an "ERROR" condition
	bra.s .closeBuf
	
.tooLittleMemory:
	lea (noBufString,PC),A0
	bra.b .lastWord	


*******************************************************************************
;         STRINGS

	ESCAPESTR

dosName:		DOSNAME
examineErrorString:	CSTR 'Can''t examine file <'
lockErrorString:	CSTR 'Can''t find file <'
openErrorString:	CSTR 'Can''t open file <'
fileIsDirString:	CSTR 'Specified file is directory: <'
closeBracket:		CSTR '>\N'
badLengthString:	CSTR 'Non-matching read lengths\N'
matchString:		CSTR 'Files are identical\N'
differString:		CSTR 'Files are dissimilar\N'
noBufString:		CSTR 'Couldn''t allocate compare buffer\N'
argsString:		CSTR 'Usage: fileCompare <file1> <file2>\N'
versionString:		CSTR '$VER: fileCompare 1.0 (20.7.92)'

*******************************************************************************
	END