PAGE ,132
TITLE File Chain Program, Version 2.16, 24-Dec-1986

; Bug Fix  Version 2.17, 28 May 1989 by David Gwillim
; Subdirectories were reported as using 0 clusters with 100% waste
; Also corrected typo for pgm lines storing file size to SIZEMSB
; Changed/Added lines marked with a comment starting with ;!!
;
; Written By Steven Georgiades
;
; File Chain Program
;   Will respond with a list of the disk clusters that make up the file chain
;   for the requested file.
;
;       If you are using this program and find it of value, your
;       contribution in any amount ($5.00 suggested) will be greatly
;       appreciated.  Makes checks payable to Steven M. Georgiades.
;               Thank you.
;
;       If you have any questions or comments about this or any other
;       SMG program, call or write:
;
;               Steven M. Georgiades
;               701-H South Hayward Street
;               Anaheim, CA 92804
;               (714) 826-9549
;

CODE      SEGMENT BYTE PUBLIC 'CODE'

          ASSUME  CS:CODE,DS:CODE,ES:CODE,SS:CODE

          ORG     5CH

FCB       LABEL   BYTE

          ORG     80H

PARAM     LABEL   BYTE

          ORG     100H

CHAIN:    JMP     BEGIN

CHAINMSG1 DB      "The Chain for $"
CHAINMSG2 DB      " is:",13,10,10,"$"
CHAINMSG3 DB      13,10,10
CHAINMSG4 DB      "Total Clusters in File =  XXXXX",13,10,10
CHAINMSG5 DB      "Physical File Length = XXXXXXXX",13,10,"$" ;!!
CHAINMSG6 DB      "Logical  File Length = XXXXXXXX ("
PERCENT   DB      " 0.00% Waste)",13,10,"$"
ISDIR     DB      'File is a SUBDIRECTORY',13,10,"$"       ;!!

LOGSZMSG  DW      OFFSET CHAINMSG6

CLSTSTR   DB      "XXXXX$"
PRCNT100  DB      "100.0"

ANDSTR    DB      " , $"
THRUSTR   DB      " to$"

SPECERR   DB      "Invalid File or Path Specification",7,": $"
CRLF      DB      13,10,"$"

SIGNON    DB      "File Chain Program, Version 2.17",13,10                  ;!!
          DB      "SMG Software",13,10
          DB      "(C) Copyright 1986, 1989 Steven M. Georgiades"    ;!!
          DB      13,10,13,10,"$"                                    ;!!

USAGE     DB      13,10,"Usage:",13,10,13,10                              ;!!
          DB      "  CHAIN [d:][path]filename[.ext]",13,10,13,10          ;!!
          DB      "    Will respond with a list of the disk clusters that make up the specified",13,10
          DB      "    file.  Note that wildcards are NOT allowed.",13,10,"$"

CLSTSEC   DW      ?
DIR_LEN   DW      ?
DIRBUF    DW      ?
DIRSEC    DW      ?
DRIVE     DB      ?
EOF       DW      0FF8H
FATSEC    DW      ?
FATSIZE   DB      3
FILENAME  DB      13 DUP(0)
PREV      DW      ?
RANGE     DB      0
SECSIZE   DW      ?
SIZLSB    DW      ?
SIZMSB    DW      ?
STARTSEC  DW      ?

FILESPEC  DB      "$",79 DUP(0)

BEGIN:    MOV     AH,9                          ; Output Sign-On Message
          MOV     DX,OFFSET SIGNON
          INT     21H
          MOV     AH,19H                        ; Get Default Drive
          INT     21H
          MOV     DRIVE,AL                      ;   and Save
          MOV     SI,OFFSET PARAM               ; Set up Pointer to Parameter
          LODSB                                 ; Read Parameter Length
          CBW
          MOV     BX,AX
          MOV     BYTE PTR [SI][BX],0           ; Terminate Parameter String
STRIP:    LODSB                                 ; Strip Off Leading Whitespace
          CMP     AL,' '
          JE      STRIP
          CMP     AL,9
          JE      STRIP
          OR      AL,AL                         ; If End-of-Parameter, Error
          JNZ     NO_ERR1
          MOV     DX,OFFSET USAGE
          JMP     ERROUT
NO_ERR1:  DEC     SI                            ; ReUse Last Character
          MOV     DI,OFFSET FILESPEC            ; Point to FileSpec Buffer
          CMP     BYTE PTR [SI+1],':'           ; If 2nd Character is Colon,
          JNE     GET_PATH
          AND     AL,0DFH
          SUB     AL,'A'                        ;   Get Drive Number
          MOV     DRIVE,AL
          ADD     SI,2                          ;   and Scan Past
GET_PATH: MOV     AL,DRIVE                      ; Get Drive Number
          ADD     AL,'A'                        ; Convert to Drive Number
          STOSB                                 ;   in FileSpec
          MOV     AL,':'                        ; Add Colon to FileSpec
          STOSB
          CMP     BYTE PTR [SI],'\'             ; If Next Character is not '\',
          JE      COPYPATH
          MOV     AL,'\'
          STOSB
          PUSH    SI                            ;   Get Current Path to FileSpec
          MOV     SI,DI
          MOV     AH,47H
          MOV     DL,DRIVE
          INC     DL
          INT     21H
          POP     SI
          MOV     AL,0                          ;   Find End-of-FileSpec
          MOV     CX,64
          REPNE   SCASB
          JE      NO_ERR2
          JMP     ERROR
NO_ERR2:  DEC     DI
          CMP     BYTE PTR [DI-1],'\'           ;   If Root DIR, Skip
          JE      COPYPATH
          MOV     AL,'\'                        ;   End-of-FileSpec = '\'
          STOSB
COPYPATH: LODSB                                 ; Copy Rest of Parameter
          OR      AL,AL                         ;   to FileSpec
          JZ      COPYDONE
          STOSB
          JMP     SHORT COPYPATH
COPYDONE: MOV     AL,'$'                        ; Set End-of-FileSpec
          STOSB
          MOV     DX,OFFSET FILESPEC            ; Convert to Upper Case
          CALL    UPPER
          MOV     AL,DRIVE                      ; Read Boot Record
          MOV     CX,1
          MOV     DX,0
          MOV     BX,OFFSET FATBUF
          INT     25H
          POPF
          MOV     AX,FATBUF[11]                 ; Read Sector Size
          MOV     SECSIZE,AX                    ;   and Save
          MOV     AL,BYTE PTR FATBUF[13]        ; Read Sectors per Cluster
          XOR     AH,AH
          MOV     CLSTSEC,AX                    ;   and Save
          MOV     CX,FATBUF[14]                 ; Read # of Reserved Sectors
          MOV     AL,BYTE PTR FATBUF[16]        ; Read # of FAT's
          XOR     AH,AH                         ; Convert to Word
          MOV     BX,FATBUF[22]                 ; Read Sectors per FAT
          MOV     FATSEC,BX
          MUL     BX                            ; Calculate Total FAT Sectors
          ADD     CX,AX                         ; Add to Reserved Sectors
          MOV     AX,FATBUF[17]                 ; Read Number of DIR Entries
          MOV     DIR_LEN,AX
          PUSH    CX                            ; Calculate DIR Sectors
          MOV     CL,5
          SHL     AX,CL
          POP     CX
          MOV     BX,SECSIZE
          XOR     DX,DX
          DIV     BX
          OR      DX,DX                         ; Adjust for Partial Sector
          JZ      NO_ADD
          INC     AX
NO_ADD:   MOV     DIRSEC,AX                     ; Save DIR Sectors
          ADD     CX,AX                         ; Add DIR Sectors to Reserved
          MOV     STARTSEC,CX                   ; Save in STARTSEC
          MOV     AX,FATSEC                     ; Calculate FAT Buffer Size
          MOV     BX,SECSIZE
          MUL     BX
          MOV     BX,AX
          LEA     AX,FATBUF[BX]                 ; Get DIR Buffer Pointer
          MOV     DIRBUF,AX
          MOV     AX,FATBUF[19]                 ; Read Total Sectors on Media
          SUB     AX,CX                         ; Calculate Total Data Clusters
          MOV     BX,CLSTSEC
          XOR     DX,DX
          DIV     BX
          CMP     AX,4079                       ; If Necessary, Adjust FAT Size
          JLE     FAT_OK
          MOV     FATSIZE,4
          MOV     EOF,0FFF8H
FAT_OK:   MOV     AL,DRIVE                      ; Read FAT
          MOV     CX,FATSEC
          MOV     DX,1
          MOV     BX,OFFSET FATBUF
          INT     25H
          POPF
          MOV     AL,DRIVE                      ; Read Root Directory
          MOV     CX,DIRSEC
          MOV     DX,STARTSEC
          SUB     DX,CX
          MOV     BX,DIRBUF
          INT     25H
          POPF
          MOV     SI,OFFSET FILESPEC[3]         ; Point to First Element of Path
NEXT_FLD: MOV     DI,OFFSET FILENAME            ; Point to FileName Buffer
NEXTCHAR: LODSB                                 ; Copy Path Element to FileName
          CMP     AL,'\'                        ;   until '\' or '$'
          JE      GOT_DIR
          CMP     AL,'$'
          JE      GOT_FNM
          STOSB
          JMP     SHORT NEXTCHAR
GOT_DIR:  MOV     AL,0                          ; If '\', Read Subdirectory
          STOSB
          CALL    READDIR
          JNC     NEXT_FLD                      ;   If No Error, Get Next Dir
          JMP     ERROR                         ;   Else Flag Error
GOT_FNM:  MOV     AL,0                          ; If '$',
          STOSB
          CALL    SRCHFILE                      ;   Read File DIR Info
          JNC     NO_ERR3
          JMP     ERROR                         ; If Error, Say So
NO_ERR3:  MOV     AX,[BX+28]                    ; Read File Length
          MOV     SIZLSB,AX
          MOV     DX,[BX+30]
          MOV     SIZMSB,DX                     ;!! was mov sizmsb,ax
          MOV     DI,OFFSET CHAINMSG6[31]       ; Convert to ASCII
          CALL    DEC8OUT                       ; Strip Off Leading Zeroes
          CALL    STRIP0
          MOV     AH,9                          ; Output FileSpec Message
          MOV     DX,OFFSET CHAINMSG1
          INT     21H
          MOV     AH,9
          MOV     DX,OFFSET FILESPEC
          INT     21H
          MOV     AH,9
          MOV     DX,OFFSET CHAINMSG2
          INT     21H
          MOV     BX,[BX+26]                    ; Read Starting Cluster Number
          XOR     CX,CX                         ; Cluster Count = 0
          OR      BX,BX                         ; If Start Cluster = 0, Done
          JZ      LASTCLST
          MOV     AX,BX                         ; Output First Cluster Number
          CALL    OUT_CLST
          MOV     PREV,BX                       ; Previous Cluster = First
          INC     CX                            ; Cluster Count = 1
          CALL    NEXTCLST                      ; Get Next Cluster Number
          CMP     BX,EOF                        ; If Last, Skip
          JNB     LASTCLST
CLSTLOOP: INC     CX                            ; Increment Cluster Count
          MOV     AX,PREV                       ; If Cluster is Previous + 1,
          INC     AX
          CMP     AX,BX
          JNE     CLSTOUT
          MOV     RANGE,-1                      ;   Set Range Flag
          JMP     SHORT NEXTONE
CLSTOUT:  CMP     RANGE,0                       ; Else
          JE      NORANGE                       ;   If Range Flag is Set,
          MOV     AH,9                          ;     Output " - "
          MOV     DX,OFFSET THRUSTR
          INT     21H
          MOV     AX,PREV                       ;     Output Last Clstr in Range
          CALL    OUT_CLST
          MOV     RANGE,0                       ;     Reset Range Flag
NORANGE:  MOV     AH,9                          ;   Output ",  "
          MOV     DX,OFFSET ANDSTR
          INT     21H
          MOV     AX,BX                         ; Output Cluster Number
          CALL    OUT_CLST
NEXTONE:  MOV     PREV,BX                       ; Previous = Current
          CALL    NEXTCLST                      ; Get Next Cluster Number
          CMP     BX,EOF                        ; If Not EOF, Repeat
          JB      CLSTLOOP
          CMP     RANGE,0                       ; If Range Flag is Set,
          JE      LASTCLST
          MOV     AH,2                          ;   Output " - "
          MOV     DL,'-'
          INT     21H
          MOV     AX,PREV                       ;   Output Last Cluster of Range
          CALL    OUT_CLST
LASTCLST: MOV     AX,SIZMSB                     ; If Size = 0, 100% Waste
          OR      AX,SIZLSB
          JNZ     NOT_ZERO
          JCXZ    LC1                           ;!! allow for <DIR> files
          MOV     LOGSZMSG,OFFSET ISDIR         ;!! say this is a subdirectory
          JMP     SHORT CLSTCNT                 ;!!
LC1:      MOV     SI,OFFSET PRCNT100
          MOV     DI,OFFSET PERCENT
          MOV     CX,5
          REP     MOVSB
          JMP     SHORT CLSTCNT
NOT_ZERO: MOV     AX,SECSIZE                    ; Calculate Percent Waste
          MOV     BX,CLSTSEC
          MUL     BX
          MOV     BX,AX
          MUL     CX
          SUB     AX,SIZLSB
          MOV     DX,100
          MUL     DX
          DIV     BX
          PUSH    DX
          XOR     DX,DX
          DIV     CX
          PUSH    DX
          MOV     DI,OFFSET PERCENT[2]          ; Convert Percent to ASCII
          CALL    DEC2OUT
          CALL    STRIP0
          POP     AX
          MUL     BX
          POP     DI
          ADD     AX,DI
          ADC     DX,0
          DIV     CX
          MOV     DX,100
          MUL     DX
          DIV     BX
          MOV     DI,OFFSET PERCENT[5]
          CALL    DEC2OUT
CLSTCNT:  MOV     AX,CX                         ; Convert Cluster Count to ASCII
          MOV     DI,OFFSET CHAINMSG4[31]
          CALL    DEC5OUT
          CALL    STRIP0                        ; Strip Off Leading Zeroes
          MOV     AX,CLSTSEC                    ; Calculate Physical File Size
          MUL     CX
          MOV     CX,SECSIZE
          MUL     CX
          MOV     DI,OFFSET CHAINMSG5[31]       ; Convert to ASCII
          CALL    DEC8OUT
          CALL    STRIP0                        ; Strip Off Leading Zeroes
          MOV     AH,9                          ; Output Trailer Message
          MOV     DX,OFFSET CHAINMSG3
          INT     21H
          MOV     AH,9                          ;!! Output Logical Size message
          MOV     DX,LOGSZMSG                   ;!!
          INT     21H                           ;!!
          MOV     AX,4C00H                      ; Exit to DOS
          INT     21H
ERROR:    MOV     AH,9                          ; Output Error Message
          MOV     DX,OFFSET SPECERR
          INT     21H
          MOV     AH,9
          MOV     DX,OFFSET FILESPEC            ; Output FileSpec
          INT     21H
          MOV     DX,OFFSET CRLF
ERROUT:   MOV     AH,9
          INT     21H
          MOV     AX,4C01H                      ; Exit to DOS
          INT     21H

SRCHFILE: PUSH    AX                            ; Save Register
          PUSH    CX
          PUSH    DI
          PUSH    SI
          CMP     FILENAME,'.'                  ; If Filename Starts with '.',
          JNE     SRCHFIL2
          MOV     SI,OFFSET FILENAME            ;   Copy Filename to FCB
          MOV     DI,OFFSET FCB[1]
          MOV     CX,11
SRCHFIL1: LODSB
          STOSB
          OR      AL,AL
          LOOPNZ  SRCHFIL1
          INC     CX                            ;   and Pad With Spaces
          DEC     DI
          MOV     AL,' '
          REP     STOSB
          JMP     SHORT SRCHFIL3
SRCHFIL2: MOV     AX,2900H                      ; Else Parse Filename
          MOV     SI,OFFSET FILENAME
          MOV     DI,OFFSET FCB
          INT     21H
SRCHFIL3: MOV     DI,OFFSET FCB[1]              ; Point to Filename in FCB
          MOV     SI,DIRBUF                     ; Point to Start of DIRBUF
          MOV     CX,DIR_LEN                    ; Load DIR Length
          JCXZ    SRCHFIL5                      ; If Zero, Not Found
SRCHFIL4: PUSH    CX                            ; Save Registers
          PUSH    DI
          PUSH    SI
          MOV     CX,11                         ; Compare Filename
          REPE    CMPSB
          POP     SI                            ; Restore Registers
          POP     DI
          POP     CX
          JE      SRCHFIL6                      ; If Match, Found
          ADD     SI,32                         ; Else Next DIR Entry
          LOOP    SRCHFIL4                      ; and Repeat
SRCHFIL5: STC                                   ; Set Error Flag
          JMP     SHORT SRCHFIL7                ; Done
SRCHFIL6: MOV     BX,SI                         ; Set Pointer to DIR Entry
          CLC                                   ; Clear Error Flag
SRCHFIL7: POP     SI                            ; Restore Registers
          POP     DI
          POP     CX
          POP     AX
          RET                                   ; Done

READDIR:  PUSH    CX                            ; Save Registers
          PUSH    DI
          PUSH    SI
          CALL    SRCHFILE                      ; Search for DIR
          JC      READDIR2                      ; If Not Found, Error
          MOV     BX,[BX+26]                    ; Get Starting Cluster Number
          MOV     DIR_LEN,0                     ; DIR Length = 0
          OR      BX,BX                         ; If Start Cluster = 0, Error
          JZ      READDIR2
          MOV     AX,SECSIZE                    ; Calculate DIR Entries/Cluster
          MOV     CX,CLSTSEC
          MUL     CX
          MOV     CL,5
          SHR     AX,CL
          MOV     CX,AX
          MOV     DI,DIRBUF                     ; Point to DIR Buffer
READDIR1: CALL    READCLST                      ; Read Cluster
          ADD     DIR_LEN,CX                    ; Increment DIR Length
          CMP     BX,EOF                        ; If Not Last Cluster,
          JB      READDIR1                      ;   Get Next Cluster
          JMP     SHORT READDIR3                ; Done Reading DIR
READDIR2: STC                                   ; Set Error Flag
READDIR3: POP     SI                            ; Restore Registers
          POP     DI
          POP     CX
          RET                                   ; Done

READCLST: PUSH    AX                            ; Save Registers
          PUSH    CX
          PUSH    DX
          MOV     AX,BX                         ; Calculate Absolute Sector #
          SUB     AX,2
          MOV     CX,CLSTSEC
          MUL     CX
          ADD     AX,STARTSEC
          MOV     DX,AX
          MOV     AL,DRIVE
          PUSH    BX                            ; Save Registers
          PUSH    DI
          MOV     BX,DI                         ; Read Cluster
          INT     25H
          POPF
          POP     DI                            ; Restore Registers
          POP     BX
          CALL    NEXTCLST                      ; Get Next Cluster Number
          MOV     AX,CLSTSEC                    ; Increment Buffer Pointer
          MOV     CX,SECSIZE
          MUL     CX
          ADD     DI,AX
          POP     DX                            ; Restore Registers
          POP     CX
          POP     AX
          RET                                   ; Done

NEXTCLST: CMP     FATSIZE,3                     ; If FAT Size = 16 Bits,
          JE      NEXTCLS1
          SHL     BX,1                          ;   Simply Read Next Cluster #
          MOV     BX,FATBUF[BX]
          RET                                   ;   Done
NEXTCLS1: PUSH    AX                            ; Save Registers
          PUSH    CX
          MOV     AX,BX                         ; Word # = Cluster # * 1.5
          SHL     AX,1
          ADD     BX,AX
          SHR     BX,1
          MOV     BX,FATBUF[BX]
          JNC     NEXTCLS2                      ; If Odd, Use 12 MSB's
          MOV     CL,4
          SHR     BX,CL
NEXTCLS2: AND     BX,0FFFH                      ; Else Use 12 LSB's
          POP     CX                            ; Restore Registers
          POP     AX
          RET                                   ; Done

UPPER:    PUSH    AX                            ; Save Registers
          PUSH    DX
          PUSH    DI
          PUSH    SI
          MOV     SI,DX                         ; Set Up Source Index
          MOV     DI,DX                         ; Set Up Destination Index
UPPER1:   LODSB                                 ; Read From Source
          OR      AL,AL                         ; If EOS, Done
          JZ      UPPER3
          CMP     AL,'a'                        ; If Lower Case,
          JL      UPPER2
          CMP     AL,'z'
          JG      UPPER2
          SUB     AL,'a'-'A'                    ;   Convert to Upper Case
UPPER2:   STOSB                                 ; Save in Destination
          JMP     SHORT UPPER1                  ; Repeat
UPPER3:   POP     SI                            ; Restore Registers
          POP     DI
          POP     DX
          POP     AX
          RET                                   ; Done

OUT_CLST: PUSH    AX                            ; Save Registers
          PUSH    DX
          PUSH    DI
          MOV     DI,OFFSET CLSTSTR[5]          ; Convert Cluster # to Decimal
          CALL    DEC5OUT
          CALL    STRIP0                        ; Strip Off Leading Zeroes
          MOV     AH,9                          ; Output Cluster Number
          MOV     DX,OFFSET CLSTSTR
          INT     21H
          POP     DI                            ; Restore Registers
          POP     DX
          POP     AX
          RET                                   ; Done

STRIP0:   CMP     BYTE PTR [DI],'0'             ; If Character != '0', Done
          JNE     STRIP1
          CMP     BYTE PTR [DI+1],'0'           ; If Next Character != Digit,
          JL      STRIP1                        ;   Done
          CMP     BYTE PTR [DI+1],'9'
          JG      STRIP1
          MOV     BYTE PTR [DI],' '             ; Change '0' to ' '
          INC     DI                            ; Point to Next Character
          JMP     SHORT STRIP0                  ; Repeat
STRIP1:   RET                                   ; Done

DEC2OUT:  PUSH    AX                            ; Save Registers
          PUSH    BX
          XOR     AH,AH                         ; Clear AH
          MOV     BL,10                         ; AH=AX%10,AL=AX/10
          DIV     BL
          ADD     AX,'00'                       ; Convert to ASCII
          SUB     DI,2
          MOV     [DI],AX                       ; Store in String
          POP     BX                            ; Restore Registers
          POP     AX
          RET                                   ; Done

DEC4OUT:  PUSH    AX                            ; Save Registers
          PUSH    BX
          MOV     BL,100                        ; AH=AX%100,AL=AX/100
          DIV     BL
          XCHG    AH,AL                         ; Convert 2 LSD's
          CALL    DEC2OUT
          XCHG    AH,AL                         ; Convert 2 MSD's
          CALL    DEC2OUT
          POP     BX                            ; Restore Registers
          POP     AX
          RET                                   ; Done

DEC5OUT:  PUSH    AX                            ; Save Registers
          PUSH    BX
          PUSH    DX                            ; DX=AX%10000,AX=AX/10000
          MOV     BX,10000
          XOR     DX,DX
          DIV     BX
          XCHG    DX,AX                         ; Convert 4 LSD's
          CALL    DEC4OUT
          XCHG    DX,AX                         ; Convert MSD
          ADD     AL,'0'
          SUB     DI,1
          MOV     [DI],AL
          POP     DX                            ; Restore Registers
          POP     BX
          POP     AX
          RET                                   ; Done

DEC8OUT:  PUSH    AX                            ; Save Registers
          PUSH    BX
          PUSH    DX
          MOV     BX,10000                      ; DX=DX:AX%10000,AX=DX:AX/10000
          DIV     BX
          XCHG    DX,AX                         ; Convert 4 LSD's
          CALL    DEC4OUT
          XCHG    DX,AX                         ; Convert 4 MSD's
          CALL    DEC4OUT
          POP     DX                            ; Restore Registers
          POP     BX
          POP     AX
          RET                                   ; Done

FATBUF    LABEL   WORD

CODE      ENDS

          END     CHAIN
