;-------------------------------------------------------------------------------
; TOUCH -- Free-DOS touch utility
;
; (c) Copyright 1989-1995 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 Free-DOS
; users may benefit.  Contact details are:
;
; Internet:  kheidens@actrix.gen.nz
; Snail:     K. Heidenstrom c/- P.O. Box 27-103, Wellington, New Zealand
;
;-------------------------------------------------------------------------------
;
; This program must be assembled with Borland's TASM.  The syntax is:
;
;	tasm /ml /t /w2 /denglish touch;
;	tlink /t touch, touch, nul
;	touch -cf touch.com
;
; The /denglish option to TASM specifies the language for which TOUCH is to
; be assembled.  National language support is implemented via language
; specific include files which are INCLUDEd via the INCLUDE file TOUCH.NLS.
; These language specific files are named TOUCH.Ixx where 'xx' indicates the
; language.  Currently only TOUCH.IEN (English) is defined.  Refer to TOUCH.NLS
; for information on creating new language-specific include files for TOUCH.
;
;-------------------------------------------------------------------------------
;
; Modified:
;
; KH.19890119.001	  Started
; KH.19890219.002	  Multiple files, parsing, etc
; KH.19890225.003	  Changed command line format, wildcards
; KH.19890307.004	  Allow /dd/mm/yy and /hh:mm[:ss] anywhere on line
; KH.19890513.005  1.0.0  Versioned
; KH.19890728.006  1.1.0  Re-written for intelligent parameter parsing
; KH.19890826.007  1.2.0  Allow 0/0/0 date (date doesn't appear in directory)
; KH.19890924.008  1.2.1  Fixed bug with paths and wildcards in combination
; KH.19891224.009  1.3.0  Standard errorlevels
; KH.19950219.010	  Started version for Free-DOS
; KH.19950605.011  1.4.0  First working version for Free-DOS
; KH.19950716.012  1.4.1  Added -Dpathname
; KH.19950722.013  1.4.2  Fixed bug - would try to touch fules '.' and '..'
; KH.19951005.014  1.4.3  Implemented suggestions from Mark Andreas (voyageur@
;			  sky.net) - (a) added conditional assembly to control
;			  whether files which don't already exist will be
;			  created if specified unambiguously (the CREATE
;			  conditional), and (b) support 'a' or 'p' suffix for
;			  time values on the command line; times are assumed to
;			  be in 24-hour format unless a suffix is found.

Ver		EQU	1
SubVer		EQU	4
ModVer		EQU	3
VerDate		EQU	"19951005"		; YYYYMMDD format

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

		PAGE	,132
		IDEAL
		%TITLE	"TOUCH -- Free-DOS touch utility"
		NAME	TOUCH

		INCLUDE	"TOUCH.NLS"	; Include appropriate language

; Notes ------------------------------------------------------------------------

; This program requires include file QUALPATH.ASM
;
; This program implements the traditional Unix 'touch' utility with a few
; enhancements.  This implementation will create files if they do not exist,
; like the standard Unix 'touch' but unlike most DOS implementations.  This
; behaviour can be overridden by changing the CREATE equate from 1 to 0,
; which will cause the program to not create files by default.
; This implementation allows the file(s) to be touched with the current date
; and time (if no date or time values are specified), or to a specific date
; (time unchanged), time (date unchanged), or date and time, or the date and
; time of another file.  This program is also able to scan the file(s) for a
; version string and set the file time accordingly, so that the file's version
; number can be seen in a directory listing, if desired.
;
; The general usage syntax is:
;
; TOUCH [-?] [-C] [-Dpathname] [-F] [Date] [Time] Pathspec [...]
;
; See the accompanying file TOUCH.TXT for more information.
;
; Possible idea for improvement - more stringent range checking on dates?
;
; ------------------------------------------------------------------------------

; Equates

AppName		EQU	"TOUCH"

; The CREATE conditional determines TOUCH's default behaviour for files that
; are named explicitly (i.e. not with wildcards) but do not exist.  If CREATE
; is 1 (true), TOUCH will (by default) create such files, and the -C option
; causes TOUCH to _not_ create such files.  This is the standard setting, and
; is compatible with Unix TOUCH (I believe) but different from most DOS TOUCH
; programs.  If CREATE is 0 (false), TOUCH will by default _not_ create such
; files, and the -C option caues TOUCH _to_ create such files.	This is similar
; to the behaviour of most DOS TOUCH programs.

CREATE		=	1		; 1 = Create by default, 0 = Don't

InBufSize	=	49152		; Size of input buffer for -F function

DTA_FName	=	30		; Filename, null-terminated, 13 chars
DTAAlloc	=	44		; Num of bytes to allocate for the DTA

PType_Switch	=	0		; Indices into Handlers1 and Handlers2
PType_Date	=	2
PType_Time	=	4
PType_Other	=	6

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

; Shorthand

MACRO	point	Reg,Label
		mov	Reg,OFFSET Label
ENDM

MACRO	clr	Regs
		IRP	Reg,<Regs>
		xor	Reg,Reg
		ENDM
ENDM

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

NL		EQU	13,10

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

; Program

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

		ORG	0100h		; Com-type file
PROC	Main		near
		jmp	Main2
ENDP	Main

Signature	DB	"SIG: Free-DOS ",AppName,".COM version ",Ver+"0","."
		DB	SubVer+"0",".",ModVer+"0",", ",VerDate,": ",LangName,"; "
		IF	CREATE
		DB	"CREATE=Y; "
		ELSE
		DB	"CREATE=N; "
		ENDIF
;---------------
;!! Change the following line if you create a derivative version of this program
		DB	"Original"
;---------------
		DB	"  ",26		; Ctrl-Z
SigLen		=	$ - Main	; Length of program signature

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

Transient_Msgs				; Macro from the language specific
					;   include file

NewlineM	DB	NL,0

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

; Parameter processing tables, for first and second command tail parse scans.
; The PType_... values provide 0/2/4/6 indices into these tables.

Handlers1	DW	ProcessSwitch	; Switch - process on first pass
		DW	ProcessDate	; Date - process on first pass
		DW	ProcessTime	; Time - process on first pass
		DW	SetHaveSpec	; Other - process on second pass

Handlers2	DW	ProcessRet	; Switch - ignore on second pass
		DW	ProcessRet	; Date - ignore on second pass
		DW	ProcessRet	; Time - ignore on second pass
		DW	ProcessSpec	; Other - process on second pass

DateFmtTbl	DW	DateFormat0
		DW	DateFormat1
		DW	DateFormat2

DateFormat1	DB	Letter_Day,Letter_Day
		DB	Letter_Month,Letter_Month
DateFormat2	DB	Letter_Year,Letter_Year
DateFormat0	DB	Letter_Month,Letter_Month
		DB	Letter_Day,Letter_Day
		DB	Letter_Year,Letter_Year

DateSeps	DB	"--."		; Date separator chars: US, Euro, Jap.

Parm2Tbl	DW	RdDay		; Tables of addrs of parameter handlers
		DW	RdMonth
Parm1Tbl	DW	RdMonth
		DW	RdDay
Parm3Tbl	DW	RdYear
		DW	RdYear
		DW	RdDay

; Variables

DateCode2	DW	0		; Date code x 2 (0/2/4)
DateSep		DB	"-"		; Date separator character

Switchar	DB	"/"		; DOS switch character

SetDate		DB	0		; Flag whether to update file dates
SetTime		DB	0		; Flag whether to update file times
HadFail		DB	0		; Flag whether any failures occurred
CFlag		DB	0		; Flag for -C option
DFlag		DB	0		; Flag for -Dpathname option
FFlag		DB	0		; Flag for -F option
HaveSpec	DB	0		; Flag whether any pathspecs found
Created		DB	0		; Flag whether file was created

Number1		DW	0		; Numbers parsed - must be adjacent!
Number2		DW	0
Number3		DW	0

NewYear		DW	0		; Date - Year
NewMonth	DW	0		;      - Month
NewDay		DW	0		;      - Day
NewHour		DW	0		; Time - Hour
NewMin		DW	0		;      - Minute
NewSec		DW	0		;      - Second
NewDDate	DW	0		; Date word from -Dpathname command
NewDTime	DW	0		; Time word from -Dpathname command

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

; The mainline code checks the DOS version and amount of memory, and relocates
; the stack, then sets up the country number and patches the characters in the
; usage syntax message which vary depending on the country setting, then the
; current date and time are requested from DOS.
; Next, the command tail is scanned twice, once using the Handlers1 table of
; parameter handlers then again using Handlers2.  On the first scan, option
; switches and date and time values are handled and pathspecs are flagged but
; not processed.  On the second scan, the reverse occurs and each pathspec is
; processed as it is found, while other parameters are ignored.

PROC	Main2		near
		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

BadUsage:	point	bx,UsageEM	; Incorrect usage

; 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.

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 text
		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 amount of available memory ---------------------------------------------

DOS_Ok:		mov	ax,cs		; Get current segment
		DB	5		; ADD AX,nnnn (avoid TASM warning!)
		DW	MemParas	; Get paragraph past end of program
		cmp	ax,[WORD ds:2]	; Check against paragraph of mem top
		mov	bx,OFFSET MemoryEM ; Prepare for error
		jae	ErrExit		; If not enough memory
		point	sp,WorkStackTop	; Relocate stack to safe area

; Get country-specific information ---------------------------------------------

		point	dx,DTA		; Set DTA away from command tail
		mov	ah,1Ah
		int	21h
		mov	ax,3800h
		int	21h		; Request current country-dependent info
		mov	si,[WORD DTA]	; Get date format code
		cmp	si,3		; Ensure within limits
		jb	CountryOK	; If so
		clr	si		; If not, use American format
CountryOK:	mov	ah,30h
		int	21h		; Get DOS version
		cmp	al,3
		mov	al,[DateSeps+si] ; Get default separator character
		jb	NoDateSep	; If not 3.0 or later
		mov	al,[DTA+11]	; If 3.0 or later, get separator
NoDateSep:	mov	[DateSep],al	; Store it
		shl	si,1		; Convert for two byte table entries
		mov	[DateCode2],si	; Store for later use

; Set up country-specific info in usage message --------------------------------

		cld			; Upwards string direction please
		mov	si,[DateFmtTbl+si] ; Point to appropriate table
		point	di,DateFmt	; Destination area
		movsw			; First parameter
		stosb			; Date separator
		movsw			; Second parameter
		stosb			; Date separator
		movsw			; Third parameter

; Get DOS date and time and current switchar -----------------------------------

		mov	ah,2Ah
		int	21h		; Request current date from DOS
		sub	cx,1980		; Subtract year offset
		mov	[NewYear],cx	; Store current year as new value
		mov	[BYTE NewMonth],dh ; Month
		mov	[BYTE NewDay],dl ; Day
		mov	ah,2Ch
		int	21h		; Request current time from DOS
		mov	[BYTE NewHour],ch ; Store current hour as new value
		mov	[BYTE NewMin],cl ; Minutes
		mov	[BYTE NewSec],dh ; Seconds

		mov	ax,3700h
		int	21h		; Get switchar to DL
		mov	[Switchar],dl	; Store for later use

; Scan command tail for dates and times, check results of scan -----------------

		point	bp,Handlers1	; Handler table for switch, date, time
		call	ProcessTail	; Process switch, date, and time parms
		mov	al,[SetDate]	; Get flag whether date specified
		or	al,[SetTime]	; Was date and/or time supplied?
		jnz	GotEither	; If so, leave it at that
		mov	[SetDate],1	; If neither specified, set both
		mov	[SetTime],1
		jmp	SHORT SkipEither ; Continue
GotEither:	cmp	[FFlag],0	; If date and/or time given, cannot
		jnz	GoBadUsage1	;   have -F as well.
SkipEither:	cmp	[HaveSpec],0	; Did we notice any pathspecs?
		jz	GoBadUsage1	; If not

; Scan command tail for pathspecs, process each as it is found -----------------

		point	bp,Handlers2	; Point to handler table for filenames
		call	ProcessTail	; Process filespec arguments

; Terminate with an appropriate errorlevel -------------------------------------

		mov	al,[HadFail]	; Get flag whether any failures
		shl	al,1		; Convert to errorlevel 0 or 2
		mov	ah,4Ch		; Terminate with errorlevel
		int	21h		; Do it
ENDP	Main2

GoBadUsage1:	jmp	BadUsage

; Command tail parsing ---------------------------------------------------------

; This function parses the command tail once.  It is called twice.  The
; address of a handler table is passed in BP.  This function uses the
; NextParm function to parse each parameter, then calls the appropriate
; handler function through the specified handler table.

PROC	ProcessTail	near
;				Func:	Perform one pass on command tail
;				In:	BP -> Table of handlers for parms
;					[DateCode2] = Date format code (0/2/4)
;				Out:	None
;				Lost:	AX BX DX SI DI
		mov	si,81h		; Point to command tail
ParmLoop:	call	NextParm	; Scan and determine type of parameter
		jc	TailDone	; If hit end of tail
		push	si		; Keep pointer to continue scan
		push	bp		; Preserve BP
		mov	si,di		; Point to parameter we just found
		xchg	ax,bx		; Get parameter type to BX
		add	bx,bp		; Index from table
		call	[WORD bx]	; Call handler
		pop	bp		; Restore BP
		pop	si		; Restore pointer for scan continuation
		jmp	SHORT ParmLoop	; Next parameter
TailDone:	ret			; Finished parsing
ENDP	ProcessTail

; Parameter parser -------------------------------------------------------------

; This function delimits one parameter on the command line.  It skips any
; leading whitespace, returning a pointer to the start of the parameter in
; DI on exit, and parses the parameter to determine the parameter type.  The
; parameter type may be:
;
; PType_Switch		If the parameter begins with '-' or the current DOS
;				switchar character
; PType_Date		If the parameter is in the form nn-nn-nn where each
;				nn is any number (not range checked) and the
;				'-' may also be '/' or the DateSep character
; PType_Time		If the parameter is in the form nn:nn[x] or nn:nn:nn[x]
;				where each nn is any number (not range checked)
;				and x is either 'a' or 'p' (upper or lower case)
; PType_Other		Any other parameter; will be presumed to be a pathspec.
;
; During parsing, Number1, Number2, and Number3 may be modified.  If the
; parameter type is returned as PType_Date or PType_Time, then these variables
; will contain parsed representations of the first, second, and third numbers
; in the date or time specification.  For the hh:mm variation of the time
; parameter, Number3 (which would normally contain the number of seconds) is
; set to zero.
;
; In:	SI -> Command line parameter or whitespace before parameter
;	[Switchar] = DOS switch character
; Out:	CF = End of command tail flag:
;		CY = Hit end of command tail
;		NC = Found a parameter, and:
;			AX = Parameter type (PType_...)
;			DI -> Start of command line parameter
;			SI -> Char on which parsing finished
;			[Number1] = First value parsed
;			[Number2] = Second value parsed
;			[Number3] = Third value parsed or zero

PROC	NextParm	near
		mov	dl,[Switchar]	; Get switchar
ParmTypLoop:	lodsb			; Get character
		cmp	al,13		; Hit end of command tail?
		stc			; Prepare for end of command tail
		je	TailDone	; If end of command tail (just a RET)
		cmp	al," "		; Whitespace?
		jbe	ParmTypLoop	; If so, keep skipping
		mov	ah,PType_Switch	; Prepare for a switch parameter
		dec	si		; Point back to start of parameter
		push	si		; Store pointer to first parm char
		cmp	al,dl		; Check for initial switchar
		je	Go_GotType	; If so
		cmp	al,"-"		; Always support '-' as switchar
		je	Go_GotType	; If switch
CheckLoop:	call	ReadDec		; Try to parse a decimal number
		jcxz	MustBePath	; If failed, must be a pathspec
		mov	[Number1],dx	; Store first value found
		cmp	al,"-"		; Support date in form xx-xx-xx
		je	IsDateSep	; If probably a date
		cmp	al,"/"		; Support date in form xx/xx/xx
		je	IsDateSep	; If probably a date
		cmp	al,[DateSep]	; Support configured date separator
		jne	Try_Time	; If it can't possibly be a date
IsDateSep:	xchg	ax,bx		; Keep separator char in BL
		call	ReadDec		; Try for second number
		jcxz	MustBePath	; If failed
		cmp	al,bl		; Same separator?
		jne	MustBePath	; If not
		mov	[Number2],dx	; Store second value found
		call	ReadDec		; Try to parse one more number
		jcxz	MustBePath	; If failed
		mov	[Number3],dx	; Store third value found
		mov	ah,PType_Date	; It looks like a date
		jmp	SHORT CheckEndWhite ; Check we terminated on whitespace
Go_GotType:	jmp	SHORT GotType	; Branch stretcher
Try_Time:	cmp	al,":"		; Did we terminate on a colon?
		jne	MustBePath	; If not, is not a time value
		call	ReadDec		; Try for second number
		jcxz	MustBePath	; If failed
		mov	[Number2],dx	; Store second value found
		mov	[Number3],0	; Prepare for zero seconds value
		cmp	al,":"		; Check for final seconds value
		jne	NoSeconds	; If not
		call	ReadDec		; Get third number
		jcxz	MustBePath	; If failed
		mov	[Number3],dx	; Store seconds value
NoSeconds:	mov	ah,al		; Get character to AH
		or	ah,00100000b	; Convert to lower case
		cmp	ah,"a"		; Is it a.m?
		jne	Not_AM		; If not
		lodsb			; Get next character
		cmp	[Number1],12	; If a.m, is it 12:xx a.m?
		jmp	SHORT AdjustTime ; Adjust if so, and continue
Not_AM:		cmp	ah,"p"		; Is it p.m?
		jne	GotTime		; If not
		lodsb			; Get next character
		add	[Number1],12	; Convert 1:00p to 13:00 etc
		cmp	[Number1],24	; If was it 12:xx p.m?
AdjustTime:	jne	GotTime		; If not
		sub	[Number1],12	; If so, adjust
GotTime:	mov	ah,PType_Time	; Is a time
CheckEndWhite:	cmp	al," "		; Expect to terminate on whitespace
		jbe	GotType		; If so
MustBePath:	mov	ah,PType_Other	; Set parameter type
GotType:	pop	di		; Restore pointer to start of parameter
		mov	si,di
SkipParmLp:	lodsb			; Get char
		cmp	al," "		; Hit whitespace yet?
		ja	SkipParmLp	; If not, keep scanning
		dec	si		; Point back to terminator character
		mov	al,ah		; Get type to AL
		clr	ah		; Zero AH, clear carry
ParmTypEnd:	ret			; Return appropriate pointers
ENDP	NextParm

; Read decimal string ----------------------------------------------------------

; This function scans an ASCII representation of a decimal number and converts
; it to an unsigned 16-bit machine integer.
;
; In:	[DS:SI] = ASCII to be converted
; Out:	AL = Character which caused termination
;	CX = Count of valid digits found
;	DX = Binary value found
;	SI -> Character after terminator unless terminator was a C/R, in
;		which case SI points to the C/R
; Lost:	AX CX DX SI

PROC	ReadDec		near
		clr	<dx,cx>		; Clear return value and character count
ReadDec1:	lodsb			; Read a character
		sub	al,"0"		; Convert it to 0-9
		cmp	al,9		; Is it a digit?
		ja	ReadDec2	; If not
		cbw			; Clear AH
		push	ax		; Keep value to add after multiply
		mov	ax,10		; Get base multiply factor
		mul	dx		; Multiply current value by 10
		pop	dx		; Get new digit value back
		add	dx,ax		; Add to result of multiply
		inc	cx		; Increment count of chars found
		jmp	SHORT ReadDec1	; Loop
ReadDec2:	add	al,"0"		; Restore terminator character
		cmp	al,13		; Did we terminate on a C/R?
		jne	NotCRTerm	; If not
		dec	si		; If so, back up to it
NotCRTerm:	ret
ENDP	ReadDec

; Process a switch option found on the command tail ----------------------------

PROC	ProcessSwitch	near
		inc	di		; Point to first / next switch letter
		mov	al,[di]		; Get switch letter
		or	al,00100000b	; Convert to lower case
		cmp	al,"c"		; Check for -C option
		jne	NotC		; If not
		mov	[CFlag],1	; Set flag
		jmp	SHORT NextSw	; Continue
NotC:		cmp	al,"d"		; Check for -D
		jne	NotD		; If not
		mov	al,[SetDate]	; Get set-date flag
		or	al,[SetTime]	; Or set-time flag
		jnz	GoBadUsage2	; If conflicting switches given
		mov	[DFlag],1	; Set flag
		inc	di		; Point to start of pathname
		push	si		; Keep SI
		mov	si,di		; Get pointer to SI
		point	di,DPathname	; Point to buffer area
		mov	dx,di		; Set up DX for later
CopyDPath:	lodsb
		stosb			; Copy character
		cmp	al," "		; Check for end of string
		ja	CopyDPath	; Loop
		mov	[BYTE di-1],0	; Null-terminate
		pop	si		; Restore SI
		mov	ax,3D00h
		int	21h		; Open file for read only
		jnc	OpenedD		; If successful
		point	bx,DupErrEM	; Error opening file for -D
		jmp	ErrExit		; Go to exit handling
OpenedD:	mov	bx,ax		; Get handle
		mov	ax,5700h
		int	21h		; Get file date/time
		mov	[NewDDate],dx	; Store date
		mov	[NewDTime],cx	; Store time
		mov	ah,3Eh
		int	21h		; Close file
		ret			; Done switch
NotD:		cmp	al,"f"		; Check for -F option
		jne	GoBadUsage2	; If not, illegal switch or '?'
		mov	[FFlag],1	; Set flag
NextSw:		cmp	[BYTE di+1]," "	; More switch letters?
		ja	ProcessSwitch	; If so, process them
		ret			; Done
ENDP	ProcessSwitch

; Process a time value found on the command tail -------------------------------

; First, ensure that this is the first time value specified (if two times are
; specified, behaviour would be undefined, so we terminate with usage message).
; Then copy the parsed values into NewHour, NewMin, and NewSec, and check that
; they are all within the appropriate ranges.

GoBadUsage2:	jmp	BadUsage

PROC	ProcessTime	near
		xor	[SetTime],1	; Set flag if it was clear
		jz	GoBadUsage2	; If it was previously set, two times!
		cmp	[DFlag],0	; Check for -D
		jnz	GoBadUsage2	; If set
		point	si,Number1	; Point to parsed values
		lodsw			; Get hours value parsed
		mov	[NewHour],ax	; Store
		cmp	ax,24		; Check hours value within range
		jae	GoBadUsage2	; If not
		lodsw			; Get minutes value parsed
		mov	[NewMin],ax	; Store
		cmp	ax,60		; Make sure minutes are legal
		jae	GoBadUsage2	; If not
		lodsw			; Get seconds value (or 0 if not given)
		mov	[NewSec],ax	; Store
		cmp	ax,60		; Check legal
		jae	GoBadUsage2	; If not
		ret
ENDP	ProcessTime

; Process a date value found on the command tail -------------------------------

; First, ensure that this is the first date value specified (if two dates are
; specified, behaviour would be undefined, so we terminate with usage message).
; Then copy the parsed values into NewDay, NewMonth, and NewYear, using
; functions indexed through Parm1Tbl, Parm2Tbl, and Parm3Tbl, which also
; check that the parameters are within the appropriate ranges.

PROC	ProcessDate	near
		xor	[SetDate],1	; Set flag if it was clear
		jz	GoBadUsage2	; If was previously set, two dates!
		cmp	[DFlag],0	; Check for -D
		jnz	GoBadUsage2	; If set
		mov	bp,[DateCode2]	; Get date code (0/2/4)
		point	si,Number1	; Point to parsed values
		mov	ax,[si+0]	; First value
		or	ax,[si+2]	; Second value
		or	ax,[si+4]	; All values zero?
		jnz	DoDate		; If not, continue
		mov	[NewYear],ax	; Set year to 0 (1980)
		inc	ax		; To 1
		mov	[NewMonth],ax	; Month = January
		mov	[NewDay],ax	; Day = 1
		ret
DoDate:		lodsw			; Get first parameter
		call	[Parm1Tbl+bp]	; Call handler for first number
		lodsw			; Get next parameter
		call	[Parm2Tbl+bp]	; Call handler for second date number
		lodsw			; Get final date number
		call	[Parm3Tbl+bp]	; Call handler
DoneDate:	ret			; Finished this date parameter
ENDP	ProcessDate

; Handlers for day, month, and year values -------------------------------------

PROC	RdDay		near
		mov	[NewDay],ax
		dec	ax		; Don't accept zero
		cmp	ax,31		; Test for above day 31
GoBadUsageAE:	jae	GoBadUsage2	; If too high
		ret
ENDP	RdDay

PROC	RdMonth		near
		mov	[NewMonth],ax
		dec	ax		; Don't accept zero
		cmp	ax,12		; Check for month past 12
		jae	GoBadUsageAE	; If too high
		ret
ENDP	RdMonth

PROC	RdYear
		cmp	ax,100		; Short forms (80-99 or 00-79)?
		jae	FullForm	; If not, treat it as a full date
		sub	ax,80		; Convert 80-99 to 0-19
		jnb	GotYear		; If was 80-99
		add	ax,100		; Convert old 00-79 to 20-99
		jmp	SHORT GotYear	; Continue
FullForm:	sub	ax,1980		; Convert 1980-2107 to 0-127
GotYear:	mov	[NewYear],ax	; Store regardless of errors
		cmp	ax,120		; Check for illegal value
		jae	GoBadUsageAE	; If too high
		ret
ENDP	RdYear

PROC	SetHaveSpec	near
		mov	[HaveSpec],1	; Flag that we got at least one
ProcessRet:	ret			; Done
ENDP	SetHaveSpec

; The following function handles a single pathspec on the command line.
; It first calls QualPathspec (code from QUALPATH.ASM) to produce a fully
; qualified drive:path\filespec string at SearchSpec.  Then the code checks
; to see whether any wildcards were found by QualPathspec (returned in the
; carry flag from QualPathspec).  If no wildcards were found, it does a
; single touch operation on the pathname, which is presumably unambiguous.
; The single touch operation may or may not create the file if it does not
; already exist, depending on the CREATE equate and the setting of the -C
; option.  If wildcards were found, the code performs a simple wildcard
; expansion.  It finds the last backslash in the search specification, and
; calls the DOS findfirst/findnext functions to find all matching files.
; For each matching file, it appends the found file name just after the final
; backslash in the search string, then performs a single touch operation on
; that file.  If no matches are found, it reports an error.  The single touch
; operation is performed by the TouchSingle function, which uses a full
; pathname at SearchSpec as the name of the file to touch or create.

PROC	ProcessSpec	near
;				Func:	Process a pathspec parameter
;				In:	[DS:SI] = Pathspec in command tail
;				Out:	None
;				Lost:	AX BX CX DX SI DI
		point	di,SearchSpec	; Point to destination area
		call	QualPathspec	; Qualify it (code from QUALPATH.ASM)
		jc	Multiple	; If wildcards found
		jmp	TouchSingle	; If not, do single touch

Multiple:	point	dx,DTA		; Point to DTA
		mov	ah,1Ah
		int	21h		; Set DTA
		point	dx,SearchSpec	; Point to the full search pathspec
		clr	cx		; Standard files only please
		mov	ah,4Eh		; Find-first function
		int	21h		; Find first file matching pathspec
		jc	NoMatch		; If error
		test	ax,ax		; Error finding first?
		jnz	NoMatch		; If so

; Have matching filename in DTA

FindFilespec:	dec	di		; Scan backwards through searchspec
		cmp	[BYTE di-1],"\"	; Hit final backslash yet?
		jne	FindFilespec	; If not, keep looking
GotMatch:	push	si		; Keep pointer into command tail
		point	si,DTA+DTA_FName ; To start of filename found
		push	di		; Preserve append pointer
AppendFName:	lodsb			; Char from found filename
		stosb			; Append
		test	al,al		; Finished?
		jnz	AppendFName	; If not, loop
		pop	di		; Restore pointer to append point
		pop	si		; Restore command tail pointer
		call	TouchSingle	; Touch the file that matched
		mov	ah,4Fh		; Find-next function
		int	21h		; Find next matching file if any
		jc	ExpandDone	; If finished
		test	ax,ax		; Any error?
		jz	GotMatch	; If not, got another file - loop
ExpandDone:	ret
NoMatch:	point	bx,NoMatchM	; "No files matching <searchspec>"
PROC	ShowError	near
		call	ErrWriteMsg	; Write specified message to StdErr
		point	bx,SearchSpec	; Point to pathspec or pathname
		call	ErrWriteMsg	; Display it too
		point	bx,NewlineM	; Terminate with newline
		call	ErrWriteMsg	; Display that
		ret
ENDP	ShowError
ENDP	ProcessSpec

		INCLUDE	"QUALPATH.ASM"	; Code to qualify a pathspec

PROC	ConvertUC	near
;				Func:	Convert character in AL to upper case
;				In:	AL = Character to be converted
;				Out:	AL = Converted character
;				Lost:	AL
		cmp	al,"a"
		jb	NoConvertUC
		cmp	al,"z"
		ja	NoConvertUC
		sub	al,"a"-"A"
NoConvertUC:	ret
ENDP	ConvertUC

; The following function performs a single touch operation on a null-terminated
; unambiguous pathname at SearchSpec.  It must not destroy SI or DI.  If it
; cannot touch the file successfully, it reports an error.

PROC	TouchSingle	near
		mov	[Created],0	; Clear the just-created flag
		point	dx,SearchSpec	; Point to pathname of file to touch
		mov	ax,3D02h	; Open with read/write access
		int	21h		; Try it
		jnc	HaveOpen	; If successful
		cmp	ax,5		; Access denied?
		point	bx,FileErrM	; Prepare for yes
		je	GoShowError	; If so
		cmp	[CFlag],1-CREATE ; Couldn't open it - create it?
		point	bx,CantOpenM	; Prepare for no; file not found
		jnz	GoShowError	; If not creating files
		cmp	ax,2		; Did we get a simple 'file not found'?
		je	TryCreate	; If so.  If not, don't try to create it
CantCreate:	point	bx,NoCreateM	; Could not create file
GoShowError:	jmp	ShowError	; Display message and pathname

TryCreate:	clr	cx		; Standard file attributes please
		mov	ah,3Ch		; Create file for write
		int	21h		; Do it
		jc	CantCreate	; If error
		inc	[Created]	; Flag that we just created this file

HaveOpen:	mov	bx,ax		; File handle to BX

; Now have the file opened with read/write access; the file handle is in BX.
; If we just created the file, it will be zero length and will have the date
; and time of its creation, and the Created flag will be set.  In this case,
; pretend that both date and time were specified; this will force the program
; to use the date and time values obtained at startup (stored in the New...
; variables) and ensure that all created files will have the same time, rather
; than using the time of creation of the file, which might cause some files to
; be touched with different times if a 2-second boundary was crossed during
; the touch process.

		cmp	[DFlag],0	; D flag?
		mov	cx,[NewDTime]	; Prepare for yes
		mov	dx,[NewDDate]
		jnz	NoScanFile	; If so, just set date and time.
		mov	ax,5700h
		int	21h		; Get file date and time
		cmp	[Created],0	; Did we create it?
		jnz	DoSetDate	; If so, set date
		cmp	[SetDate],0	; Update Date if specified
		jz	NoSetDate	; If not specified
DoSetDate:	push	cx		; Don't destroy CX - contains file time
		mov	dx,[NewYear]	; Get year
		mov	cl,4
		shl	dx,cl		; Shift it up
		or	dx,[NewMonth]	; Combine the month into the value
		inc	cx
		shl	dx,cl		; Shift them up
		or	dx,[NewDay]	; Combine the day into the value
		pop	cx		; Restore the file time
NoSetDate:	cmp	[Created],0	; Did we create it?
		jnz	DoSetTime	; If so, set time
		cmp	[SetTime],0	; Update Time if specified
		jz	NoSetTime	; If not
DoSetTime:	mov	ax,[NewHour]	; If so, get hour
		mov	cl,6
		shl	ax,cl		; Shift it up
		or	ax,[NewMin]	; Combine minutes into the value
		dec	cx
		shl	ax,cl		; Shift them up
		mov	cx,[NewSec]	; Get seconds
		shr	cx,1		; Halve the value
		or	cx,ax		; Combine with hours and minutes
NoSetTime:	cmp	[FFlag],0	; Scan the file for a version string?
		jz	NoScanFile	; If not
		cmp	[Created],0	; Did we create the file?
		jnz	NoScanFile	; If so, don't scan for version number
		call	ScanVersion	; Scan for version string; return in CX
		jc	CloseErr	; If error, close file and display msg

; Now have new date and time for file in DX and CX respectively.

NoScanFile:	mov	ax,5701h
		int	21h		; Set file Date and Time
		point	dx,TouchErrM	; Prepare for error touching file
		jnc	Successful	; If successful

; Error exit point - close file and issue error message pointed to by AX

CloseErr:	push	ax		; Keep pointer to message
		mov	ah,3Eh
		int	21h		; Close the file
		pop	bx		; Error message pointer to BX
		jmp	ShowError	; Display message and continue

Successful:	mov	ah,3Eh
		int	21h		; Close the file
		ret			; Successful
ENDP	TouchSingle

; The following function scans an open file for a version string and creates a
; time value from that string, returning the time value in CX using the DOS
; directory entry time format.	The file handle is provided in BX on entry.
; This function must preserve BX and DX.  It returns carry set if a version
; string cannot be found or if the version is unusable (middle digit greater
; than 5), and a pointer to the error message in AX.

PROC	ScanVersion	near
		push	dx		; Preserve DX
		point	dx,InBuffer	; Point at area to load file into
		mov	cx,InBufSize	; Maximum amount of file to load
		mov	ah,3Fh
		int	21h		; Read starting part of file
		jc	VersionError	; If error reading file
		xchg	ax,cx		; Count of bytes read to CX
		jcxz	VersionError	; If no bytes read
		mov	di,dx		; Point to start of buffer
		add	di,cx		; Point past end of data read
		xor	ax,ax		; Zero
		stosw			; Stuff some null terminators

ScanPass:	push	cx		; Keep count of bytes to scan
		mov	di,dx		; Point to search point in buffer
		mov	al,"V"		; Try for upper case "V" first
		repne	scasb		; Find it
		pop	cx		; Restore count
		mov	si,di		; End-of-scan pointer to SI
		push	cx
		mov	di,dx		; Point to search point in buffer
		mov	al,"v"		; Try for lower case "v" next
		repne	scasb		; Find it
		pop	cx		; Restore count
		cmp	si,di		; Which 'V' did we find first?
		jbe	GotFindPoint	; If SI already points to first one
		mov	si,di		; If not, point SI to the first one
GotFindPoint:	add	cx,dx
		sub	cx,si		; Calculate number of bytes remaining
		mov	dx,si		; Update scan continuation point
		jcxz	VersionError	; If scanned all data

; Now have found 'V' or 'v'.  Now check for the supported version strings:
; "VERSION ", "VERS ", "VERS. ", "VER ", "VER. ", "V ", "V.", and "V".
; SI points to the character just past the first 'V'.

		mov	al,[si]		; Get character following 'v'
		call	ConvertUC	; Convert to upper case
		cmp	al," "		; Check for space
		je	Digits1		; If found 'V '
		cmp	al,"."		; Check for dot
		je	Digits1		; If found 'V.'
		cmp	al,"E"		; Check for more of 'VER'
		je	TryVersion2	; If so, continue
		cmp	al,"0"		; Check for immediate digit
		jb	ScanPass	; If not, no match
		cmp	al,"9"		; Check other limit
		ja	ScanPass	; If not, no match
		jmp	SHORT Digits0	; If found 'V'

TryVersion2:	lodsb			; Skip the 'E'
		lodsb			; Get the 'R' (presumably)
		call	ConvertUC	; Convert to upper case
		cmp	al,"R"		; Verify that it's 'R'
		jne	ScanPass	; If not
		lodsb			; Next character
		cmp	al," "		; Check for space
		je	Digits0		; If found 'VER '
		cmp	al,"."		; Check for dot
		jne	NotDot2		; If not
		lodsb			; Try for 'VER. '
		cmp	al," "
		je	Digits0		; If found 'VER. '
		jmp	SHORT ScanPass	; Try again
NotDot2:	call	ConvertUC	; Convert to upper case
		cmp	al,"S"		; Expect 'S'
		jne	ScanPass	; If not, keep looking
		lodsb			; Next character
		cmp	al," "		; Check for space
		je	Digits0		; If found 'VERS '
		cmp	al,"."		; Check for dot
		jne	NotDot3		; If not
		lodsb			; Try for 'VERS. '
		cmp	al," "
		je	Digits0		; If found 'VERS. '
GoScanPass:	jmp	SHORT ScanPass	; Try again

VersionError:	point	ax,NoVersionM	; No version found
		jmp	SHORT ScanVersErr ; Go to error exit point

NotDot3:	call	ConvertUC	; Had 'VERS', check for 'VERSION '
		cmp	al,"I"		; Expect 'I'
		jne	GoScanPass	; If not
		lodsb			; Next char
		call	ConvertUC	; To upper case
		cmp	al,"O"		; Expect 'O'
		jne	GoScanPass	; If not
		lodsb			; Next char
		call	ConvertUC	; To upper case
		cmp	al,"N"		; Expect 'N'
		jne	GoScanPass	; If not
		lodsb			; Next char
		cmp	al," "		; Expect ' '
		jne	GoScanPass	; If not
		DB	3Ch		; Skip next 1-byte instruction
Digits1:	inc	si		; Skip a char

; SI now points to what should be digits in any of the following forms:
; "x", "x.y", "x.yz", "x.y.z"

Digits0:	lodsb			; Get first digit
		sub	al,"0"		; Convert to 0-9
		cmp	al,10		; Validate
		jae	GoScanPass	; If bad
		mov	ch,al		; Keep first version number in CH
		xor	dx,dx		; Init other version numbers
		lodsb			; Next digit
		cmp	al,"."		; Expect dot
		jne	GotVersion	; If not
		lodsb			; Next digit
		sub	al,"0"		; Convert to 0-9
		cmp	al,10		; Validate
		jae	GotVersion	; If bad, use what we have so far
		mov	dh,al		; Keep middle version digit
		lodsb			; Next character
		cmp	al,"."		; Check for dot
		jne	NotDot4		; If not
		lodsb			; If dot, skip it
NotDot4:	sub	al,"0"		; Convert to 0-9
		cmp	al,10		; Validate
		jae	GotVersion	; If bad, use what we have so far
		mov	dl,al		; Keep last digit of version number

GotVersion:
		mov	al,10
		mul	dh		; AL = middle digit x 10
		add	dl,al		; DL = bottom two digits (minutes)
		mov	cl,6		; Shift count
		mov	al,ch		; First digit to AL (AH already zero)
		shl	ax,cl		; Shift hour up 6 bits
		or	al,dl		; Combine with last two digits
		dec	cx		; Shift count = 5
		shl	ax,cl		; Shift to appropriate position
		xchg	ax,cx		; To CX

		cmp	dh,6		; Validate middle version digit
		jb	ScanVersOK	; If alright
		point	ax,BadVersionM	; If bad
ScanVersErr:	stc
		DB	0B2h		; MOV DL,nn (skip next CLC instruction)
ScanVersOK:	clc			; Flag no error; CX contains version
		pop	dx		; Restore DX
		ret
ENDP	ScanVersion

; Working stack area -----------------------------------------------------------

		ALIGN	2

WorkStack	DB	512 DUP(?)	; Working stack
WorkStackTop	= $

; Uninitialised transient data -------------------------------------------------

SearchSpec	DB	2 DUP(?)	; Drive letter and colon
Pathspec	DB	66 DUP(?)	; Pathspec contents (search and touch)

DPathname	DB	128 DUP(?)	; Storage for pathname for -D option

QualPathDTA	DB	DTAAlloc DUP(?)	; DTA for QualPathspec function
DTA		DB	DTAAlloc DUP(?)	; DTA storage for date and wildcard exp.

InBuffer	DB	InBufSize DUP(?) ; File deblocking storage (-F option)
		DB	2 DUP(?)	; Extra space for null terminators

; End of transient portion -----------------------------------------------------

		MASM
FreeSpace	=	$
MemParas	=	(OFFSET (FreeSpace-@curseg+15) SHR 4)
		prexp	<Transient size:> %(MemParas SHL 4) < bytes>
		IDEAL

; Note - if the transient size starts approaching 65536, reduce InBufSize.

		ENDS	ComFile
		END	Main

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