////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2024 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWLIB.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// Checksum MD5 calculator/verifier, file integrity checker.                  //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 2016xxxx - Creation                                                        //
// 20240118 - Modified for FPC (A.K.)                                         //
////////////////////////////////////////////////////////////////////////////////

program checksum; // CheckSum calculator/verifier.

{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

{$WARN 5023 off : Unit "$1" not used in $2}
{$WARN 5028 off : Local $1 "$2" is not used}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 sysutils, classes, math, masks,
 _crw_alloc, _crw_cmdargs, _crw_environ,
 _crw_ef, _crw_str, _crw_fio, _crw_crypt;

{$R *.res}

const
 stdin                     = 0;                // Identify stdin
 stdout                    = 1;                // Identify stdout
 stderr                    = 2;                // Identify stderr
 ScanSpacesMD5             = JustSpaces+['*']; // Delimeters for *.MD5
 CommentDelims             = [';','#'];        // Comment delimeters
 Quiet        : Boolean    = False;            // Show less details
 Verbose      : Boolean    = False;            // Show more details
 MakeMd5      : Boolean    = False;            // false/true=check/make
 FilePattern  : LongString = '';               // File pattern to make MD5
 FileExclude  : LongString = '';               // File pattern to exclude
 BaseDir      : LongString = '';               // Base directory
 Md5File      : LongString = '';               // *.md5 file
 LogFile      : LongString = '';               // *.log file
 DefBaseDir   : LongString = '';               // default basedir
 DefMd5File   : LongString = '';               // default *.md5 file
 DefLogFile   : LongString = '';               // default *.log file
 Recursion    : Integer    = 0;                // Max.subdirectories
 FilesChecked : Integer    = 0;                // Count of files checked
 FilesConfirm : Integer    = 0;                // Count of files suucess
 FilesMakeMd5 : Integer    = 0;                // Count of files md5 made
 ErrorsBadMd5 : Integer    = 0;                // Count of errors bad checksum
 ErrorsAbsent : Integer    = 0;                // Count of errors files not found
 ErrorsFormat : Integer    = 0;                // Count of errors MD5 format
 ErrorsWrite  : Integer    = 0;                // Count of errors file write
 ErrorsOpen   : Integer    = 0;                // Count of errors file open
 ErrorsRead   : Integer    = 0;                // Count of errors file read
 ErrorsFails  : Integer    = 0;                // Count of errors failure
 ErrorsTotal  : Integer    = 0;                // Count of errors total
 ecOk                      = 0;                // Exit code Success
 ecErrors                  = 1;                // Exit code Errors found
 ecBadArgs                 = 2;                // Exit code Bad Arguments
 ecFailure                 = 3;                // Exit code Failure
 DefRecursion              = 1024;             // Default recursion

procedure FixError(var Errors:Integer);
begin
 inc(Errors);
 inc(ErrorsTotal);
end;

procedure HandleException(E:Exception; aHalt:Boolean=False);
var Msg:LongString;
begin
 FixError(ErrorsFails);
 if (Pos('Error: ',E.Message)<>1) then Msg:='Error: '+E.Message+EOL else Msg:=E.Message+EOL;
 if (FileWrite(GetStdHandle(STD_ERROR_HANDLE),PChar(Msg)^,Length(Msg))<0) then FixError(ErrorsWrite);
 if aHalt then Halt(ecFailure);
 ExitCode:=ecFailure;
end;

procedure Print(n:Integer; const S:LongString; const LogFile:LongString='');
var h:THandle; F:Text;
begin
 if (S<>'') then
 try
  h:=0;
  case n of
   stdout: h:=GetStdHandle(STD_OUTPUT_HANDLE);
   stderr: h:=GetStdHandle(STD_ERROR_HANDLE);
  end;
  if (h<>0) then if FileWrite(h,PChar(S)^,Length(S))<0  then FixError(ErrorsWrite);
  if not IsEmptyStr(LogFile) then begin
   IoResult;
   Assign(F,Trim(LogFile));
   if FileExists(Trim(LogFile)) then Append(F) else Rewrite(F);
   if (IoResult<>0) then FixError(ErrorsOpen);
   try
    Write(F,S);
    if (IoResult<>0) then FixError(ErrorsWrite);
   finally
    Close(F);
   end;
  end;
 except
  on E:Exception do HandleException(E);
 end;
end;

procedure Fatal(n:Integer; const msg:LongString; const LogFile:LongString='');
begin
 Print(stderr,msg,LogFile);
 Halt(n);
end;

function IsInteractive:Boolean;
begin
 Result:=False;
 if Quiet then Exit;
 if (ParamCount>0) then Exit;
 if (GetFileType(GetStdHandle(STD_INPUT_HANDLE))<>FILE_TYPE_CHAR) then Exit;
 if (GetFileType(GetStdHandle(STD_OUTPUT_HANDLE))<>FILE_TYPE_CHAR) then Exit;
 if (GetFileType(GetStdHandle(STD_ERROR_HANDLE))<>FILE_TYPE_CHAR) then Exit;
 Result:=True;
end;

function ReadAnyKey:Char;
var c:Char;
begin
 if (FileRead(GetStdHandle(STD_INPUT_HANDLE),c,SizeOf(c))=SizeOf(c))
 then Result:=c else Result:=Chr(0);
end;

procedure WaitAnyKey;
begin
 if IsInteractive then begin
  Print(stdout,'Press Enter to continue...');
  ReadAnyKey;
 end;
end;

procedure PrintErrors(n:Integer; const Prefix:LongString; const LogFile:LongString='');
begin
 if n in [stdout,stderr] then begin
  if Verbose or (ErrorsAbsent<>0) then
  Print(n,Prefix+Format('Error(s) file not found : %d',[ErrorsAbsent])+EOL,LogFile);
  if Verbose or (ErrorsBadMd5<>0) then
  Print(n,Prefix+Format('Error(s) bad checksum   : %d',[ErrorsBadMd5])+EOL,LogFile);
  if Verbose or (ErrorsFormat<>0) then
  Print(n,Prefix+Format('Error(s) file format    : %d',[ErrorsFormat])+EOL,LogFile);
  if Verbose or (ErrorsOpen<>0) then
  Print(n,Prefix+Format('Error(s) file open      : %d',[ErrorsOpen])+EOL,LogFile);
  if Verbose or (ErrorsRead<>0) then
  Print(n,Prefix+Format('Error(s) file read      : %d',[ErrorsRead])+EOL,LogFile);
  if Verbose or (ErrorsWrite<>0) then
  Print(n,Prefix+Format('Error(s) file write     : %d',[ErrorsWrite])+EOL,LogFile);
  if Verbose or (ErrorsFails<>0) then
  Print(n,Prefix+Format('Error(s) failure        : %d',[ErrorsFails])+EOL,LogFile);
  Print(n,Prefix+Format('Error(s) total          : %d',[ErrorsTotal])+EOL,LogFile);
 end;
end;

function CheckFileMd5(const FileName:LongString):Integer;
var F:Text; Line,md5,summ,item:LongString; i,iLine:Integer;
begin
 Result:=-1;
 try
  if IsEmptyStr(FileName) then begin
   Print(stderr,'Error: checksum file is not specified (*.md5)'+EOL,LogFile);
   FixError(ErrorsFails);
   Exit;
  end;
  if not FileExists(FileName) then begin
   Print(stderr,Format('Error: checksum file is not exists "%s"',[Trim(FileName)])+EOL,LogFile);
   FixError(ErrorsAbsent);
   Exit;
  end;
  iLine:=0;
  IoResult;
  Assign(F,Trim(FileName));
  Reset(F);
  if (IoResult<>0) then begin
   Print(stderr,Format('Error: could not open "%s"',[Trim(FileName)])+EOL,LogFile);
   FixError(ErrorsOpen);
  end else
  try
   while not Eof(F) do begin
    Readln(F,Line); Inc(iLine);
    for i:=1 to Length(Line) do
    if (Line[i] in CommentDelims) then begin
     Line:=Copy(Line,1,i-1);
     Break;
    end else begin
     if (Line[i] in JustSpaces) then Continue else Break;
    end;
    if (IoResult<>0) then begin
     Print(stderr,Format('Error: could not read "%s"',[Trim(FileName)])+EOL,LogFile);
     FixError(ErrorsRead);
     Break;
    end;
    Line:=Trim(Line);
    if IsEmptyStr(Line) then Continue;
    if (WordCount(Line,ScanSpacesMD5)<2) then begin
     Print(stderr,Format('Error: bad MD5 format file "%s" line "%d"',[Trim(FileName),iLine])+EOL,LogFile);
     FixError(ErrorsFormat);
     Continue;
    end;
    md5:=ExtractWord(1,Line,ScanSpacesMD5);
    item:=Trim(SkipWords(1,Line,ScanSpacesMD5));
    if IsRelativePath(item) then item:=Trim(AddPathDelim(BaseDir)+item);
    item:=UnifyFileAlias(item); // Validate file name
    if FileExists(item) then begin
     summ:=GetMD5FromFile(item,df_Hex);
     if SameText(md5,summ) then begin
      if not Quiet then if Verbose then
      Print(stdout,' SUCCESS   : '+item+EOL,LogFile);
      inc(FilesConfirm);
     end else begin
      if not Quiet then
      Print(stdout,' BAD MD5   : '+item+EOL,LogFile);
      FixError(ErrorsBadMd5);
     end;
    end else begin
     if not Quiet then
     Print(stdout,' NOT FOUND : '+item+EOL,LogFile);
     FixError(ErrorsAbsent);
    end;
    inc(FilesChecked);
   end;
  finally
   Close(F);
  end;
  if (IoResult<>0) then begin
   Print(stderr,Format('Error: could not read "%s"',[Trim(FileName)])+EOL,LogFile);
   FixError(ErrorsRead);
  end;
  Print(stdout,Format('File(s) checked         : %d',[FilesChecked])+EOL,LogFile);
  Print(stdout,Format('File(s) confirmed OK    : %d',[FilesConfirm])+EOL,LogFile);
  PrintErrors(stdout,'',LogFile);
  Result:=ErrorsTotal;
 except
  on E:Exception do HandleException(E);
 end;
end;

function HasSubString(const sub,str:LongString):Boolean;
var wsub,wstr:WideString;
begin
 wsub:=WideLowerCase(StrToWide(sub));
 wstr:=WideLowerCase(StrToWide(str));
 Result:=(Pos(wsub,wstr)>0);
end;

procedure DoCalcMd5(const FileName    : LongString;
                    const FileDetails : TSearchRec;
                          SubDirLevel : Integer;
                      var Terminate   : Boolean;
                          CustomData  : Pointer);
var md5,exword,RelFileName,TestFileName:LongString; i:Integer;
begin
 if not SameText(Trim(FileName),Trim(LogFile)) then
 if not SameText(Trim(FileName),Trim(Md5File)) then
 //if not SameText(Trim(FileName),Trim(ProgName)) then
 if not HasFlags(FileDetails.Attr,faDirectory) then begin
  if IsRelativePath(Trim(FileName))
  then RelFileName:=Trim(FileName)
  else RelFileName:=ExtractRelativePath(AddPathDelim(BaseDir),Trim(FileName));
  TestFileName:=AddPathDelim('.')+RelFileName;
  if (FileExclude<>'') then
  for i:=1 to WordCount(FileExclude,JustSpaces) do begin
   exword:=ExtractWord(i,FileExclude,JustSpaces);
   if IsWildCard(exword) then begin
    if MatchesMask(TestFileName,ExtractWord(i,FileExclude,JustSpaces)) then Exit;
   end else begin
    if HasSubString(exword,TestFileName) then Exit;
   end;
  end;
  md5:=GetMd5FromFile(FileName,df_Hex);
  if IsEmptyStr(md5) then begin
   Print(stderr,Format('Error: could not make MD5 for file "%s"',[FileName])+EOL);
   FixError(ErrorsBadMd5);
  end else begin
   Print(stdout,Format('%s *%s',[md5,RelFileName])+EOL,Md5File);
   Inc(FilesMakeMd5);
  end;
 end;
end;

function VersionInfo:LongString;
const Info:LongString='';
begin
 if (Info='') then Info:=GetFileVersionInfoAsText(ProgName);
 Result:=Info;
end;

function FileDescription:LongString;
begin
 Result:=CookieScan(VersionInfo,'FileDescription');
end;

function ProductVersion:LongString;
begin
 Result:=CookieScan(VersionInfo,'ProductVersion');
end;

function LegalCopyright:LongString;
begin
 Result:=CookieScan(VersionInfo,'LegalCopyright');
end;

function DotEnding(const S:LongString):LongString;
const dot='.';
begin
 Result:=S;
 if (Result='') then Exit;
 if (StrFetch(Result,Length(Result))<>dot) then Result:=Result+dot;
end;

procedure ShowHelp;
var sAbout,base:LongString;
begin
 base:=ExtractBaseName(ProgName);
 sAbout:=TrimDef(DotEnding(base+' version '+ProductVersion),'Help on '+base+' command.')+EOL
        +TrimDef(DotEnding(LegalCopyright),'Copyright(c) Alexey Kuryakin 2021-2024 daqgroup@mail.ru.')+EOL
        +TrimDef(DotEnding(FileDescription),base+' - utility to verify/calculate MD5 checksum.')+EOL
        +''+EOL
        +'Usage: '+base+' [-opt[para]] [args] ...'+EOL
        +' [-opt] is option, of -short or --long style'+EOL
        +' [para] is parameter which depends on option'+EOL
        +' [args] is arguments which depends on option'+EOL
        +' many options and arguments is allowed:'+EOL
        +' '+base+' -h              - same as --help'+EOL
        +' '+base+' --help          - show help and exit'+EOL
        +' '+base+' -q              - same as --quiet'+EOL
        +' '+base+' --quiet         - quiet print mode, show less details'+EOL
        +' '+base+' -v              - same as --verbose'+EOL
        +' '+base+' --verbose       - verbose print mode, show more details'+EOL
        +' '+base+' -r[r]           - same as --recursion=[r]'+EOL
        +' '+base+' --recursion=[r] - set maximal recursion level to make md5 checksum'+EOL
        +'                          - if -r option is not set, no recursion, i.e. r=0'+EOL
        +'                          - if -r option uses, r=1024 by default'+EOL
        +' '+base+' -b[b]           - same as --basedir=[b]'+EOL
        +' '+base+' --basedir=[b]   - set base directory, by default current directory'+EOL
        +'                          - base directory is start directory where checksum'+EOL
        +'                          - to be verified or calculated; all files may be set'+EOL
        +'                          - relative to base directory'+EOL
        +' '+base+' -c[m]           - same as --check=[m]'+EOL
        +' '+base+' --check=[m]     - check (verify) file integrity, it`s default option'+EOL
        +'                          - optional parameter [m] specify *.md5 file to check'+EOL
        +'                          - by default check checksum.md5 in current directory'+EOL
        +'                          - [m] may be set relative to basedir, see -b option'+EOL
        +' '+base+' -l[l]           - same as --logfile=[l]'+EOL
        +' '+base+' --logfile=[l]   - write check results to specified log file [l]'+EOL
        +'                          - [l] may be set relative to basedir, see -b option'+EOL
        +' '+base+' -m[m] [p]       - same as --make=[m] [p]'+EOL
        +' '+base+' --make=[m] [p]  - make new md5 checksum by file mask pattern [p]'+EOL
        +'                          - optional parameter [m] specify *.md5 file to write'+EOL
        +'                          - by default, if skipped, pattern [p] is *.*'+EOL
        +'                          - if [m] specified, write result to [m] file'+EOL
        +'                          - [m] may be set relative to basedir, see -b option'+EOL
        +' '+base+' -e [e]          - same as --exclude [e]'+EOL
        +' '+base+' --exclude [e]   - exclude files specified by file mask pattern [e]'+EOL
        +'                          - by default, if skipped, no exclude files'+EOL
        +'Notes:'+EOL
        +' 1) On Unix SHELL, any file masks must be quoted like "*" to avoid'+EOL
        +'    SHELL filename substitution.'+EOL
        +' 1) On Unix SHELL, use all-files mask "*" istead of *.* in Windows.'+EOL
        +'Exit code returned by '+base+':'+EOL
        +' 0                          - OK, checksum is good'+EOL
        +' 1                          - checksum is not good or file(s) not found'+EOL
        +' 2                          - invalid options or arguments bad specified'+EOL
        +' 3                          - failure, i.e. unexpected error(s) found'+EOL
        +'Examples:'+EOL
        +' '+base+'                   - check file checksum.md5 in current directory'+EOL
        +' '+base+' -h                - show help'+EOL
        +' '+base+' -ctest -v -l      - check test.md5, verbose, log to checksum.log'+EOL
        +' '+base+' -m -e "*.exe" -r  - make checksum of *.* except *.exe, recursive'+EOL
        +' '+base+' -mtest "*.exe"    - make checksum of *.exe, write to test.md5'+EOL
        +' '+base+' -r -mchecksum "*" - make checksum of all files, write to checksum.md5'+EOL
        +' '+base+' -m -bc:\test      - make checksum of *.* relative to base c:\test'+EOL
        +' etc'+EOL;
 Print(stdout,sAbout);
end;

function ListPatterns(const Pattern:LongString):LongString;
var i:Integer; pat:LongString;
begin
 Result:='';
 for i:=1 to WordCount(Pattern,JustSpaces) do begin
  pat:=ExtractWord(i,Pattern,JustSpaces);
  if HasChars(pat,['*','?','[',']']) then pat:=AnsiQuotedStr(pat,QuoteMark);
  if (Result='') then Result:=pat else Result:=Result+' '+pat;
 end;
end;

procedure SetLogFile(const S:LongString; const BaseDir:LongString='');
begin
 LogFile:=Trim(S);
 if not IsEmptyStr(LogFile) then begin
  LogFile:=DefaultExtension(LogFile,'.log');
  if not IsEmptyStr(BaseDir) and IsRelativePath(LogFile)
  then LogFile:=AddPathDelim(BaseDir)+LogFile;
 end;
end;

procedure SetMd5File(const S:LongString; const BaseDir:LongString='');
begin
 Md5File:=Trim(S);
 if not IsEmptyStr(Md5File) then begin
  Md5File:=DefaultExtension(Md5File,'.md5');
  if not IsEmptyStr(BaseDir) and IsRelativePath(Md5File)
  then Md5File:=AddPathDelim(BaseDir)+Md5File;
 end;
end;

procedure SetBaseDir(const S:LongString);
begin
 BaseDir:=Trim(S);
 if IsEmptyStr(BaseDir) or IsRelativePath(BaseDir)
 then BaseDir:=DropPathDelim(FExpand(AddPathDelim(DefBaseDir)+BaseDir));
end;

procedure SetCheckFile(const S:LongString);
begin
 MakeMd5:=False;
 Md5File:=Trim(S);
end;

procedure SetMakeFile(const S:LongString);
begin
 MakeMd5:=True;
 Md5File:=Trim(S);
end;

function RelMd5File:LongString;
begin
 if IsEmptyStr(Md5File) then Exit('');
 Result:=ExtractRelativePath(AddPathDelim(BaseDir),Md5File);
end;

function CheckOpt(const ShortOpt,LongOpt,Param:LongString; var Arg:LongString):Boolean;
begin
 Result:=False; Arg:='';
 if IsOption(ShortOpt) and (Pos(ShortOpt,Param)=1) then begin
  Arg:=Param; Delete(Arg,1,Length(ShortOpt));
  Result:=True;
  Exit;
 end;
 if IsOption(LongOpt) and (Pos(LongOpt,Param)=1) then begin
  Arg:=Param; Delete(Arg,1,Length(LongOpt));
  Result:=True;
  Exit;
 end;
end;

var
 Param:Integer=1;
 ParamArg:LongString='';

begin
 try
  DefBaseDir:=StartAtDir;
  DefMd5File:=ChangeFileExt(ExtractFileName(ProgName),'.md5');
  DefLogFile:=ChangeFileExt(ExtractFileName(ProgName),'.log');
  Param:=1;
  while (Param<=ParamCount) do begin
   if CheckOpt('-h','--help',ParamStr(Param),ParamArg) then begin
    ShowHelp;
    Exit;
   end else
   if CheckOpt('-q','--quiet',ParamStr(Param),ParamArg) then begin
    Quiet:=True;
   end else
   if CheckOpt('-v','--verbose',ParamStr(Param),ParamArg) then begin
    Verbose:=True;
   end else
   if CheckOpt('-r','--recursion=',ParamStr(Param),ParamArg) then begin
    Recursion:=StrToIntDef(ParamArg,DefRecursion);
   end else
   if CheckOpt('-b','--basedir=',ParamStr(Param),ParamArg) then begin
    if IsEmptyStr(ParamArg)
    then BaseDir:=DefBaseDir
    else SetBaseDir(ParamArg);
   end else
   if CheckOpt('-l','--logfile=',ParamStr(Param),ParamArg) then begin
    if IsEmptyStr(ParamArg)
    then LogFile:=DefLogFile
    else LogFile:=Trim(ParamArg);
   end else
   if CheckOpt('-c','--check=',ParamStr(Param),ParamArg) then begin
    SetCheckFile(ParamArg);
   end else
   if CheckOpt('-m','--make=',ParamStr(Param),ParamArg) then begin
    SetMakeFile(ParamArg);
    inc(Param);
    while (Param<=ParamCount) and (Pos('-',ParamStr(Param))<>1) do begin
     if (FilePattern='')
     then FilePattern:=ParamStr(Param)
     else FilePattern:=FilePattern+' '+ParamStr(Param);
     Inc(Param);
    end;
    if IsOption(ParamStr(Param)) then Continue;
   end else
   if CheckOpt('-e','--exclude', ParamStr(Param),ParamArg) then begin
    Inc(Param);
    while (Param<=ParamCount) and (Pos('-',ParamStr(Param))<>1) do begin
     if Length(FileExclude)=0
     then FileExclude:=ParamStr(Param)
     else FileExclude:=FileExclude+' '+ParamStr(Param);
     inc(Param);
    end;
    if IsOption(ParamStr(Param)) then Continue;
   end else
   if not IsEmptyStr(ParamStr(Param)) then begin
    Fatal(ecBadArgs,'Error: invalid option '+Trim(ParamStr(Param))+EOL);
   end;
   Inc(Param);
  end;
  SetBaseDir(BaseDir);
  if IsEmptyStr(BaseDir) or not DirectoryExists(BaseDir)
  then Fatal(ecBadArgs,'Error: base directory is not exist '+BaseDir+EOL);
  if MakeMd5 then begin
   LogFile:='';
   SetMd5File(Md5File,BaseDir);
   if not IsEmptyStr(Md5File) and FileExists(Md5File) and not DeleteFile(Md5File)
   then Fatal(ecFailure,'Error: could not delete file '+Md5File+EOL);
   if IsEmptyStr(FilePattern) then FilePattern:=AnyFileMask;
   Print(stdout,'; Created by checksum - MD5 checksum calculator/verifier.'+EOL,Md5File);
   Print(stdout,'; Copyright (c) 2016-2024 Alexey Kuryakin daqgroup@mail.ru.'+EOL,Md5File);
   Print(stdout,'; '+FormatDateTime('yyyy.mm.dd-hh:nn:ss -',Now),Md5File);
   Print(stdout,' MD5 checksum created by command:'+EOL,Md5File);
   Print(stdout,'; checksum',Md5File);
   Print(stdout,' -m'+RelMd5File+' '+ListPatterns(FilePattern),Md5File);
   if not IsEmptyStr(FileExclude) then
   Print(stdout,' -e '+ListPatterns(FileExclude),Md5File);
   if (Recursion>0) then
   if (Recursion=DefRecursion) then
   Print(stdout,' -r',Md5File) else
   Print(stdout,' -r'+IntToStr(Recursion),Md5File);
   if not SameText(StartAtDir,BaseDir) then
   Print(stdout,' -b'+ExtractRelativePath(AddPathDelim(DefBaseDir),BaseDir),Md5File);
   Print(stdout,EOL,Md5File);
   Print(stdout,'; Started at directory '+AddPathDelim(StartAtDir)+EOL,Md5File);
   ForEachFile(BaseDir,FilePattern,DoCalcMd5,Recursion,nil);
   Print(stdout,Format('; %s - %d file(s) complete, %d error(s) found',
        [FormatDateTime('yyyy.mm.dd-hh:nn:ss',Now),FilesMakeMd5,ErrorsTotal])+EOL,Md5File);
   if Verbose then PrintErrors(stdout,'; ',Md5File);
   if (ErrorsTotal<>0) then ExitCode:=ecErrors;
  end else begin
   if IsEmptyStr(Md5File) then Md5File:=DefMd5File;
   SetMd5File(Md5File,BaseDir);
   SetLogFile(LogFile,BaseDir);
   if not IsEmptyStr(LogFile) and FileExists(LogFile) and not DeleteFile(LogFile)
   then Fatal(ecFailure,'Error: could not delete file '+LogFile+EOL);
   Print(stdout,FormatDateTime('yyyy.mm.dd-hh:nn:ss',Now)+' - ',LogFile);
   Print(stdout,TrimDef(DotEnding(FileDescription),'checksum - file integrity verifier.')+EOL,LogFile);
   Print(stdout,TrimDef(DotEnding(LegalCopyright),'Copyright(c) 2016-2024 Alexey Kuryakin daqgroup@mail.ru.')+EOL,LogFile);
   Print(stdout,Format('Checking file: %s',[Md5File])+EOL,LogFile);
   if CheckFileMd5(Md5File)<>0 then ExitCode:=ecErrors;
   if (ErrorsTotal<>0) then ExitCode:=ecErrors;
   if (ExitCode<>ecOk)
   then Print(stdout,'File(s) checking result : FAILURE'+EOL,LogFile)
   else Print(stdout,'File(s) checking result : SUCCESS'+EOL,LogFile);
   if IsInteractive then WaitAnyKey;
  end;
 except
  on E:Exception do HandleException(E);
 end;
end.

//////////////
// END OF FILE
//////////////

