| Index | Manual | Download | Source | Macros | Embedded word Case ins.
Sitemap | Links | Forum | Tests | Projects

# dirslinc.htm
Data
Data
Procedures
ByDate
ByName
ByVolume
DirVolume
EraseOlderFile
Error
ExcludeDir
LoadAndCompareScans
LoadNewer
LoadOlder
MainCon
ParseToBuffer
ReadFile_ini
Scan
SetMaxDaysAgo
SetToday
StoreFileName
StoreIndex

This is a CON module of EuroTool program EuroDirs for Linux.

             EUROASM CPU=X64, AutoAlign=yes, Unicode=no
    dirslinc PROGRAM Format=COFF,Width=64
              %DROPMACRO *                 ; Forget macros from previous assembly, if any.
              INCLUDEHEAD1 argument.htm    ; Include header of this file.
              INCLUDE1 linabi.htm, cpuext64.htm, linsfile.htm, linf64.htm,
              INCLUDE1 memory64.htm, string64.htm, status32.htm, sort64.htm, time.htm
    HEAD
    SCAN_INDEX  STRUC     ; A structure which describes one scaned directory.
    .Ptr         D DWORD  ; Relative pointer to the directory name in UTF-8. Related to [NamesInMemory].
    .Size        D DWORD  ; Size of the directory name in bytes.
    .Volume      D QWORD  ; Total amount of bytes in all files in this directory.
                ENDSTRUC SCAN_INDEX
    ENDHEAD

    [.rodata]   ; Text constants.
    Ext_names   DB ".names",0              ; The extension of temporary names file.
    Ext_index   DB ".index",0              ; The extension of temporary index file.
    Ext_scan    DB ".scan",0               ; The extension of scan file.
    DateMask    DB "????.??.??_??.??",0    ; File date mask.
    EuroDirs::  DB "EuroDirs",0
    Version::   DB " version %^Date",0
    Help::      DB 0xEF,0xBB,0xBF       ; BOM UTF-8.
                DB "; EuroDirs default configuration:",10
                DB "ScanNow=no        ; Perform the scan when /SN=yes. Otherwise compare scans files.",10
                DB "ScanDir="".""       ; Directory for scan files.",10
                DB "TimeZone=0        ; TZ of scan files in hours. /TZ=0 for UTC, /TZ=1 for Prague etc.",10
                DB "LeaveTemporary=no ; Do not erase *.name and *.index files (for debugging).",10
                DB "MaxDays=32        ; Older scan files will be deleted before creating a new scan.",10
                DB "InputFile=        ; Old scan file to compare. Default is the oldest in ScanDir.",10
                DB "OutputFile=       ; New scan file to compare. Default is the newest in ScanDir.",10
                DB "ExcludeDirs=      ; Do not scan dirs beginning with it. Default is empty.",10
                DB "IncludeDirs=      ; Scan those dirs only. Default is scan all.",10
                DB "ViewDirs=Inc      ; Ins|Inc|Unch|Dec|Del - select one of 5 views.",10
                DB 0
    Comparing   DB 10,"Comparing directories from two scan files:",0
    Report1::   DB " new created directories,",0
    Report2::   DB " increased directories,",0
    Report3::   DB " directories which did not change,",0
    Report4::   DB " decreased directories,",0
    Report5::   DB " deleted directories.",0
    [.data]     ; Text variables.
    Today       DB "yyyy.mm.dd_hh.mm",0 ; Today's date during the scan.
    MaxDaysAgo  DB "yyyy.mm.dd_hh.mm",0 ; Today minus ArgMaxDays.
    [.bss]      ; Working memory variables.
    SystemTime          DS SYSTEMTIME  ; A time structure.
    ScanIndex           DS SCAN_INDEX  ; A directory index.
    File_ini            DS FILE64 ; Configuration FILE64.
    File_names          DS FILE64 ; Temporary FILE64 for directory names (UTF-8).
    File_index          DS FILE64 ; Temporary FILE64 for directory indexes.
    File_scan           DS FILE64 ; Scan FILE64 (UTF-8), alphabetically sorted.
    File_scanOlder::    DS FILE64 ; Older scan FILE64 to compare.
    File_scanNewer::    DS FILE64 ; Newer scan FILE64 to compare.
    Dir::               D 1K*BYTE ; The working directory which is scanned.
    Total               D QWORD   ; Total size of all files and subdirectories.
    NamesInMemory       D QWORD   ; Pointer to the contents of File_names mapped in memory.
    NamesSize           D QWORD   ; Size of the contents of File_names mapped in memory.
    IndexInMemory::     D QWORD   ; Pointer to the contents of File_index mapped in memory.
    IndexEndInMemory::  D QWORD   ; Size of the contents of File_index mapped in memory.
    IndexNumber::       D QWORD   ; Number of scanned subdirectories.
    IncludedDirsBuffer  D QWORD   ; BUFFER for the parsed directories of ArgIncludedDirs.
    IncludedDirsPtr     D QWORD   ; Pointer to IncludedDirsBuffer contents.
    IncludedDirsEnd     D QWORD   ; Pointer to the end of IncludedDirsBuffer contents.
    ExcludedDirsBuffer  D QWORD   ; BUFFER for the parsed directories of ArgExcludedDirs.
    ExcludedDirsPtr     D QWORD   ; Pointer to ExcludedDirsBuffer contents.
    ExcludedDirsEnd     D QWORD   ; Pointer to the end of ExcludedDirsBuffer contents.
    ScanDirBuffer       D QWORD   ; BUFFER for the dates of old scans.
    ScanDirPtr          D QWORD   ; Pointer to ScanDirBuffer contents.
    ScanDirEnd          D QWORD   ; Pointer to the end of ScanDirBuffer contents.
    ScanDirNr           D QWORD   ; Number of scans in ScanDir.
    OlderPtr            D QWORD   ; Pointer to the old scan mapped in memory, variable.
    OlderInMemory::     D QWORD   ; Pointer to the beginning of old scan mapped in memory.
    OlderEndInMemory    D QWORD   ; Pointer to the end of old scan mapped in memory.
    NewerPtr            D QWORD   ; Pointer to the new scan mapped in memory, variable.
    NewerInMemory::     D QWORD   ; Pointer to the beginning of new scan mapped in memory.
    NewerEndInMemory    D QWORD   ; Pointer to the end of new scan mapped in memory.
    Buffers::           ; Array of buffers.
    Buffer1::           D QWORD   ; Pointer to BUFFER with INDEXes of inserted dirs.
    Buffer2::           D QWORD   ; Pointer to BUFFER with INDEXes of increased dirs.
    Buffer3::           D QWORD   ; Pointer to BUFFER with INDEXes of unchanged dirs.
    Buffer4::           D QWORD   ; Pointer to BUFFER with INDEXes of decreased dirs.
    Buffer5::           D QWORD   ; Pointer to BUFFER with INDEXes of deleted dirs.
    BufferSelected::    D QWORD   ; One of Buffer1..Buffer5 selected by a button or /ViewDirs.
    MaxLineNr::         D DWORD   ; Ordinal number of the last line in the selected buffer.
    ArgNr               D DWORD   ; Binary number of the current cmd-line argument.
    Numbers::           ; Array of numbers (ANSI).
    Number1:            D 20*BYTE ; Decimal number (ANSI) of inserted dirs.
    Number2:            D 20*BYTE ; Decimal number (ANSI) of increased dirs.
    Number3:            D 20*BYTE ; Decimal number (ANSI) of unchanged dirs.
    Number4:            D 20*BYTE ; Decimal number (ANSI) of decreased dirs.
    Number5:            D 20*BYTE ; Decimal number (ANSI) of deleted dirs.
    NumberA::           D 20*BYTE ; A working space for numbers. Zero terminated, e. g. "1234567",0.
    Number_A::          D 26*BYTE ; Number but with separators _, e. g. "1_234_567".
    [.text]

MainCon
  The main procedure executed both in console and in graphic mode.
  It reads arguments and then it decides by /ScanNow=. If /SN=true, it scans all directories, writes a scan file to /ScanDir and terminates.
  When /ScanNow=false (default), EuroDirs opens a graphic window and the user can browse directories, change the /ViewDirs mode or change older and newer scan files.

    MainCon:: PROC
        StdOutput EuroDirs,Version,Eol=yes
        ; Try to load arguments from the configuration file "/etc/eurodirs.ini".
        FileAssign File_ini,=B'/etc/eurotool/eurodirs.ini'
        FileExists? File_ini
        JNC .15:
        FileMkDir File_ini     ; Create /etc/eurotool/ if it didn't exist.
        JC .10:
        LEA RDI,[Help]
        GetLength$ RDI, Unicode=no
        FileStore File_ini, RDI, RCX
        JNC .15:
    .10:StdOutput =B'Configuration "',File_ini.Name, =B'" could not be saved.',Eol=yes, Unicode=no
        JMP .20:
    .15:CALL ReadFile_ini
    .20:; And finally read arguments from the command-line.
        MOV EAX,[ArgNr]
        INC EAX                ; The next argument.
        MOV [ArgNr],EAX
        GetArg RAX, Frame=RSP  ; Returns in RSI,RCX the line with one argument, e. g. /ScanDir="/var/log/eurodirs".
        JC .30:                            ; When they are no more arguments.
        CALL ArgParse::        ; Use ArgParse to translate it to a public symbol Arg***.
        JNC .20:
        StdOutput ErrorMessage::,EscDefColor::, Eol=yes
        JNSt [Status::],ArgHelpRequired,.25:
        LEA RDI,[Help+3]
        StdOutput RDI, Eol=yes
    .25:TerminateProgram 8
    .30:; Arguments are retrieved. Check if ArgScanDir ends with slash.
         LEA RDI,[ArgScanDir::]
         GetLengthUTF8 RDI       ; Returns RCX=size in bytes,
         MOV AX,'/'
         CMP [RDI+RCX-1],AL
         JE .40
         MOV [RDI+RCX],AX
    .40:; Create buffers for SCAN_INDEX objects. They will be used when /SN=no.
        BufferCreate Size=16K
        JC ErrorBuffer
        MOV [ScanDirBuffer],RAX
        BufferCreate Size=4M
        JC ErrorBuffer
        MOV [Buffer1],RAX
        BufferCreate Size=4M
        JC ErrorBuffer
        MOV [Buffer2],RAX
        BufferCreate Size=8M
        JC ErrorBuffer
        MOV [Buffer3],RAX
        BufferCreate Size=4M
        JC ErrorBuffer
        MOV [Buffer4],RAX
        BufferCreate Size=4M
        JC ErrorBuffer
        MOV [Buffer5],RAX
        JSt [Status::],ArgScanNow,.80:     ; Should the computer be scanned now?
        CALL LoadAndCompareScans           ; If not, compare two scans to Buffer1..5.
        JC .Terminate:                     ; On error terminate.
        JMP MainGui::                      ; Otherwise show the graphic version.
    .80:CALL Scan:                         ; Compute and save the Scan and then terminate.
    .Terminate::
        FileClose File_scanOlder, File_scanNewer
        BufferDestroy [ScanDirBuffer],[Buffer1],[Buffer2],[Buffer3],[Buffer4],[Buffer5]
        TerminateProgram 0
      ENDP MainCon::

Scan

  Procedure Scan is called when an argument /ScanNow=yes was detected. EuroDirs is usually called with this argument from TaskScheduler daily or weekly and it creates a scan file "YYYY.MM.DD_HH.MM.scan" in the directory specified by `/ScanDir=`.

Called by
  MainCon.

    Scan: PROC ; Scan now.
        CALL SetToday           ; Specify the nominal date_time for the scan.
        CALL SetMaxDaysAgo
        LEA RDI,[ArgScanDir::]
        GetLengthUTF8 RDI
        MOV AL,'/'
        CMP [RDI+RCX-1],AL
        JE .10:
        MOV [RDI+RCX],AL        ; Make sure that ScanDir ends with /.
    .10:FileAssign File_scan,RDI,DateMask,Ext_scan ; Find files "ScanDir/????.??.??_??.??.scan"
        FileEach File_scan,EraseOlderFile          ; Erase files older than ArgMaxDays.
        ; Create files with extensions .scan, .names, .index
        LEA RBX,[File_scan]
        FileAssign RBX,RDI,Today,Ext_scan
        FileCreate RBX
        JNC .20:
        CMP AX,-EACCES
        JNE ErrorFile:
        StdOutput =B"Permission denied. Use sudo.",Eol=yes
        JMP ErrorWrite
    .20:LEA RBX,[File_names]
        FileAssign RBX,RDI,Today,Ext_names
        FileCreate RBX
        JC ErrorWrite
        LEA RBX,[File_index]
        FileAssign RBX,RDI,Today,Ext_index
        FileCreate RBX
        JC ErrorWrite
        ; Prepare excluded directories.
        BufferCreate Size=4K
        MOV [ExcludedDirsBuffer],RAX
        LEA RSI,[ArgExcludeDirs::]
        CALL ParseToBuffer
        BufferRetrieve [ExcludedDirsBuffer]
        LEA RDX,[RSI+RCX]
        MOV [ExcludedDirsPtr],RSI
        MOV [ExcludedDirsEnd],RDX
        ; Scan included dirs (if not empty) or all directories otherwise. Omit excluded dirs.
        BufferCreate Size=4K
        MOV [IncludedDirsBuffer],RAX
        LEA RSI,[ArgIncludeDirs::]
        CMPB [RSI],0
        JNZ .30:
        MOVW [RSI],'/'          ; When /IncludeDirs was empty, use / instead.
    .30:CALL ParseToBuffer      ; RSI=^ArgIndludeDirs, RAX=IncludeDirsBuffer.
        BufferRetrieve [IncludedDirsBuffer]
        LEA RDX,[RSI+RCX]
        MOV [IncludedDirsPtr],RSI
        MOV [IncludedDirsEnd],RDX
    .40:MOV RSI,[IncludedDirsPtr] ; Scan the next directory from IncludedDirs.
        CMP RSI,[IncludedDirsEnd]
        JNB .50:
        LODSQ           ; 1st QWORD is a size, 2nd QWORD is a pointer to the directory name.
        MOV ECX,EAX
        LODSQ
        MOV [IncludedDirsPtr],RSI
        MOV RSI,RAX
        LEA RDI,[Dir]
        JRCXZ .40:
        REP MOVSB       ; Included directory will be put to Dir.
        XOR EAX,EAX
        STOSB
        CALL ExcludeDir ; Skip if the folder in Dir is one of the excluded directories.
        JC .40:         ; Dir is excluded, ignore it.
        LEA RDI,[Dir]
        StdOutput =B"Scanning directory """,RDI,=B"""",Eol=yes
        CALL DirVolume  ; The main recursive procedure will scan Dir and all its subdirectories.
        ADD [Total],RAX
        CALL StoreIndex
        JMP .40:
    .50:; DirVolume is now scanned and written to File_names and File_index.
        MOV RBX,[IncludedDirsBuffer]
        BufferDestroy RBX
        MOV RBX,[ExcludedDirsBuffer]
        BufferDestroy RBX
        FileClose File_names,File_index ; Temporarily close files.
        LEA RBX,[File_names]
        FileMapOpen RBX                 ; Directory names will be sorted by name.
        JC ErrorMap
        MOV [NamesInMemory],RSI
        MOV [NamesSize],RAX
        LEA RBX,[File_index]
        FileMapCreate RBX
        JC ErrorMap
        MOV [IndexInMemory],RDI
        ADD RDI,RAX
        MOV [IndexEndInMemory],RDI
        SHR EAX,4               ; Size of SCAN_INDEX is 16.
        MOV [IndexNumber],RAX
        LEA RDI,[NumberA]
        MOV RBX,RDI
        StoD RDI
        MOVB [RDI],0
        StdOutput =B'Sorting ',RBX,=B' directories...',Eol=yes
        LEA RDX,[ByName]
        MOV RSI,[IndexInMemory]
        MOV RAX,[IndexNumber]
        ShellSort RSI,RAX,16,RDX        ; Sort the indexes by name.
        LEA RDI,[File_scan.Name]
        StdOutput =B'Writing scan "',RDI,=B'"',Eol=yes
    .60:CMP RSI,[IndexEndInMemory]      ; Write the sorted indexes to a scan file.
        JAE .80:
        MOV RAX,[RSI+SCAN_INDEX.Volume]
        LEA RDI,[NumberA]
        MOV RBX,RDI
        StoD RDI
        SUB RDI,RBX
        MOV EDX,[RSI+SCAN_INDEX.Ptr]
        MOV ECX,[RSI+SCAN_INDEX.Size]
        ADD RDX,[NamesInMemory]
        FileWrite File_scan,RDX,RCX,=B 0x09,1,RBX,RDI,=B 0x0A,1 ; DirNameInUTF8, HT, DirVolume, LF.
        ADD RSI,SIZE# SCAN_INDEX
        JMP .60:
    .80:FileClose File_names,File_index,File_scan
        JSt [Status::],ArgLeaveTemporary,.90:
        FileDelete File_names,File_index
    .90:StdOutput =B"Scan terminated.",Eol=yes
        RET
       ENDP Scan

DirVolume
  This is a recursive procedure which will scan a single directory.
  The directory name in Dir may or may not be terminated with a backslash. It is always zero terminated.
Input
  Dir contains name of the examined folder, encoded in UTF-8.
Output
  ZF=0, RAX=total data volume of Dir's files and subdirectories.
Error
  ZF=1, RAX=0 if the directory does not exist or if it is excluded.
Called by
  Scan. It also calls itself.
Clobbers
  RCX,RDX,RSI,RDI,R8,R11

    DirVolume:  PROC
       PUSH RCX,RSI,RDI,R12
        XOR EAX,EAX
        PUSH RAX                 ; Accumulator for the total volume of the directory in Dir.
         CALL ExcludeDir
         JC .90:                 ; if the directry should be excluded.
         LEA RDI,[Dir]           ; RDI is a directory, always slash-terminated.
         MOV ECX,SIZE# Dir
         XOR EAX,EAX
         REPNE SCASB
         LEA R12,[RDI-1]         ; End of directory name (R12 points at the terminating NUL).
         LEA RDI,[Dir]
         LinABI open,RDI,O_RDONLY+O_DIRECTORY,0,Fastmode=yes
         MOV RDI,RAX             ; The returned directory handle.
    %TempBufSize  %SETA (SIZE# FILE64.Name+7)&~7 ; Buffer of NAME_MAX rounded up to QWORD (264).
    %StatSize     %SETA (SIZE# STAT+7)&~7 ; Buffer for STAT structure (144), reused for SCAN_INDEX structure (16).
         SUB RSP,%TempBufSize
         SUB RSP,%StatSize       ; RSP=^STAT.
    .10: LEA RSI,[RSP+%StatSize] ; RSI=^Temporary buffer for several DIRENT64 entries.
         LinABI getdents64,RDI,RSI,%TempBufSize
         TEST RAX
         JZ .80:                 ; No more files/directories.
         CMP RAX,-ERR_MAX
         JNC .80:                ; Jump on error (EBADF).
         LEA RCX,[RSI+RAX]       ; End of this buffer.
    .20: CMP RSI,RCX             ; At the end of buffer?
         JNB .10:                ; If yes, load the next portion.
         LEA RDX,[RSI+DIRENT64.d_name]
         MOV R10,RSI
         MOV R11,RDI             ; Temporary save RSI,RDI.
          MOV RSI,RDX
          MOV RDI,R12
          MOV AL,'/'
          CMP [RDI-1],AL
          JE .30:
          STOSB
    .30:  LODSB
          STOSB                  ; Append the file/subdirectory name to the path in Dir.
          CMP AL,0
          JNE .30:
         MOV RDI,R11             ; Restore RDI,RSI.
         MOV RSI,R10
         CMPB [RSI+DIRENT64.d_type],DT_DIR ; Is it a directory?
         JE .40:
         ; Treat other file types as an ordinary file.
         MOV R11,RDI
          LEA RDI,[Dir]          ; A regular file.
          MOV RDX,RSP            ; RDX=^Buffer for STAT structure.
          LinABI lstat,RDI,RDX,Fastmode=no
          MOV RAX,[RDX+STAT.st_size]
          LEA RDX,[RSP+%TempBufSize+%StatSize] ; RDX=^QWORD accumulator for total volume.
          ADD [RDX],RAX          ; Accumulate the size of files in subdirectory Dir.
          MOVB [R12],0           ; Remove the file/subdirectory name from Dir.
         MOV RDI,R11
         JMP .60:
    .40: ; RDX is a subdirectory name without path. Dir is the same name with path.
         CMPW [RDX],'.'
         JE .60:
         CMPW [RDX],'..'
         JNE .50:
         CMPB [RDX+2],0
         JE .60:                 ; Ignore "." and ".." directories.
    .50: ; Dir is a subdirectory name, other than "." and "..".
         CALL ExcludeDir
         JC .55:
         CALL DirVolume          ; Recursive call itself. Does not change RCX,RSI,RDI.
         LEA RDX,[RSP+%TempBufSize+%StatSize] ; RDX=^accumulator for total volume.
         ADD [RDX],RAX           ; Add the subdirectory size.
         CALL StoreIndex
    .55: MOVB [R12],0
    .60: MOVZXW EAX,[RSI+DIRENT64.d_reclen]
         ADD RSI,RAX
         JMP .20:
    .80:ADD RSP,%TempBufSize+%StatSize
    .90:POP RAX                  ; Return the total volume of the directory Dir in RAX.
        POP R12,RDI,RSI,RCX
        RET
       ENDP DirVolume

StoreIndex
  Procedure StoreIndex takes the directory name from Dir and stores the name and index to File_names and File_index.
Input
  Dir
Output
  Files are updated.
Called by
  Scan and DirVolume.
Clobbers
  RAX,RBX,RDX

    StoreIndex PROC
     PUSH RCX,RDI
      LEA RDX,[ScanIndex]      ; Store the directory name+volume to temporary File_names and File_index.
      MOV [RDX+SCAN_INDEX.Volume],RAX
      LEA RDI,[Dir]
      MOV RBX,RDI
      XOR EAX,EAX
      MOV ECX,SIZE# Dir
      REPNE SCASB
      DEC RDI
      SUB RDI,RBX
      MOV RAX,[File_names.Pos]
      MOV [RDX+SCAN_INDEX.Ptr],EAX
      MOV [RDX+SCAN_INDEX.Size],EDI
      FileWrite File_names,RBX,RDI
      LEA RBX,[File_names]
      JC ErrorWrite:
      FileWrite File_index,RDX,SIZE# SCAN_INDEX
      LEA RBX,[File_index]
      JC ErrorWrite:
     POP RDI,RCX
     RET
    ENDP StoreIndex

LoadAndCompareScans

  Global procedure LoadAndCompareScans checks arguments.
  If /ScanDir is empty it fills it with ..
  If /InputFile is empty (default), it assigns it with the oldest scan from /ScanDir.
  If /OutputFile is empty (default), it assigns it with the newest scan from /ScanDir.
  If there are less than two scans in /ScanDir, LoadAndCompareScans ends with CF=1.

  By default it selects the oldest and the newest scan in ScanDir.

  The procedure fills the five buffers with ScanFiles records (indexes) with differences between those two scans.

Input
  -
Output
  CF=0, Buffer1,Buffer2,Buffer3,Buffer4,Buffer5 are loaded with the differences and sorted.
Error
  CF=1 less than two scans were found.
Called by
  DirVolume.

    LoadAndCompareScans:: PROC
        ; Clear buffers from previous run of LoadAndCompareScans procedure.
        BufferClear [ScanDirBuffer],[Buffer1],[Buffer2],[Buffer3],[Buffer4],[Buffer5]
        ; Load dates of all scans into a ScanDirBuffer.
        LEA RDI,[ArgScanDir::]
        FileAssign File_scan, RDI, DateMask, Ext_scan, Unicode=no
        FileEach File_scan,StoreFileName ; Store the date from file names to ScanDirBuffer.
        FileClose File_scan
        BufferRetrieve [ScanDirBuffer]
        CMP ECX,2*16                     ; At least two scans are required.
        JAE .20:
        StdOutput =B"At least two scan files are required in /ScanDir=""",RDI,=B""".",Eol=yes
        StdOutput =B"Please use 'sudo ./eurodirs.x -ScanNow=yes'",Eol=yes
        STC
        JMP .90:
    .20:MOV RAX,RCX
        ADD RCX,RSI
        SHR RAX,4                        ; Each item in ScanDirBuffer has 16 bytes.
        MOV [ScanDirPtr],RSI
        MOV [ScanDirEnd],RCX
        MOV [ScanDirNr],RAX
        ShellSort RSI, RAX, 16, ByDate
        FileClose File_scanOlder,File_scanNewer ; Close files, if opened by previous comparison.
        LEA RDI,[ArgInputFile::]         ; If ArgInputFile is specified, it will be used as File_scanOlder.
        LEA RBX,[File_scanOlder]
        MOV RDX,RDI
        CMPB [RDI],0
        JNE .25:
        LEA RSI,[ArgScanDir::]
        GetLength$ RSI
        REP MOVSB
        MOV RSI,[ScanDirPtr]            ; ArgInputFile is empty (default), use the first date of the sorted ScanDirPtr.
        MOV ECX,16
        REP MOVSB
        LEA RSI,[Ext_scan]
        MOV CL,6
        REP MOVSB
    .25:FileAssign RBX,RDX
    .30:FileMapOpen RBX
        JC ErrorMap:
        TEST RAX
        JZ ErrorZeroSize
        MOV [OlderPtr],RSI
        MOV [OlderInMemory],RSI
        ADD RAX,RSI
        MOV [OlderEndInMemory],RAX
        LEA RDI,[ArgOutputFile::]      ; If ArgOutputFile is specified, it will be used as File_scanNewer.
        LEA RBX,[File_scanNewer]
        MOV RDX,RDI
        CMPB [RDI],0
        JNE .35:
        LEA RSI,[ArgScanDir::]
        GetLength$ RSI
        REP MOVSB
        MOV RSI,[ScanDirEnd]
        MOV ECX,16
        SUB RSI,RCX
        REP MOVSB
        LEA RSI,[Ext_scan]
        MOV CL,6
        REP MOVSB
    .35:FileAssign RBX,RDX
    .40:FileMapOpen RBX
        JC ErrorMap:
        TEST RAX
        JZ ErrorZeroSize
        MOV [NewerPtr],RSI
        MOV [NewerInMemory],RSI
        ADD RAX,RSI
        MOV [NewerEndInMemory],RAX
        ; Check if newer > older.
        RstSt [Status::],ArgSwappedDates
        LEA RSI,[File_scanNewer.Name]
        LEA RDI,[File_scanOlder.Name]
        GetLength$ RDI
        MOV EDX,ECX
        GetLength$ RSI
        CMP ECX,EDX
        JNE .45:
        REPE CMPSB
        JGE .45:
        SetSt [Status::],ArgSwappedDates
    .45:; Comparing two scan files starts.
        StdOutput Comparing,Eol=yes
        StdOutput =B"Old file: """,File_scanOlder.Name,=B"""",Eol=yes
        StdOutput =B"New file: """,File_scanNewer.Name,=B"""",Eol=yes
        JNSt [Status::],ArgSwappedDates,.50:
        StdOutput =B"Warning: old scan is younger than the new scan.",Eol=yes
    .50: ; Start to compare File_scanOlder and File_scanNewer.
    .LoadBoth:CALL LoadNewer: ; Output: R8=Volume,RBP=RSI=Pointer to a dir-name,RCX=R12=Size or -1 when at EOF.
    .LoadOld: CALL LoadOlder: ; Output: R9=Volume,RBX=RDI=Pointer to a dir-name,RDX=Size or -1 when at EOF.
    .Compare: CMP RCX,-1
        JNE .55:
        ; File_scanNewer is over or higher. Keep loading File_scanOlder, store it to Buffer5.
        CMP RDX,-1
        JE .EndCompare:       ; Both files are at end.
        JMP .NewIsHigher:
    .55:CMP RDX,-1
        JE .OldIsHigher:      ; File_scanOlder is over or higher. Keep loading File_scanNewer, store it to Buffer1.
        CMP ECX,EDX
        JBE .60:
        MOV ECX,EDX
    .60:REPE CMPSB            ; Compare at the matching size.
        MOV RDI,RBX
        JB .OldIsHigher:
        JA .NewIsHigher:
        CMP R12,RDX           ; When names match, look at their sizes.
        JB .OldIsHigher:
        JA .NewIsHigher:
    ; NoneIsHigher; Directory names are equal.
    ; ScanIndex goes to Buffer2,3 or 4.  SCAN_INDEX.Ptr is relative to [NewerInMemory].
        SUB RBP,[NewerInMemory]
        MOV [ScanIndex.Ptr],EBP
        MOV [ScanIndex.Size],EDX
        SUB R8,R9        ; Volume difference is in R8.
        MOV [ScanIndex.Volume],R8
        JNE .75:
        MOV RBX,[Buffer3]
        MOV [ScanIndex.Volume],R9
        JMP .80:
    .75:MOV RBX,[Buffer4]
        JB .80:
        MOV RBX,[Buffer2]
    .80:BufferStore RBX,ScanIndex,SIZE# ScanIndex
        JC ErrorBuffer
        JMP .LoadBoth:
    .NewIsHigher:  ; Line retrieved from File_scanNewer is higher or it already ended.
    ; Keep the line (in R8,RBP,R12) and store the index of line retrieved from File_scanOlder (R9,RBX=RDI,RDX).
    ; ScanIndex goes to Buffer5, SCAN_INDEX.Ptr is relative to [OlderInMemory].
        SUB RBX,[OlderInMemory]
        MOV [ScanIndex.Ptr],EBX
        MOV [ScanIndex.Size],EDX
        NEG R9
        MOV [ScanIndex.Volume],R9
        MOV RBX,[Buffer5]
        BufferStore RBX,ScanIndex,SIZE# ScanIndex
        JC ErrorBuffer
        MOV RSI,RBP
        MOV RCX,R12
        JMP .LoadOld:
    .OldIsHigher:; Line retrieved from File_scanOlder is higher or it already ended.
    ; Keep the line (in R9,RBX=RDI,RDX) and store the index of line retrieved from File_scanNewer (R8,RBP=RSI,R12=RCX).
    ; ScanIndex goes to Buffer1, SCAN_INDEX.Ptr is relative to [NewerInMemory],
        SUB RBP,[NewerInMemory]
        MOV [ScanIndex.Ptr],EBP
        MOV [ScanIndex.Size],R12D
        MOV [ScanIndex.Volume],R8
        MOV R10,RBX
        MOV RBX,[Buffer1]
        BufferStore RBX,ScanIndex,SIZE# ScanIndex
        JC ErrorBuffer
        MOV RBX,R10
        CALL LoadNewer:
        JMP .Compare:
    .EndCompare:
    Nr  %FOR 1..5                           ; Set the lengths of five buffers to NumberxA and NumberxW.
           BufferRetrieve [Buffer%Nr]
           MOV EAX,ECX
           SHR EAX,4
           LEA RDI,[Number%Nr]
           StoD RDI
           XOR EAX,EAX
           STOSB
        %ENDFOR Nr
        SetSt [Status::],ArgDescending
        MOV R8,[NewerInMemory]
        BufferRetrieve [Buffer1]
        SHR ECX,4                        ; Let RCX=number of SCAN_INDEX records (16 bytes each).
        ShellSort RSI,RCX,16,ByVolume    ; ArgDescending, R8=NewerInMemory
        BufferRetrieve [Buffer2]
        SHR ECX,4                        ; Let RCX=number of SCAN_INDEX records (16 bytes each).
        ShellSort RSI,RCX,16,ByVolume    ; ArgDescending, R8=NewerInMemory
        BufferRetrieve [Buffer3]
        SHR ECX,4                        ; Let RCX=number of SCAN_INDEX records (16 bytes each).
        ShellSort RSI,RCX,16,ByVolume    ; ArgDescending, R8=NewerInMemory
        RstSt [Status::],ArgDescending
        BufferRetrieve [Buffer4]
        SHR ECX,4                        ; Let RCX=number of SCAN_INDEX records (16 bytes each).
        ShellSort RSI,RCX,16,ByVolume    ; ArgAscending, R8=NewerInMemory
        MOV R8,[OlderInMemory]
        BufferRetrieve [Buffer5]
        SHR ECX,4                        ; Let RCX=number of SCAN_INDEX records (16 bytes each).
        ShellSort RSI,RCX,16,ByVolume    ; ArgAscending, R8=OlderInMemory
        StdOutput EuroDirs, =B" has found", Eol=yes
    Nr  %FOR 1..5
          StdOutput Number%Nr, Report%Nr, Eol=yes
        %ENDFOR Nr
        CLC
    .90:RET
       ENDP LoadAndCompareScans

ExcludeDir
  Procedure ExcludeDir returns Carry flag when the directory name in Dir is among the ArgExcludeDirs.
  Directory name does not end with `/` unless it is the root directory.
Input
  ExcludedDirsPtr, ExcludedDirsEnd
Output
  CF=1 if Dir is excluded from scanning.
  CF=0 Dir is not excluded, perform the scan.
Called by
  Scan.
Clobbers
  RAX

    ExcludeDir PROC
        PUSH RCX,RSI,RDI
         MOV RSI,[ExcludedDirsPtr]
    .10: CMP RSI,[ExcludedDirsEnd]
         JAE .90:
         LODSQ           ; The first QWORD is directory name size,
         MOV RCX,RAX
         LODSQ           ;  the second is a pointer to the excluded name.
         JRCXZ .10:
         XCHG RSI,RAX
         LEA RDI,[Dir]
         REPE CMPSB
         XCHG RAX,RSI
         JNE .10:
         STC             ; Yes, it is excluded.
    .90:POP RDI,RSI,RCX
        RET
      ENDP ExcludeDir

ParseToBuffer
  Procedure ParseToBuffer will parse a multiple semicolon-separated pathes pointed to by RSI and for each path it stores two QWORDs to the buffer: directory size and pointer.
Called by
  Scan.
Input
  RAX=output empty BUFFER
  RSI=pointer to ArgIncludeDirs or ArgExcludeDirs.
Output
  The buffer at RAX is filled with two QWORDS {size and pointer) for each semicolon-separated directory.
  When the directory name ends with `/`, it is omitted if it's not the root directory `/`.
Error
  Abort by jump to ErrorBuffer.
Called by
  Scan
Clobbers
  RAX,RBX,RDX,RSI,RDI

    ParseToBuffer PROC
        MOV RBX,RAX           ; The output buffer.
    .10:MOV RDX,RSI           ; Pointer at the (perhaps multiple) directories.
    .20:LODSB
        CMP AL,0
        JE .50:
        CMP AL,';'            ; ; is the folder names separator.
        JNE .20:
        CALL .50:             ; Store to buffer and continue.
        JMP .10:
    .50:; Tail-procedure which will store the size RSI-1-RDX and pointer RDX to the buffer RBX.
        LEA RAX,[RSI-1]
        SUB RAX,RDX
        JZ .90:               ; Ignore empty directory.
        CMPB [RAX+RDX-1],'/'
        JNE .70:
        DEC RAX
        CMP RAX,RDX
        JA  .70:
        INC RAX
    .70:PUSH RAX              ; Save folder name size on stack.
          MOV RAX,RSP
          BufferStore RBX,RAX,8 ; Dir name size in bytes.
        POP RAX
        JC ErrorBuffer
        PUSH RDX                ; Save pointer to folder name on stack.
          MOV RAX,RSP
          BufferStore RBX,RAX,8 ; Dir ptr.
        POP RAX
        JC ErrorBuffer
    .90:RET
      ENDP ParseToBuffer

LoadOlder
  Load one directory name and volume from File_scanOlder mapped at OlderInMemory..OlderEndInMemory.
  Each record has variable size: `/dir_name 0x09 dir_volume_decimal 0x0A`
Input
  OlderPtr is pointer to one record,
  OlderEndInMemory points to the end of records.
Output
  R9=volume,
  RBX=RDI=pointer to a directory name,
  RDX=directory name size or -1 when at EOF.
  OlderPtr is updated.
Preserves
  R8,RCX,RSI,RBP
Clobbers
  RAX,R10,R11

    LoadOlder PROC
        MOV R10,RCX                     ; Save preserved RCX.
        MOV RDI,[OlderPtr]
        MOV RCX,[OlderEndInMemory]
        MOV RBX,RDI
        MOV RDX,RDI
        SUB RCX,RDI
        JNA .90:
        MOV AL,9
        REPNE SCASB
        MOV R11,RSI
        MOV RSI,RDI
        DEC RDI                         ; Go at HT.
        SUB RDI,RDX
        XCHG RDI,RDX
        LodD RSI
        MOV R9,RAX
        INC RSI                         ; Skip LF.
        MOV [OlderPtr],RSI
        MOV RSI,R11
        MOV RCX,R10                     ; Restore preserved RCX.
        RET                             ; Normal end.
    .90:MOV RDX,-1
        MOV RCX,R10                     ; Restore preserved RCX.
        RET                             ; End at EOF.
      ENDP LoadOlder

LoadNewer
  Load one directory name and volume from File_scanNewer mapped at NewerInMemory..NewerEndInMemory.
  Each record has variable size: `/dir_name 0x09 dir_volume_decimal 0x0A`
Input
  NewerPtr is pointer to one record,
  NewerEndInMemory points to the end of records.
Output
  R8=volume,
  RBP=RSI=pointer to a directory name,
  RCX=R12=directory name size or -1 when at EOF.
  NewerPtr is updated.
Preserves
  R9,RBX,RDX,RDI
Clobbers
  RAX,R10

    LoadNewer PROC
        MOV R10,RDI                     ; Save preserved RDI.
        MOV RDI,[NewerPtr]
        MOV RCX,[NewerEndInMemory]
        MOV RBP,RDI
        SUB RCX,RDI
        JNA .90:
        MOV AL,9
        REPNE SCASB
        MOV RSI,RDI
        DEC RDI
        SUB RDI,RBP
        MOV ECX,EDI
        LodD RSI
        MOV R8,RAX
        INC RSI                         ; Skip LF.
        MOV [NewerPtr],RSI
        MOV RDI,R10                     ; Restore preserved RDI.
        MOV RSI,RBP                     ; Normal end.
        MOV R12,RCX
        RET                             ; Normal end.
    .90:MOV RDI,R10                     ; Restore preserved RDI.
        MOV RCX,-1
        MOV R12,RCX
        RET                             ; End at EOF.
      ENDP LoadNewer

ByName
  Procedure ByName is a callback function to sort array of indexes by directory name.
Input
  RSI,RDI point to SCAN_INDEX records to compare.
Clobbers
  RAX,RDX,RSI,RDI,R10,R11,R12
Called by
  Scan.

    ByName: PROC
        MOV R10,RCX
        MOV R11,RSI
        MOV R12,RDI
        MOV EDX,[RDI+SCAN_INDEX.Size]
        MOV ECX,[RSI+SCAN_INDEX.Size]
        CMP ECX,EDX
        JBE .10:
        MOV ECX,EDX
    .10:MOV ESI,[RSI+SCAN_INDEX.Ptr]
        MOV EDI,[RDI+SCAN_INDEX.Ptr]
        ADD RSI,[NamesInMemory]
        ADD RDI,[NamesInMemory]
        DEC ECX
        XCHG RSI,RDI
        REPE CMPSB
        JA .90:
        JB .50:
        CMP EDX,[R11+SCAN_INDEX.Size]
        JAE .90
    .50:MOV RAX,[R11+SCAN_INDEX.Ptr]
        MOV RDX,[R11+SCAN_INDEX.Volume]
        XCHG RAX,[R12+SCAN_INDEX.Ptr]
        XCHG RDX,[R12+SCAN_INDEX.Volume]
        MOV [R11+SCAN_INDEX.Ptr],RAX
        MOV [R11+SCAN_INDEX.Volume],RDX
    .90:MOV RCX,R10
        RET
      ENDP ByName:

ByDate
  Procedure ByDate is a callback function to sort a table of DateMask records in ScanDirBuffer.
Input
  RSI,RDI point to DateMask records to compare.
Clobbers
  RAX,RDX,R10,R11,R12
Called by
  Scan.

    ByDate: PROC
        XCHG RSI,RDI
        PUSH RCX,RSI,RDI
         MOV ECX,SIZE# DateMask
         REPE CMPSB
        POP RDI,RSI,RCX
        JAE .90:
        MOV RAX,[RSI+0]
        MOV RDX,[RSI+8]
        XCHG [RDI+0],RAX
        XCHG [RDI+8],RDX
        MOV  [RSI+0],RAX
        MOV  [RSI+8],RDX
    .90:RET
      ENDP ByDate

ByVolume
  Procedure ByVolume is a callback function to sort a table of SCAN_INDEX records by volume and then by directory name.
Input
  RSI,RDI point to SCAN_INDEX records to compare.
  R8 contains [NewerInMemory] or [OlderInMemory].
  ArgDescending specifies the sort direction.
Clobbers
  RAX,RDX,RSI,RDI

    ByVolume:PROC
        JSt [Status::],ArgDescending,.10:
        XCHG RSI,RDI
    .10:MOV RAX,[RSI+SCAN_INDEX.Volume]
        MOV RDX,[RDI+SCAN_INDEX.Volume]
        CMP RDX,RAX
        CLC
        JG .90:
        MOV R10,RCX
        MOV R11,RSI
        MOV R12,RDI
        XCHG RSI,RDI
        JL .60:
        MOV ECX,[RSI+SCAN_INDEX.Size]
        MOV ESI,[RSI+SCAN_INDEX.Ptr]
        MOV EDX,[RDI+SCAN_INDEX.Size]
        MOV EDI,[RDI+SCAN_INDEX.Ptr]
        ADD RSI,R8
        ADD RDI,R8
        CMP ECX,EDX
        JBE .20:
        MOV ECX,EDX
    .20:REPE CMPSB
        JA .80:
        JB .50:
        CMP EDX,[R11+SCAN_INDEX.Size]
        JAE .80:
    .50:MOV RAX,[R11+SCAN_INDEX.Volume]
        MOV RDX,[R12+SCAN_INDEX.Volume]
    .60:MOV [R12+SCAN_INDEX.Volume],RAX
        MOV [R11+SCAN_INDEX.Volume],RDX
        MOV RAX,[R11+SCAN_INDEX.Ptr]
        MOV RDX,[R12+SCAN_INDEX.Ptr]
        MOV [R12+SCAN_INDEX.Ptr],RAX
        MOV [R11+SCAN_INDEX.Ptr],RDX
        STC
    .80:MOV RCX,R10
    .90:RET
     ENDP ByVolume:

SetToday
  Procedure SetToday will set a nominal local time of a scan into string variable Today.
Called by
  MainCon.
Clobbers
  RAX,RBX,RCX,RSI,RDI

    SetToday PROC
        MOV ECX,[ArgTimeZone::]     ; 0, 1, .. 23.
        MOV EAX,60*60
        MUL ECX
        MOV ESI,EAX
        LinABI time,0               ; Returns RAX=seconds since 1.1.1970 00:00:00 UTC.
        LEA RBX,[SystemTime]
        Time2SystemTime RBX, TZ=RSI ; Convert time RAX to a SystemTime structure.
        LEA RDI,[Today]
    .50:MOVZXW EAX,[RBX+SYSTEMTIME.wYear]
        StoD RDI,Size=4,Signed=no,Align=right,LeadingZeroes=yes
        MOV AL,'.'
        STOSB
        MOVZXW EAX,[RBX+SYSTEMTIME.wMonth]
        StoD RDI,Size=2,Signed=no,Align=right,LeadingZeroes=yes
        MOV AL,'.'
        STOSB
        MOVZXW EAX,[RBX+SYSTEMTIME.wDay]
        StoD RDI,Size=2,Signed=no,Align=right,LeadingZeroes=yes
        MOV AL,'_'
        STOSB
        MOVZXW EAX,[RBX+SYSTEMTIME.wHour]
        StoD RDI,Size=2,Signed=no,Align=right,LeadingZeroes=yes
        MOV AL,'.'
        STOSB
        MOVZXW EAX,[RBX+SYSTEMTIME.wMinute]
        StoD RDI,Size=2,Signed=no,Align=right,LeadingZeroes=yes
        RET
     ENDP SetToday

SetMaxDaysAgo
  Procedure SetMaxDaysAgo will set a current time minus /MaxDays= into string variable MaxDaysAgo.
Called by
  MainCon.
Uses
  SetToday.
Clobbers
  RAX,RBX,RCX,RSI,RDI

    SetMaxDaysAgo PROC
       MOV ECX,[ArgTimeZone::]
       MOV EAX,60*60
       MUL ECX
       MOV EDI,EAX
       MOV EAX,24*60*60
       MOV ECX,[ArgMaxDays::]     ; Default is 32 days.
       MUL ECX
       MOV ESI,EAX
       LinABI time,0              ; Returns RAX=seconds since 1.1.1970 00:00:00 UTC.
       SUB EAX,ESI
       LEA RBX,[SystemTime]
       Time2SystemTime RBX,TZ=RDI ; Convert time RAX to a SystemTime structure.
       LEA RDI,[MaxDaysAgo]       ; MaxDaysAgo  DB "yyyy.mm.dd_hh.mm",0 ; Today-ArgMaxDays in bytes.
       JMP SetToday.50:           ; Use the tail of procedure SetToday.
      ENDP SetMaxDaysAgo

EraseOlderFile
  Procedure EraseOlderFile is called as a callback from FileEach in Scan.
Input
  RBX=^FILE64 with the name.
  RDI=^WIN32_FIND_DATAW
Clobbers
  RAX,RCX,RSI,RDI

    EraseOlderFile: PROC
        LEA RSI,[RBX+FILE64.Name]
        GetLength$ RSI
        LEA RSI,[RSI+RCX-4-SIZE# MaxDaysAgo]
        LEA RDI,[MaxDaysAgo]
        MOV ECX,SIZE# MaxDaysAgo -1
        REPE CMPSB                      ; Compare datetime encoded in the file name.
        JAE .90:
        LEA RAX,[RBX+FILE64.Name]
        StdOutput =B"Old scan """,RAX,=B""" was deleted.",Eol=yes
        FileDelete RBX
    .90:RET
      ENDP EraseOlderFile:

StoreFileName
  Procedure StoreFileName is called as a callback from FileEach.
  It stores the ASCII strings such as "2024.12.31_11.00",0 from the scan file name to ScanDirBuffer.
Input
  RSI=pointer to file name without path, e.g. eurodirs_2024.12.31_11.00.scan.
  [ScanDirBuffer] is the buffer for storing the dates.
Clobbers
  RAX,RSI

    StoreFileName:PROC
        BufferStore [ScanDirBuffer],RSI,SIZE# DateMask - 1 ; 16 bytes.
        JC ErrorBuffer
        RET
       ENDP StoreFileName

ReadFile_ini
  Procedure ExcludeDir reads File_ini (UTF-8) and parses its each line into configuration, in UTF-8, too.
  It writes information about the file name and whether it was found.
Called by
  MainCon.
Input
  File_ini is an assigned file.
Clobbers
  RAX,RCX,RDX,RSI,RDI

    ReadFile_ini PROC
        SetSt [Status::],ArgFromFile
        StdOutput =B"Configuration """,File_ini.Name,=B'"'
        FileStreamOpen File_ini,BufSize=4K
        JNC .10:
        StdOutput =B" was not found.",Eol=yes
        JMP .90:
    .10:FileStreamReadLn File_ini
        JBE .80:
        MOV ECX,EAX
        ; The first line may begin with BOM.
        MOV AX,[RSI]
        CMPW AX,0xBBEF   ; UTF-8 BOM?
        JNE .20:
        ADD RSI,3        ; Skip the BOM.
        SUB ECX,3
        JB .80:
        JMP .20:
    .15:FileStreamReadLn File_ini ; Read a line to RSI,RAX.
        JBE .80:
        MOV ECX,EAX
    .20:CALL ArgParse::
        JNC .15:
    .Help:StdOutput ErrorMessage::,Eol=yes
        JNSt [Status::],ArgHelpRequired,.70:
        LEA RDI,[Help+3]
        StdOutput RDI, Eol=yes
    .70:TerminateProgram 8
    .80:FileClose File_ini
        StdOutput =B" was accepted.",Eol=yes
    .90:RstSt [Status::],ArgFromFile
        RET
      ENDP ReadFile_ini

Error
  Simplified error handling will abort the program with errorlevel 8.
Input
  RBX=^FILE64 which caused the error.
Called by
  Scan

    Tidy: LEA RDI,[TermIO::]
          LinABI ioctl,0,TCGETS,RDI          ; Load current local terminal status.
          SetSt [RDI+TERMIO.c_lflag],ICANON+ECHO
          LinABI ioctl,0,TCSETS,RDI          ; Enable terminal echo.
          FileClose File_scan, File_scanOlder, File_scanNewer
          StdOutput EscShowCursor::,EscDisableMouse::,EscErase::,Eol=yes
          RET
    ErrorBuffer:
          CALL Tidy:
          StdOutput =B"Error allocating memory.",Eol=yes
          TerminateProgram 8
    ErrorZeroSize:; RBX=^FILE64
          CALL Tidy:
          StdOutput =B'Error: zero size of the file "'
          JMP Msg
    ErrorMap:  ; RBX=^FILE64
          CALL Tidy:
          StdOutput =B'Error: memory-mapping the file "'
          JMP Msg
    ErrorFile: ; RBX=^FILE64
          CALL Tidy:
          StdOutput =B'Error: could not open the file "'
          JMP Msg
    ErrorWrite:  ; RBX=^FILE64
          CALL Tidy:
          StdOutput =B'Error writing to the file "'
    Msg:  LEA RAX,[RBX+FILE64.Name]
          StdOutput RAX,=B'"',Eol=yes
          TerminateProgram 8

       ENDPROGRAM dirslinc

▲Back to the top▲