; CHKDIR.MAC ; Vers equ 10 ; version SubVers equ ' ' ; revision level ; ; USAGE: ; ; CHKDIR dir: ; ; Checks for the following conditions: ; Duplicate directory entries ; User # > 31 ; Extent # > 31 ; Illegal characters in the filename ; Record count > 80h ; Allocation group assigned to multiple files, or to same file twice ; Reports 0-length files ; ; Requirements: ; Hardware: Z80-compatible processor ; Software: ZCPR3 operating system (with BDOS or ZRDOS) ; ; System addresses . . . ; WBoot equ 0 Bdos equ 5 CpmFcb equ 5Ch ; default file control block AltFcb equ 6Ch ; alternate file control block CpmBuf equ 80h ; CP/M default buffer ; ; BDOS functions . . . ; GetVer equ 12 ; get CP/M version SelDrv equ 14 ; select drive GetDsk equ 25 ; get current drive ; ; ASCII characters . . . ; BEL equ 7 CR equ 0Dh LF equ 0Ah ; ; SYSLIB and Z3LIB modules ; ext eprint,cout,pfn3,padc,pa2hc,phlfdc,phl4hc,$memry ext z3init,z3log,getmsg,puter2,zsyschk,gzmtop,prtname ext inverror ; public print ; for zsyschk ; MACLIB Z80 ; extended Intel mnemonics ; jmp Start ; db 'Z3ENV' ; ZCPR3 header db 1 Z3EAdr: dw 0FE00h ; environment address ; ; Configuration area . . . ; db 0,0 ; filler db 'CHKDIR' db Vers/10+'0',Vers mod 10+'0' WBFlg: db 0 ; non-zero=exit with warm boot, 0=return NulFlg: db 0 ; non-zero=include null files, 0=exclude TagChr: db 0 ; tag character for disk labels, 0=none ; ; Start of program . . . ; Start: lhld Z3EAdr ; point to ZCPR3 environment call zsyschk ; is this a Z-system? rnz ; (nope) call z3init ; initialize the ZCPR3 environment mvi c,GetVer ; check for CP/M 2.2 call Bdos mov a,l ; HL = 0022h if CP/M 2.2 cpi 22h+1 ; check for MP/M or CP/M 3.0 jnc CPM3 ; exit if CP/M 3.0, we can't use it ; Get BIOS vectors into local table lhld 1 ; address of BIOS warm start vector lxi d,8*3 ; skip over vectors we don't need mov c,e ; set up counter also mov b,d dad d lxi d,SelDsk ; pointer to local jump table ldir ; ; CODEND has been replaced with a direct load of the first-free-address ; value. CODEND returns the address of the first free PAGE of memory; ; this results in anywhere from 0 to 255 bytes of memory being wasted. ; sspd Stack ; save old stack pointer lxi h,Stack ; set up new stack sphl lhld $memry ; get start of free memory shld DirBuf ; address of directory buffer shld RDest ; destination pointer for directory copy also lxi h,Errors ; initialize required values xra a mvi b,7 ; 7 bytes to initialize FlInit: mov m,a inx h djnz FlInit lda CpmFcb+1 ; check for help request cpi ' ' ; any option means help jnz Help lda CpmFcb ; check for drive ora a jz Help ; (none) ; ; Select drive and load disk parameter block ; lxi d,CpmFcb call z3log ; log into disk mvi c,GetDsk ; find out what the default drive is call Bdos mov c,a ; prepare for disk parameter block call eprint db 'Checking Drive ',0 adi 'A' ; make printable call cout mvi a,':' call cout call SelDsk ; get CP/M Disk Parameter Block data mov e,m ; the BIOS SelDsk returns DPH inx h ; address in HL mov d,m ; extract to translation table sded SecTbl ; save table address lxi d,9 ; offset to DPB in DPH dad d mov a,m ; get DPB address into HL inx h mov h,m mov l,a lxi d,Dpb ; copy to local DPB lxi b,DpbLen ldir lhld DRM ; get maximum directory entries on disk inx h ; convert to 1-relative call RotRHL ; divide by 4 for number of 128-byte call RotRHL ; ..logical sectors in directory shld DirSec ; save number of sectors shld SecCnt ; initialize sector counter for read lhld Bdos+1 ; get top of TPA lda WBFlg ; save CPR? ora a cz gzmtop lded DirBuf ; subtract start of directory buffer ora a dsbc de ; available space (bytes) mov a,l ; mask to multiple of 32 bytes ani not 31 mov c,a mov b,h ; byte count to BC push b ; save dcx b ; count byte we're about to do push d pop h ; address of directory buffer to HL mvi m,0E5h ; put E5 byte in directory inx d ; DE still points to DirBuf, bump it ldir ; fill directory buffer with E5's pop h ; size of buffer back to HL mvi b,5 ; set up counter GetDMx: call RotRHL ; divide by 32 to get number of djnz GetDMx ; ..entries that will fit shld MaxDir ; maximum active entries ; ; RdDir -- Read the directory of the current disk into the directory ; buffer, deleting erased entries as we go. The directory is read ; from the disk one 128-byte sector at a time. After each sector is ; read into buffer at CpmBuf, it is copied to directory buffer at ; DirBuf, skipping over any erased entries, and zero-length entries. ; Thus, the directory is built up incrementally, a sector at a time. ; This allows the program to handle any disk, regardless of the maximum ; number of directory entries the disk may have. The counter of active ; directory entries is incremented as valid entries are copied into ; buffer. If counter exceeds number of entries that fit in memory, an ; error message is printed and CHKDIR aborts. That should be rare ; because even a 48K TPA has room for 1,458 active entries. ; RdDir: call eprint db ' --> Reading .. ',0 RdDir1: lhld SysTrk ; select first directory track shld CurTrk ReadLp: lxi h,CpmBuf shld DmaDDR ; read buffer address push h ; save start of buffer call ReadDD ; read next sector into buffer pop h ; restore source pointer mvi b,4 ; loop counter - 4 entries per sector PackLp: mov a,m ; look at user # of next entry cpi 0E5h ; see if erased jrz SkipHL ; if so, bump HL past this entry ; look for a zero-length file, defined as EX=0, S2=0, and RC=0 push h ; save file pointer lxi d,12 dad d mov d,m ; extent to D inx h inx h mov a,m ; S2 byte to A ani 0Fh inx h mov e,m ; RC byte to E ora d ; OR them all together ora e jrnz NoSkip ; (ok, if not all = 0) call eprint ; tell user we found one db CR,LF,' Zero Length File: ',0 pop h ; restore file pointer push h call PFNam1 lxi h,ZCnt ; count it inr m pop h ; get back file pointer lda NulFlg ; do we include null files? ora a jrz SkipHL ; (no) push h inx h ; point to first character of filename lda TagChr ; get tag character ora a jrz NoSkip cmp m jrnz NoSkip ; (no, include this one too) pop h SkipHL: lxi d,32 ; bump HL over the current entry dad d jr PakEnd ; NoSkip: lhld DirAct ; count this one as active inx h shld DirAct shld ii ; directory count is starting sort count ChkFul: xchg lhld MaxDir ; compare current entry count to max ora a dsbc de pop h jc NoRoom ; error if carry push b ; copy into directory buffer lxi b,32 ; copy 32 bytes lded RDest ; get destination for move into DE ldir sded RDest ; update destination pointer pop b PakEnd: djnz PackLp ; loop for next entry lhld SecCnt dcx h ; decrement sector counter shld SecCnt mov a,h ora l jnz ReadLp ; loop until all sectors done lda ZCnt ; Get count of zero-length files ora a jrz SizDir ; skip if none call eprint db CR,LF,' ',0 call padc call eprint db ' null file(s)',CR,LF,0 ; SizDir: lhld DirAct ; number of active directory entries mov a,h ; do we have any? ora l jz Finish ; (no, exit) ; ; Sort directory ; call Swap ; swap EX & S2 to make it pretty call eprint db 'Sorting .. ',0 ; Shell-Metzner sort lhld ii shld SRecNm lhld DirBuf shld SrtAdr push h ; and save it lxi h,32 shld SRecLn push h ; and save it ; Now divide number of fields by 2 Divide: lhld SRecNm ; get value call RotRHL shld SRecNm ; save result mov a,l ; if SRecNm<>0 ora h ; then jrnz NotDon ; ..not done ; All fields sorted pop b ; clean up stack pop d jmp SwapBk ; swap back & continue ; NotDon: xchg lhld ii ora a dsbc de shld SRecLn lxi h,1 shld SortV1 shld SrtAdr dcr l pop b push b NDone1: dad d dcx b mov a,b ora c jrnz NDone1 shld SortV2 xchg pop b pop h push h push b NDone2: shld SortV4 shld SortV3 xchg dad d xchg Compar: pop b push b push d ; fix from SAP 4.4 push h Compr1: ldax D ani 7Fh push b mov c,a mov a,m ani 7Fh mov b,a mov a,c sub b pop b jrnz NotEqu inx h inx d dcx b mov a,b ora c jrnz Compr1 jr NoSwit ; ; Condition at NotEqu has to be changed for a descending sort -- if ; reverse=0, JNC to NoSwit, else JC to NoSwit ; NotEqu: jrnc NoSwit pop h ; fix from SAP 4.4 pop d pop b push b Switch: push b mov b,m ldax d mov m,a mov a,b stax d inx h inx d pop b dcx b mov a,b ora c jrnz Switch lhld SRecNm mov a,h cma mov d,a mov a,l cma mov e,a lhld SortV1 dad d jrnc NoSw1 ; fix from SAP 4.4 inx h shld SortV1 lhld SortV3 xchg lhld SortV2 mov a,e sub l mov l,a mov a,d sbb h mov h,a shld SortV3 jr Compar ; NoSwit: pop h ; clean up stack -- from SAP 4.4 pop h NoSw1: lhld SrtAdr inx h shld SrtAdr shld SortV1 xchg lhld SRecLn mov a,l sub e mov a,h sbb d jc Divide lhld SortV4 pop d push d dad d lded SortV2 jmp NDone2 ; SwapBk: call Swap ; swap S2 & EX back to correct positions ; ; Check for duplicate directory entries call eprint db 'Checking .. ',0 lxi h,0 ; i = 0 shld ii DupCk1: lhld ii call Index ; HL = DirBuf + 32 * i shld ThisDr lded ThisDr ; check for duplicate entries lxi h,32 ; compare with next entry dad d mvi b,15 ; duplicate if 1st 15 chars the same CmpDir: ldax d sub m ani 7Fh ; in the last 7 bits jrnz DupCk3 inx d inx h djnz CmpDir call eprint db CR,LF,' Duplicate directory entry --> ',0 call PFName mvi a,0FAh ; set error code sta Errors DupCk3: lhld ii ; i = i + 1 inx h shld ii lded DirAct ora a dsbc de jrnz DupCk1 ; loop until i = DirAct ; ; Check directory lxi h,0 ; i = 0 Chek1: shld ii call Index ; HL = DirBuf + 32 * i shld ThisDr mov a,m cpi 32 ; check for bad user # jrnc BadUsr mvi b,11 ; check for bad chars in name inx h ChekLp: mov a,m ani 7Fh ; mask out attribute bit cpi 32 ; bad if less than a space jrc BadNam cpi 060h ; bad if lower case or {}|~ jrnc BadNam cpi '*' jrz BadNam cpi ',' jrz BadNam inx h djnz ChekLp ; Name is OK mov a,m cpi 20h ; bad if extent # > 31 jrnc BadExt inx h ; skip over S1, S2 inx h inx h mov a,m cpi 81h ; bad if record # > 80h jrnc BadRec ChekDN: lhld ii ; i = i + 1 inx h lded DirAct push h ora a dsbc de pop h ; loop until i = DirAct jrnz Chek1 jmp AChek ; BadUsr: call eprint db CR,LF,' User over 31 --> ',0 mvi a,0FBh ; set error code BadChk: sta Errors ; store error code call PFName jr ChekDN ; BadNam: call eprint db CR,LF,' Illegal filename --> ',0 mvi a,0FCh ; set error code jr BadChk ; BadExt: call eprint db CR,LF,' Extent over 31 --> ',0 mvi a,0FDh ; set error code jr BadChk ; BadRec: call eprint db CR,LF,' Record over 128 --> ',0 mvi a,0FEh ; set error code jr BadChk ; ; Allocation checking ; AChek: lxi h,0 mvi b,8 ; compute # entries in each data map lda DSM+1 ; = 8 or 16 ora a jrnz AChek0 ralr b AChek0: sbcd NMap AChek1: shld ii shld jj call Index ; HL = DirBuf + 32 * i shld ThisDr shld ThatDr lxi d,16 lixd ThisDr dadx de ; IX -> first allocation map entry lbcd NMap AChek5: push b lhld ThisDr ; get the next allocation map entry shld ThatDr ; ..from the first file mvi h,0 ldx l,0 lda DSM+1 ; (test for 1 or 2-byte allocation block#) ora a jrz AChek9 ldx h,1 ; allocation block # is now in HL AChek9: mov a,h ; if it is zero, ora l ; ..skip it jrz AChk3a pushix popiy jr AChek6 ; AChek4: mvi d,0 ; get allocation map entry to compare with ldy e,0 lda DSM+1 ora a jrz AChek8 ldy d,1 ; it is now in DE AChek8: mov a,d ; if it is zero, ora e ; ..skip it jrz AChek6 push h dsbc de pop h cz BdAloc AChek6: inxiy lda DSM+1 ; (test for 1 or 2-byte allocation block#) ora a jrz AChek7 inxiy ; increment two bytes if needed AChek7: djnz AChek4 ; ; Now checked against everything in that directory -- get next push h ; (allocation block # being checked) lhld jj inx h shld jj ; HL = # of next directory entry xchg lhld DirAct ora a dsbc de ; if beyond end of directory xchg jrz AChek3 ; jump here call Index ; if ok, get start of next entry shld ThatDr lxi d,16 dad d push h popiy ; point IY to next allocation map lbcd NMap pop h ; restore allocation block # jr AChek4 ; AChek3: pop h AChk3a: pop b inxix ; get next "1st" allocation block # lda DSM+1 ; (test for 1 or 2-byte allocation block#) ora a jrz AChekA inxix ; increment Index twice if needed AChekA: lhld ii ; reset 'that' file pointer shld jj ; ..to the current entry dcr b jrnz AChek5 inx h ; done with this directory - get next lded DirAct ; see if all entries checked push h ora a dsbc de pop h jnz AChek1 ; loop back for next one if not ; ; Exit ; Finish: lda Errors ; see if we have errors ora a jrnz Exit call eprint db 'No Problems.',0 Exit: call puter2 ; write to error flag ora a mov b,a ; put error code in B cnz inverror ; call error handler lda WBFlg ; do we warm boot? ora a jnz WBoot ; (yes) lspd Stack ; restore stack ret ; return to the CPR ; ; Not enough memory to read in all active directory entries NoRoom: call eprint db CR,LF,BEL,'Insufficient memory.',0 mvi a,12 ; set error code jr Finish ; ; CP/M 3.0 not allowed by this program ; CPM3: call eprint db BEL,'Not for Z3Plus.',0 jmp WBoot ; ; Directory read error ; RError: call eprint db CR,LF,BEL,'Read error.',0 mvi a,4 ; set error code sta Errors jmp Finish ; ; Subroutines . . . ; ; PFName -- print filename ; PFName: lhld ThisDr ; HL -> User # PFNam1: push h ; save entry pointer mov a,m call padc ; print user # call eprint db ':',0 ; print colon pop d ; get pointer back inx d ; filename call pfn3 call eprint ; add 4 spaces db ' ',0 ret ; ; BdAloc -- report duplicate allocation blocks ; BdAloc: push h call eprint db CR,LF,' Two files use allocation block ',0 pop h push h lda DSM+1 ora a jrz Prt2 call phl4hc jr ANames Prt2: mov a,l call pa2hc ANames: call PFName lhld ThatDr call PFNam1 mvi a,0FFh ; set error code sta Errors pop h ret ; ; ReadDD - Read selected sector into the current DMA address. On ; entry, the desired sector has been selected by setting the proper ; track and sector values into CurTrk and CurSec. The starting DMA ; address for the read is in DmaDDR. The track, sector, and DMA ; address values are incremented by this routine, so it may be called ; repetitively from within a loop to read multiple sequential sectors ; into a buffer. ; ReadDD: lbcd DmaDDR ; set DMA address call SetDma lbcd CurTrk ; set track call SetTrk lbcd CurSec ; set sector (with translation) call SetSec call Read ; read the sector ora a ; set flag on status return from BIOS jnz RError ; exit if read error lded CurSec ; increment current sector inx d lhld SPT ; check to see if beyond end of track dcx h ; convert to 0-relative ora a dsbc de jrnc NextOk lhld CurTrk ; if so, bump track counter inx h shld CurTrk ; set new current track lxi d,0 ; set sector 0 NextOk: sded CurSec ; set new current sector lhld DmaDDR lxi d,80H ; advance to next DMA address dad d shld DmaDDR ret ; ; Swap -- Developed by Joe Wright, this routine swaps the S2 and extent ; bytes of each directory entry before sorting, and swaps them back after ; sorting. This gets the extents in the proper sequence, because the S2 ; byte, which is really the "high order" byte of the extent counter, ; comes after the extent byte in the FCB. The extent counter is really ; in two parts. The "low order" part is the extent field of the FCB, and ; has values from 0 to 31. The "high order" part is the S2 byte, which ; ranges from 0 to 15. (This is where standard CP/M's limit of 8 Megabytes ; per file comes from: 16 "superextents" x 32 extents x 16K per extent = ; 8192K). Normally the sort routine would sort the extents of a large ; file into the sequence "1, 33, 34, 35, 2, 3, ... 32" (using the S2 byte ; as a counter of groups of 32 extents). Actually, CP/M doesn't really ; care if the extents are in any particular order, but this way is much ; more aesthetically satisfying. If you want a sorted directory, might ; as well have it sorted properly! ; Swap: lhld DirBuf ; beginning of dir buffer lxi d,12 ; offset to EX byte dad d ; point to EX in 1st sort buffer entry lxi d,32 ; bytes per entry lbcd DirAct ; get count of active directory entries Swap1: push b ; remaining count push h ; pointer to EX mov c,m ; get the extent byte into C inx h inx h ; point to S2 mov a,m ; swap them mov m,c pop h mov m,a dad d ; point to next entry pop b ; get count dcx b ; down mov a,b ora c jrnz Swap1 ; again ret ; finished ; ; Divide HL by 2 ; RotRHL: ora a ; clear carry mov a,h rar mov h,a mov a,l rar mov l,a ret ; ; Index ; Index: dad h ; given entry number in HL (0,1,...), index dad h ; sets HL to point to the start of that dad h ; ..entry in the directory buffer dad h ; multiply HL by 32, the size of each entry dad h lded DirBuf ; offset from the start of the buffer dad d ret ; ; Print Help message ; Help: call eprint db 'CHKDIR Version ' db Vers/10+'0','.',Vers mod 10+'0',SubVers,CR,LF db 'Usage:',CR,LF,' ',0 call prtname call eprint db ' dir:',CR,LF db 'Only drive is significant.',CR,LF db 'Reports: zero-length files, duplicate directory entries,',CR,LF db ' user numbers greater than 31, extent numbers greater than',CR,LF db ' 31, record counts greater than 128, filenames with illegal',CR,LF db ' characters, allocation groups assigned to multiple files,',CR,LF db ' and allocation groups assigned twice to the same file.' db 0 xra a jmp Exit ; ; Select the (0-relative) sector number in BC ; SetSec: lded SecTbl ; point to translation table call SecTrn ; let the BIOS translate the sector lda SPT+1 ; if less than 256 sectors per track ora a jrnz GStSec mov h,a ; zero the high-order byte of the sector # GStSec: mov b,h ; put the translated sector # into BC mov c,l jmp BStSec ; execute BIOS call ; ; This jmp prevents linking of PRINT, PSTR, CCOUT, and SATCCC ; print: jmp eprint ; DSEG ; ; Uninitialized data . . . ; ; BIOS jump table is copied into this area SelDsk: ds 3 SetTrk: ds 3 BStSec: ds 3 SetDma: ds 3 Read: ds 3 Write: ds 3 ; not used LstS: ds 3 ; not used SecTrn: ds 3 ; DirBuf: ds 2 ; address of directory buffer MaxDir: ds 2 ; max # of entries readable from disk RDest: ds 2 ; destination pointer for directory copy DmaDDR: ds 2 ; DMA address for read DirSec: ds 2 ; # of sectors in directory SecCnt: ds 2 ; sector counter for directory read CurTrk: ds 2 ; current track for read SecTbl: ds 2 ; address of sector translation table ; Loop counters for the various routines ii: ds 2 ; At entry to sort, = # directory entries; ; general loop counter for other routines jj: ds 2 ; inner loop for AChek ; The following values are initialized as a block to save space, so don't ; move them around without looking at the initialization code at FlInit. Errors: ds 1 ; non-zero if error encountered DirAct: ds 2 ; # of active directory entries CurSec: ds 2 ; current sector for read ZCnt: ds 1 ; count of zero-length files ; Directory entry pointers ThisDr: ds 2 ThatDr: ds 2 NMap: ds 2 ; # entries in allocation map - 8 or 16 ; Sort variables SRecLn: ds 2 SrtAdr: ds 2 SortV1: ds 2 SortV2: ds 2 SortV3: ds 2 SortV4: ds 2 SRecNm: ds 2 ; Disk parameter block is copied here from CP/M Dpb equ $ SPT: ds 2 ; not used BSH: ds 1 ; not used BLM: ds 1 ; not used EXM: ds 1 ; not used DSM: ds 2 ; not used DRM: ds 2 ; not used AL0: ds 1 ; not used AL1: ds 1 ; not used CKS: ds 2 ; not used SysTrk: ds 2 DpbLen equ $-Dpb ; Stack ds 50 Stack: ds 2 ; END