; ; PETASCII.MAC -- Version 1.0 ; ; USAGE: ; ; PETASCII {d:} {d:}{} ; ; Copies the input file (first file reference) to the output file ; (second file reference), converting PETASCII characters to ASCII ; characters. Also inserts linefeeds after carriage returns where ; they do not already exist. ; ; If the drivecode d: of the input file is omitted the current drive ; is assumed. An input filename is required. ; ; If the output file reference is omitted, it will be given the same ; name as the input file. Any file of the same name on the destination ; drive will be renamed to filetype .BAK, which will preserve the ; input file if both input and output use the same drive. If the ; output filename does not include a filetype, the output file will ; be given the same filetype as the input file. If a drive speci- ; fication is not given, output will default to the current drive. ; ; Version 1.0, 9/17/87, Gene Pizzetta -- for the I/O routines I have ; relied heavily on David E. Cortesi, "A Programmer's Notebook: ; Utilities for CP/M-80" ; ; This program was developed using SLRMAC from SLR Systems. I will ; also assemble with MAC, but you will need to have Z80.LIB on your ; default disk. ; WarmStart equ 0000h ; vector to BIOS re-boot CpmBasePage equ high WarmStart Bdos equ 0005h ; vector to CP/M BDOS BdosAddress equ 0006h ; address in the Bdos jump CpmFcb equ 005Ch ; default FCB, file operand 1 CpmFcb2 equ CpmFcb+10h ; file operand 2 CpmBuffer equ 0080h ; default file buffer 80..FFh CpmTail equ CpmBuffer ; command tail TransientArea equ 0100h ; start of the .COM file SuccessCode equ 0000h ; program success code FailCode equ 0FF00h ; program failure code ; ; BDOS functions BdosKeyin equ 01h ; one console input byte BdosType equ 02h ; type byte at console BdosPrint equ 05h ; print one byte BdosString equ 09h ; type "string$" at console BdosLine equ 0Ah ; read a buffered console line BdosTestCon equ 0Bh ; test console, A > 0 if key hit BdosSetDrive equ 0Eh ; select default drivecode BdosOpen equ 0Fh ; open a disk file BdosClose equ 10h ; close (output) file BdosSrch1 equ 11h ; initial directory search BdosSrchn equ 12h ; successive directory searches BdosErase equ 13h ; erase a file BdosRead equ 14h ; read disk sequential BdosWrite equ 15h ; write disk sequential BdosMake equ 16h ; create a file directory entry BdosRename equ 17h ; rename file BdosDrive equ 19h ; get default drive number BdosSetBuffer equ 1Ah ; set file buffer address BdosGetAlloc equ 1Bh ; get address of allocation vector BdosGetDPB equ 1Fh ; get address of DPB BdosFreeSpace equ 2Eh ; get disk free space BdosReturn equ 6Ch ; program return code BdosParse equ 98h ; parse filename ; ; ASCII values AsciiLF equ 0Ah ; LineFeed AsciiCR equ 0Dh ; CarriageReturn CpmEof equ 1Ah ; ^Z, end of ascii file AsciiBlank equ 20h ; blank AsciiDEL equ 7Fh ; DEL, highest valid byte ; MACLIB Z80 ; ; Program entry macro: find bottom of Bdos and set the stack ; there. Call label Main with HL --> end of storage. Provide ; code to support the ABORT macro and the command tail. ; PROLOG MACRO ORG TransientArea ;; start at TPA LHLD BdosAddress ;; HL --> BDOS base MVI L,255 INX H ;; HL --> page above BDOS DCR H ;; HL --> page below BDOS SPHL ;; SP --> high storage DCR H ;; Allow 256 byte stack DCX H ;; HL --> last usable byte CALL Main ;; call mainline code ; ; On normal exit, the main routine will return here ... EPILOG: JMP WarmStart ;; do a warm start ; ; If a fatal error is detected the ABORT macro will come ; here, with the address of a "message$" string on top of ; the stack and all registers as they were. When debugging ; with DDT, set a breakpoint here to trap an abort. When ; using SID, set a pass-counter at label ERROREXIT. ; ERROREXIT: POP D ;; type the string on MVI C,BdosString ;; ..the console CALL Bdos LXI D,FailCode ;; kill any active MVI C,BdosReturn ;; ..submit file CALL Bdos JMP WarmStart ;; and terminate ENDM ; ; Bdos-service macro: call Bdos for a service, saving all ; registers except A and sometimes HL. Load DE with the ; service parameter if one was specified. ; SERVICE MACRO ?S,?DE PUSH B PUSH D IF (?S NE 12) AND (?S NE 24) AND (?S NE 27) IF (?S NE 29) AND (?S NE 31) PUSH H ENDIF ENDIF IF NOT NUL ?DE LXI D,?DE ENDIF MVI C,?S CALL Bdos IF (?S NE 12) AND (?S NE 24) AND (?S NE 27) IF (?S NE 29) AND (?S NE 31) POP H ENDIF ENDIF POP D POP B ENDM ; ; Abort-the-program-with-a-message macro: The operand ; is the address of a message in storage. ; ; The macro puts the address of the message on the ; stack, preserving all registers for use in debugging. It ; branches to the ERROREXIT assembled by PROLOG, where the ; message will be printed. ; ABORT MACRO ?MSG PUSH H LXI H,?MSG XTHL JMP ERROREXIT ENDM ; ; Macro to make it easy to origin the assembler to the next ; page boundary. ; ORGPAGE MACRO ORG ($+255) AND 0FF00h ENDM ; ; Macro to test the console and, if a key has been hit, to ; jump to a named location. No registers are changed, so ; the macro can be inserted anywhere in the code. ; TESTCON MACRO WHERE local skippit local endtest push h push d push b push psw mvi c,BdosTestCon call Bdos ;; test the console lxi h,endtest ;; assume no key hit ora a ;; was one? JRZ skippit ;; jump if not lxi h,WHERE ;; yes, prepare to exit skippit pop psw ;; restore A, flags, pop b ;; regs BC, pop d ;; ..and DE xthl ;; restore H, stack address ret ;; go to 'endtest' or WHERE endtest equ $ ENDM ; ; Program begins here ... ; PROLOG ; ; Input Variables: used by SetUpInput and GetChar. ; PFCB dw CpmBuffer+1 ; FCB addresses for BdosParse dw 0 ; (service function 152) InFCB ds 36 ; Input FCB InIndex db 0 ; Index over CpmBuffer ; ; Output Variables: used by SetUpOutput, PutChar, DumpOut. ; OutFCB ds 36 ; Output FCB OutType ds 3 ; hold for real output filetype OutBuffer dw 0 ; address of OutBuffer OutIndex dw 0 ; index over OutBuffer OutLimit dw 0 ; negative of size of OutBuffer DollarType db '$$$' ; type of an output workfile BAKType db 'BAK' ; type of BAK file ; ; Messages: ; MsgNoName db 'An input filename is required.$' MsgNoFile db 'Input file not found.$' MsgNoBuffer db 'Not enough room for an output buffer.$' MsgNoParseIn db 'Illegal input filename.$' MsgNoParseOut db 'Illegal output filename.$' MsgAmbig db 'The output file may not be ambiguous.$' MsgNoMake db 'Can''t create the work file.$' MsgWrite db 'Error writing the work file.$' MsgNoClose db 'Error closing the work file.$' MsgRename db 'Error renaming work file to proper name.$' MsgRenameEx db 'Error renaming existing file to .BAK.$' MsgSignOn: db 'PETASCII Version 1.0',AsciiCR,AsciiLF,'$' ; ; the main program -- initialization ; Main: lxi d,MsgSignOn ; print sign on message mvi c,BdosString call Bdos ; call SetUpInput ; open the input file lxi b,InFCB ; BC --> default fileref lxi d,OBspace ; DE --> start of buffer space ; prolog sets HL --> end of buffer space call SetUpOutput ; prepare the output stuff ; ; the main program -- main loop ; call GetChar ; get first character ; MainWhile: cpi CpmEof ; while not EOF ... JRZ MainEnd ; cpi AsciiCR ; is it a carriage return? JRZ IsReturn ; (yes, handle it) ; cpi 041h ; is it control, number, symbol? JRC MainPut ; (yes, print it) ; cpi 05Bh ; is it PETASCII lowercase? JRC MakeLower ; (yes, make ASCII lower) ; cpi 061h ; is it a symbol? JRC MainPut ; (yes, print it) ; cpi 07Bh ; is it PETASCII low uppercase? JRC MakeUpper1 ; (yes, make ASCII upper) ; cpi 0C1h ; is it a symbol? JRC ClearHigh ; (yes, handle it) ; cpi 0DBh ; is it PETASCII high uppercase? JRC MakeUpper2 ; (yes, make ASCII upper) ; JR ClearHigh ; handle any leftovers ; ; MakeLower: it's a PETASCII lowercase character which we make ; into ASCII lowercase by adding 32 to it ... ; MakeLower: adi 20h JR MainPut ; ; MakeUpper1: it's a PETASCII uppercase character in the 61h-7Ah ; range which we make into ASCII uppercase by subtracting 32 ... ; MakeUpper1: sui 20h JR MainPut ; ; MakeUpper2: it's a PETASCII uppercase character in the C1h-DAh ; range which we make into ASCII uppercase by substracting 128 ... ; MakeUpper2: sui 80h JR MainPut ; ; IsReturn: it's a carriage return. We send it and then determine ; if the next character is a linefeed. If it's not, we send one. ; IsReturn: call PutChar ; send the carriage return call GetChar ; get the next character cpi AsciiLF ; is it a linefeed? JRZ MainPut ; (yes, send it) push psw ; no, save the character mvi a,AsciiLF call PutChar ; send a linefeed pop psw ; recover character jmp MainWhile ; ..and loop ; ; ClearHigh: we have a character with the high bit set so we must ; reset it and fall through to send it ... ; ClearHigh: ani AsciiDEL ; ; MainPut: we come here with the adjusted character in A. We ; must send it, get the next character, and loop ... ; MainPut: call PutChar call GetChar jmp MainWhile ; ..end while ; MainEnd: call FinishOutput ; complete output file ret ; end main ; ; I/O subroutines -- ; ; GetChar: return the next byte of input in A. The default ; buffer at 80h..FFh is used. Since we are dealing with ASCII ; files, CpmEof (^Z) is the end of file mark. When it appears, ; don't advance the index past it, so further call will keep on ; returning CpmEof indefinitely. ; Preserves: BC, DE, HL ; GetChar: push h ; save a work register lda InIndex inr a ; InIndex = InIndex + 1 JRNZ GetChar2 ; ; InIndex was FFh, the buffer is empty. Put EOF in the buffer ; so that if physical end of file occurs, we will see it as ; logical end of file instead. ; push d push b lxi h,CpmBuffer mvi m,CpmEof ; put CpmEof in buffer xchg ; DE --> buffer mvi c,BdosSetBuffer call Bdos ; set buffer address lxi d,InFCB mvi c,BdosRead call Bdos ; read a record pop b pop d mvi a,80h ; InIndex = 80h ; ; At this point there is either data or CpmEof at the ; location addressed by InIndex = A. ; GetChar2: sta InIndex mvi h,CpmBasePage ; form an address mov l,a mov a,m ; A = data cpi CpmEof ; if it's EOF, JRNZ GetChar3 lxi h,InIndex dcr m ; ..InIndex = InIndex - 1 GetChar3: pop h ret ; ; PutChar: write the byte in A into the output buffer. If the ; buffer is full, (OutIndex=OutLimit), then dump it first. ; Preserves: All ; PutChar: push h push d lhld OutIndex xchg ; DE = OutIndex lhld OutLimit dad d ; carry = OutIndex=OutLimit JRNC PutChar2 ; call DumpOut ; dump the buffer to the file lxi d,0 ; OutIndex = 0 ; PutChar2: lhld OutBuffer dad d ; HL --> next output byte spot mov m,a inx d xchg shld OutIndex ; OutIndex = OutIndex +1 pop d pop h ret ; ; DumpOut: write buffered output data to the work file. Called ; in two cases: (1) when the buffer is full (OutIndex=OutLimit) ; and (2) at the end of the program when OutIndex is ; unpredictable and might even be zero. The buffer must be a ; multiple of 128 bytes, but needn't be on any special boundary. ; Preserves: All ; DumpOut: push psw push h push d push b lhld OutIndex mov a,h ora l ; is there any data at all? JRZ DumpOut9 ; ..no data? do nothing. ; ; When the buffer is full, OutIndex mod 128 is zero. At the ; end of the run, though, OutIndex could be in mid-record. In ; that case, fill the last record with EOF marks. ; xchg ; DE = OutIndex lhld OutBuffer dad d ; HL --> next output byte DumpOut2: mov a,e ani 128-1 ; test OutIndex mod 128 JRZ DumpOut3 ; (it was zero) mvi m,CpmEof ; ..stick in an EOF inx d ; ..step OutIndex inx h ; .. and step storage pointer JR DumpOut2 ; ; The buffer now contains a whole number of 128-byte records. ; Write them all. Keep the limit index in BC, and the current ; record's index in DE. ; DumpOut3: mov b,d ; BC = index of last record mov c,e dcx b ; ..less one lxi d,0 ; DE = starting index of 0 DumpOut4: lhld OutBuffer dad d xchg ; DE --> record, HL has index SERVICE BdosSetBuffer SERVICE BdosWrite,OutFCB ora a ; check for disk full, etc. JRNZ DumpOutError ; lxi d,128 dad d ; step index (in HL) xchg ; DE = index to next record call CmpBD ; set flags fro BC vs DE JRNC DumpOut4 ; continue if limit <= index ; DumpOut9: pop b pop d pop h pop psw ret ; DumpOutError: ABORT MsgWrite ; ; CmpBD: compare BC to DE as a 16-bit unsigned integer, setting ; the machine flags as for a CMP instruction. ; Preserves: All except the flags ; CmpBD: push h mov h,a mov a,b cmp d JRNZ CmpBDZ mov a,c cmp e CmpBDZ: mov a,h pop h ret ; ; SetUpInput: initialize the input file for utility I/O with ; GetChar. Requires definition of InFCB, InIndex (InVars). ; Preserves: All ; SetUpInput: push psw push b push d push h ; ; Prepare the input file for reading ; lxi h,InFCB ; put addr of InFCB into PFCB shld PFCB+2 lxi d,PFCB ; parse filename to InFCB mvi c,BdosParse call Bdos ; ; If input filename omitted, abort ; shld PFCB ; save return code mvi a,0FFh ; check return code cmp h JRNZ SUI1 ; parsed okay cmp l JRNZ SUI1 ABORT MsgNoParseIn SUI1: lxi h,InFCB+1 mvi a,AsciiBlank cmp m JRNZ SUI2 ; parse successful, continue ABORT MsgNoName ; ; get an input drivecode for the FCB ; SUI2: dcx h mov a,m ; A = given drivecode ora a ; was it omitted? JRNZ SUI3 SERVICE BdosDrive ; get, e.g., A=00, B=01 inr a ; make it A=01, B=02, etc. SUI3: sta InFCB ; explicit drivecode in FCB ; ; open the input FCB ; SERVICE BdosOpen,InFCB inr a ; if no file exists, abort JRNZ SUI4 ABORT MsgNoFile ; ; InIndex = FFh ; SUI4: mvi a,0FFh sta InIndex ; pop h pop d pop b pop psw ret ; ; FillZero: replicate 00h, beginning at DE, for BC bytes ; FillZero: push psw xra a JR FillX ; ; FillA: replicate the byte in A, beginning at DE, for BC bytes. ; If the length is 2 or greater, use MoveHtoD to do the work. If ; the length is 1, that won't work. ; Preserves: A, BC, HL ; Returns: DE advanced by BC ; FillA: push psw FillX: ; FillZero joins here push h stax d ; fill one byte inx d ; ..step, dcx b ; ..count the one mov h,a ; save fill byte mov a,b ; guard against case of BC=0001 ora c JRZ FillZ mov a,h ; recover fill byte mov h,d ; create address of fill data mov l,e ; ..in storage dcx h ; HL --> first byte, DE --> second call MoveHtoD ; do propogation-move FillZ: inx b ; restore entry value of b pop h pop psw ret ; ; MoveHtoD: Z80 LDIR instruction. It does [HL++] --> [DE++] for ; BC times. BC must be 1 or more. BC is preserved. That is handy for ; repetitive use of the routine. ; MoveHtoD: push b ldir pop b ret ; ; Delimiter: HL addresses a byte. Find out if it is one of the ; standard CP/M token delimiters. ; Preserves: BC, DE, HL ; Returns: Z flag true if byte is a delimiter ; A = the byte itself ; Note: The tests are made in descending order by ASCII value. ; This "rnc" returns if the byte is equal, or if it is greater ; and hence cannot possibly be one of the bytes tested later. ; Delimiter: mov a,m cpi ']' ; delimiters for option lists rnc cpi '[' rnc cpi '=' ; delimits filerefs in, e.g., ren rnc cpi ';' ; delimits passwords (BDOS 3) rnc cpi '/' ; delimits optional parameters rnc cpi '.' ; delimits filetypes rnc cpi ',' ; delimits tokens (supposedly) rnc cpi ' ' ; only token delimiter that works rnc cmp a ; control character -- force Z true ret ; ; SetUpOutput: prepare the utility output mechanism. ; A missing filetype from the command line will be filled in with ; the filetype of the input file (from InFCB). ; Input: DE --> start of output buffer ; HL --> end of output buffer (as PROLOG gives it) ; CpmFcb2 contains output fileref ; Preserves: All ; Note: this code assumes that the OutVars and FileMessages ; units from this file have been included. ; It aborts the program under these conditions: ; MsgAmbigOut, if the output fileref is ambiguous ; MsgNoBuffer, if the output buffer is less than 256 bytes ; MsgNoMake, if "outname.$$$" can't be defined ; MsgNoClose, if "outname.$$$" doesn't close successfully ; MsgNoRename, if the rename to change the type doesn't work ; MsgNoParseIn, if input filename is illegal ; MsgNoParseOut, if output filename is illegal ; SetUpOutput: push psw push b push d push h ; ; prepare the buffer address, index, and index limit. The limit ; is the 2s complement of the buffer-size -- see DumpOut for use ; xchg ; HL --> start of buffer mov a,l ; ensure buffer starts on 128-multiple dcr a ori 128-1 mov l,a inx h shld OutBuffer xchg ; DE --> start, HL --> end mov a,d cma mov d,a mov a,e cma mov e,a inx d ; DE = 2s complement of buffer start dad d ; HL = buffer size mov a,h ; ..which we want to ensure is at ora a ; least 256 bytes ... JRNZ SUO2 ; (it is) ABORT MsgNoBuffer SUO2: mov a,l ; ensure buffer size is a multiple ani 128 ; ..of 128 bytes, too cma mov l,a mov a,h cma mov h,a inx h shld OutLimit ; OutLimit = 2s complement of size lxi h,0 shld OutIndex ; OutIndex = 0 ; ; fill filename and type in OutFCB with spaces ; mvi a,AsciiBlank lxi b,11 lxi d,OutFCB+1 call FillA ; ; make the rest of the FCB zeroes ; lxi b,36-12 call FillZero ; ; see if there's an output fileref in the command tail and, if so, ; parse it into the OutFCB ; lda PFCB ; is there more in the tail cpi 0 ; if the return code = 0, JRNZ SUO2a ; ..there's no more lda PFCB+1 cpi 0 JRNZ SUO2a JR SUO2b ; (no more, get default) SUO2a: lxi h,OutFCB ; there's more ... try parsing shld PFCB+2 ; ..it to see if it's a fileref lxi d,PFCB mvi c,BdosParse ; parse second fileref call Bdos mvi a,0FFh ; check return code cmp h JRNZ SUO2b ; parsed okay cmp l JRNZ SUO2b ABORT MsgNoParseOut ; ; Get an output drivecode for the FCB ; SUO2b: lxi h,OutFCB mov a,m ; A = given drivecode ora a ; was it omitted? JRNZ SUO2c ; (no, store it) SERVICE BdosDrive ; get, e.g., A=00, B=01 inr a ; make it A=01, B=02, etc. ; SUO2c: sta OutFCB ; ; get an output filename for the FCB, and make sure that ; the name given is explicit (has no question marks). ; SUO3: lxi b,8 lxi d,OutFCB+1 ; DE --> destination lxi h,InFCB+1 ; HL --> 2nd operand filename lda OutFCB+1 ; was it omitted? cpi AsciiBlank cz MoveHtoD ; if so, copy over default lxi h,OutFCB+1 ; HL --> the final filename mvi a,'?' ; BC still has the length call ScanForA ; look for "?" in name JRNZ SUO4 ; (there are none) ABORT MsgAmbig ; ; get an output filetype and save it. Make sure it isn't ; ambiguous, either. ; SUO4: lxi b,3 ; BC = length of filetype lxi d,OutType ; DE --> destination (not FCB) lxi h,OutFCB+9 ; HL --> 2nd operand filetype mov a,m cpi AsciiBlank ; was one given? JRNZ SUO5 ; (yes, move it) lxi h,InFCB+9 ; ..no, use default filetype SUO5: call MoveHtoD ; move filetype to OutType lxi h,OutType ; ..then check the chosen type mvi a,'?' call ScanForA ; look for "?" in type JRNZ SUO6 ; (none found) ABORT MsgAmbig ; ; Prepare a workfile for output ; move '$$$' to the output FCB filetype ; SUO6: lxi h,DollarType ; "$$$" for use in work file lxi d,OutFCB+9 lxi b,3 ; length of filetype call MoveHtoD ; ; erase any existing workfile of that name ; (note: if drive is R/O, we die here) ; SERVICE BdosErase,OutFCB ; ; make a file of the workfile's name ; SERVICE BdosMake,OutFCB inr a ; if the make fails, abort JRNZ SUO7 ABORT MsgNoMake ; SUO7: pop h pop d pop b pop psw ret ; ; FinishOutput: close the work file and rename to proper type. ; Preserves: All ; FinishOutput: push psw push b push d push h ; call DumpOut ; write any buffered output ; lxi d,OutFCB ; then close the work file mvi c,BdosClose call Bdos inr a ; FFh signals failure JRNZ FinO2 ; (wasn't that) ABORT MsgNoClose ; ; OutFCB presently named "y:outname.$$$". Stuff in the type ; of the real output file, and erase that file. This is where, ; if the output fileref had any question marks, we could do a ; lot of damage! ; FinO2: lhld OutType lda OutType+2 shld OutFCB+9 sta OutFCB+9+2 lxi d,OutFCB mvi c,BdosOpen call Bdos ora a ; does the file exist? JRNZ FinO2a ; (no, continue) ; lhld BAKType lda BAKType+2 shld OutFCB+9 sta OutFCB+9+2 lxi d,OutFCB mvi c,BdosErase ; erase any .BAK file existing call Bdos ; lxi h,OutFCB ; move .BAK filename to new name lxi d,OutFCB+16 lxi b,16 call MoveHtoD lhld OutType ; put output filename in old name lda OutType+2 shld OutFCB+9 sta OutFCB+9+2 lxi d,OutFCB mvi c,BdosRename ; rename existing file to filename.BAK call Bdos inr a ; FFh means failure JRNZ FinO2a ; (not the case) ABORT MsgRenameEx ; ; Now set the FCB up so that it reads: ; OutFCB+0: y outname $$$ ; OutFCB+16: - outname typ ; and do the rename function to rename the work file. ; FinO2a: lxi h,OutFCB lxi d,OutFCB+16 lxi b,16 call MoveHtoD lhld DollarType mov a,h shld OutFCB+9 sta OutFCB+9+2 lxi d,OutFCB mvi c,BdosRename call Bdos inr a ; FFh means failure JRNZ FinO3 ; (not the case) ABORT MsgRename ; FinO3: pop h pop d pop b pop psw ret ; ; ScanForA: scan BC bytes of HL --> text, looking for a match to ; the byte in A. Return BC and HL updated, and the Z flag true ; if a match is found or false if it is not. ; Input: HL --> text ; BC = count of bytes to scan (0000h means 65536) ; A = the byte to search for ; Preserves: A, DE ; Returns: HL incremented from 1 to BC bytes (HL points to ; the byte after the match, or to the byte after ; the scanned field if no match was found). ; BC decremented by at least 1 (BC decremented to ; zero if no match was found). ; Z flag true if a match was found. ; Note: this is the Z80 CPIR instruction. Note also that input ; of BC=0000 will cause a scan of all of storage. ; ScanForA: CCIR ret ; ; The end of the program, and the start of the buffer space. ; OBSpace equ $ ; end ;