unit IOPatch;

{$POINTERMATH ON}

interface

uses
  Threading, Utils, SynCommons, SynCrypto, ParseClass, ParseExpr,
  IOUtils,
  WinAPI.Windows, WinAPI.ShlObj,
  System.SysUtils, System.Classes, System.SyncObjs, System.Math, System.Types,
  System.StrUtils, System.RTLConsts, System.TimeSpan, System.Diagnostics,
  System.IOUtils, System.Generics.Defaults, System.Generics.Collections;

type
  PEncodeOptions = ^TEncodeOptions;

  TEncodeOptions = record
    Threads: Integer;
    MinSize, MaxSize: Int64;
  end;

  PDecodeOptions = ^TDecodeOptions;

  TDecodeOptions = record
    Threads: Integer;
  end;

procedure PrintHelp;
procedure Parse(ParamArg: TArray<string>; out Options: TEncodeOptions);
  overload;
procedure Parse(ParamArg: TArray<string>; out Options: TDecodeOptions);
  overload;
procedure Encode(Input1, Input2: String; Output: TStream;
  Options: TEncodeOptions);
procedure Decode(Input: TStream; Output: String; Options: TDecodeOptions);

implementation

uses
  XDeltaDLL;

procedure PrintHelp;
var
  I, J: Integer;
  S: string;
begin
  WriteLn(ErrOutput, 'patch - creates patches between two data sets');
  WriteLn(ErrOutput, '');
  WriteLn(ErrOutput, 'Usage:');
  WriteLn(ErrOutput, '  xtool patch [parameters] old_data new_data patch_data');
  WriteLn(ErrOutput, '');
  WriteLn(ErrOutput, '');
  WriteLn(ErrOutput, 'Parameters:');
  WriteLn(ErrOutput, '  -t#  - number of working threads [50p]');
  WriteLn(ErrOutput, '');
end;

procedure Parse(ParamArg: TArray<string>; out Options: TEncodeOptions);
var
  ArgParse: TArgParser;
  ExpParse: TExpressionParser;
  S: String;
begin
  ArgParse := TArgParser.Create(ParamArg);
  ExpParse := TExpressionParser.Create;
  try
    S := ArgParse.AsString('-t', 0, '50p');
    S := ReplaceText(S, 'p', '%');
    S := ReplaceText(S, '%', '%*' + CPUCount.ToString);
    Options.Threads := Max(1, Round(ExpParse.Evaluate(S)));
    S := ArgParse.AsString('--min=', 0, '64k');
    S := ReplaceText(S, 'KB', '* 1024^1');
    S := ReplaceText(S, 'MB', '* 1024^2');
    S := ReplaceText(S, 'GB', '* 1024^3');
    S := ReplaceText(S, 'K', '* 1024^1');
    S := ReplaceText(S, 'M', '* 1024^2');
    S := ReplaceText(S, 'G', '* 1024^3');
    Options.MinSize := Max(0, Round(ExpParse.Evaluate(S)));
    S := ArgParse.AsString('--max=', 0, '16g');
    S := ReplaceText(S, 'KB', '* 1024^1');
    S := ReplaceText(S, 'MB', '* 1024^2');
    S := ReplaceText(S, 'GB', '* 1024^3');
    S := ReplaceText(S, 'K', '* 1024^1');
    S := ReplaceText(S, 'M', '* 1024^2');
    S := ReplaceText(S, 'G', '* 1024^3');
    Options.MaxSize := Max(0, Round(ExpParse.Evaluate(S)));
  finally
    ArgParse.Free;
    ExpParse.Free;
  end;
end;

procedure Parse(ParamArg: TArray<string>; out Options: TDecodeOptions);
var
  ArgParse: TArgParser;
  ExpParse: TExpressionParser;
  S: String;
begin
  ArgParse := TArgParser.Create(ParamArg);
  ExpParse := TExpressionParser.Create;
  try
    S := ArgParse.AsString('-t', 0, '50p');
    S := ReplaceText(S, 'p', '%');
    S := ReplaceText(S, '%', '%*' + CPUCount.ToString);
    Options.Threads := Max(1, Round(ExpParse.Evaluate(S)));
  finally
    ArgParse.Free;
    ExpParse.Free;
  end;
end;

procedure Encode(Input1, Input2: String; Output: TStream;
  Options: TEncodeOptions);
var
  I, J, K: Integer;
  I64: Int64;
  B, C: Boolean;
  LFilename: String;
  BaseDir1, BaseDir2: String;
  LList1, LList2: TArray<String>;
  LBytes: TBytes;
  LEntry: TEntryStruct2;
  FStream: TFileStream;
  //SStream1, SStream2: TSharedMemoryStream;
  Tasks: TArray<TTask>;
  CS: TCriticalSection;
  TempDir: String;
begin
  C := FileExists(Input1) and FileExists(Input2);
  if FileExists(Input1) then
    BaseDir1 := ExtractFilePath(TPath.GetFullPath(Input1))
  else if DirectoryExists(Input1) then
    BaseDir1 := IncludeTrailingPathDelimiter(TPath.GetFullPath(Input1))
  else
    BaseDir1 := ExtractFilePath(TPath.GetFullPath(Input1));
  LList1 := GetFileList([Input1], True);
  for I := Low(LList1) to High(LList1) do
    LList1[I] := ReplaceText(LList1[I], BaseDir1, '');
  if FileExists(Input2) then
    BaseDir2 := ExtractFilePath(TPath.GetFullPath(Input2))
  else if DirectoryExists(Input2) then
    BaseDir2 := IncludeTrailingPathDelimiter(TPath.GetFullPath(Input2))
  else
    BaseDir2 := ExtractFilePath(TPath.GetFullPath(Input2));
  LList2 := GetFileList([Input2], True);
  for I := Low(LList2) to High(LList2) do
    LList2[I] := ReplaceText(LList2[I], BaseDir2, '');
  K := XTOOL_PATCH;
  Output.WriteBuffer(K, K.Size);
  if not C then
    for I := High(LList1) downto Low(LList1) do
    begin
      if IndexText(LList1[I], LList2) < 0 then
      begin
        LEntry.Op := TPatchOp.opDelete;
        LEntry.Filename := LList1[I];
        LEntry.Size := FileSize(BaseDir1 + LList1[I]);
        Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
        Delete(LList1, I, 1);
      end;
    end;
  if not C then
    for I := High(LList2) downto Low(LList2) do
    begin
      if IndexText(LList2[I], LList1) < 0 then
      begin
        LEntry.Op := TPatchOp.opMissing;
        LEntry.Filename := LList2[I];
        LEntry.Size := FileSize(BaseDir2 + LList2[I]);
        Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
        FStream := TFileStream.Create(BaseDir2 + LList2[I], fmShareDenyNone);
        try
          CopyStreamEx(FStream, Output, LEntry.Size);
        finally
          FStream.Free;
        end;
        Delete(LList2, I, 1);
      end;
    end;
  for I := High(LList2) downto Low(LList2) do
  begin
    if C then
      LFilename := Input1
    else
      LFilename := BaseDir1 + LList2[I];
    if FileSize(LFilename) <> FileSize(BaseDir2 + LList2[I]) then
    begin
      if InRange(FileSize(BaseDir2 + LList2[I]), Options.MinSize,
        Options.MaxSize) then
        continue;
    end
    else
    begin
     { SStream1 := TSharedMemoryStream.Create
        (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
        '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), LFilename);
      SStream2 := TSharedMemoryStream.Create
        (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
        '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)),
        BaseDir2 + LList2[I]);
      try
        B := SynCommons.CompareMem(SStream1.Memory, SStream2.Memory,
          SStream1.Size);
      finally
        SStream1.Free;
        SStream2.Free;
      end;      }
      if not B then
        if InRange(FileSize(BaseDir2 + LList2[I]), Options.MinSize,
          Options.MaxSize) then
          continue;
    end;
    LEntry.Op := TPatchOp.opMissing;
    LEntry.Filename := LList2[I];
    LEntry.Size := FileSize(BaseDir2 + LList2[I]);
    Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
    FStream := TFileStream.Create(BaseDir2 + LList2[I], fmShareDenyNone);
    try
      CopyStreamEx(FStream, Output, LEntry.Size);
    finally
      FStream.Free;
    end;
    Delete(LList2, I, 1);
  end;
  TempDir := IncludeTrailingPathDelimiter(GetCurrentDir) +
    LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
    '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF1));
  if not DirectoryExists(TempDir) then
    CreateDir(TempDir);
  SetLength(LList1, 0);
  CS := TCriticalSection.Create;
  SetLength(Tasks, Options.Threads);
  J := -1;
  K := 0;
  try
    for I := Low(Tasks) to High(Tasks) do
    begin
      Tasks[I] := TTask.Create(I);
      Tasks[I].Perform(
        procedure(X: IntPtr)
        var
          Y, Z: Integer;
          S1, S2: String;
          A: Boolean;
          //SS0, SS1, SS2: TSharedMemoryStream;
          Res: NativeUInt;
        begin
          Z := Length(LList2);
          Y := AtomicIncrement(J);
          while Y < Z do
          begin
            S1 := IncludeTrailingPathDelimiter(TempDir) + Y.ToHexString +
              XTOOL_MAPSUF3;
            if C then
              S2 := Input1
            else
              S2 := BaseDir1 + LList2[Y];
           { SS0 := TSharedMemoryStream.Create
              (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
              '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), S1);
            SS1 := TSharedMemoryStream.Create
              (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
              '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), S2);
            SS2 := TSharedMemoryStream.Create
              (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
              '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)),
              BaseDir2 + LList2[Y]);
            try
              SS0.Size := Max(SS1.Size, SS2.Size);
              A := xd3_encode(SS2.Memory, SS2.Size, SS1.Memory, SS1.Size,
                SS0.Memory, @Res, SS0.Size, Integer(XD3_NOCOMPRESS)) = 0;
              if A then
                SS0.Size := Res;
            finally
              SS0.Free;
              SS1.Free;
              SS2.Free;
            end;  }
            if not A then
              DeleteFile(S1);
            CS.Acquire;
            try
              Insert(S1, LList1, Length(LList1));
              Inc(K);
            finally
              CS.Release;
            end;
            Y := AtomicIncrement(J);
          end;
        end);
      Tasks[I].Start;
    end;
    I := 0;
    while I < Length(LList2) do
    begin
      while I >= K do
        Sleep(10);
      LFilename := IncludeTrailingPathDelimiter(TempDir) + I.ToHexString +
        XTOOL_MAPSUF3;
      if FileExists(LFilename) then
      begin
        LEntry.Op := TPatchOp.opDifferent;
        LEntry.Filename := LList2[I];
        LEntry.Size := FileSize(BaseDir2 + LList2[I]);
        Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
        I64 := FileSize(LFilename);
        Output.WriteBuffer(I64, I64.Size);
        FStream := TFileStream.Create(LFilename, fmShareDenyNone);
        try
          CopyStreamEx(FStream, Output, I64);
        finally
          FStream.Free;
        end;
        TFile.Delete(LFilename);
      end
      else
      begin
        LEntry.Op := TPatchOp.opMissing;
        LEntry.Filename := LList1[I];
        LEntry.Size := FileSize(BaseDir1 + LList1[I]);
        Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
        FStream := TFileStream.Create(BaseDir1 + LList1[I], fmShareDenyNone);
        try
          CopyStreamEx(FStream, Output, LEntry.Size);
        finally
          FStream.Free;
        end;
      end;
      Inc(I);
    end;
    WaitForAll(Tasks);
  finally
    for I := Low(Tasks) to High(Tasks) do
      Tasks[I].Free;
    CS.Free;
    if DirectoryExists(TempDir) then
      TDirectory.Delete(TempDir, True);
  end;
  FillChar(LEntry, SizeOf(TEntryStruct2), 0);
  LEntry.Op := TPatchOp.opNone;
  Output.WriteBuffer(LEntry, SizeOf(TEntryStruct2));
end;

procedure Decode(Input: TStream; Output: String; Options: TDecodeOptions);
var
  I64: Int64;
  B: Boolean;
  S1, S2: String;
  LFilename: String;
  BaseDir: String;
  LEntry: TEntryStruct2;
  FStream: TFileStream;
  //SStream0, SStream1, SStream2: TSharedMemoryStream;
  Res: NativeUInt;
begin
  if FileExists(Output) then
    BaseDir := ExtractFilePath(TPath.GetFullPath(Output))
  else if DirectoryExists(Output) then
    BaseDir := IncludeTrailingPathDelimiter(TPath.GetFullPath(Output))
  else
    BaseDir := ExtractFilePath(TPath.GetFullPath(Output));
  S1 := IncludeTrailingPathDelimiter(GetCurrentDir) +
    LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
    '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF3));
 { SStream0 := TSharedMemoryStream.Create
    (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
    '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), S1);
  try
    Input.ReadBuffer(LEntry, SizeOf(TEntryStruct2));
    while LEntry.Op <> TPatchOp.opNone do
    begin
      if FileExists(Output) then
        LFilename := Output
      else
        LFilename := BaseDir + LEntry.Filename;
      case LEntry.Op of
        TPatchOp.opDelete:
          TFile.Delete(LFilename);
        TPatchOp.opMissing:
          begin
            ForceDirectories(ExtractFilePath(LFilename));
            with TFileStream.Create(LFilename, fmCreate) do
              try
                CopyFrom(Input, LEntry.Size);
              finally
                Free;
              end;
          end;
        TPatchOp.opDifferent:
          begin
            Input.ReadBuffer(I64, I64.Size);
            SStream0.Position := 0;
            CopyStreamEx(Input, SStream0, I64);
            S2 := ChangeFileExt(LFilename, '_' + Random($7FFFFFFF).ToHexString +
              XTOOL_MAPSUF3);
            SStream1 := TSharedMemoryStream.Create
              (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
              '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), LFilename);
            SStream2 := TSharedMemoryStream.Create
              (LowerCase(ChangeFileExt(ExtractFileName(Utils.GetModuleName),
              '_' + Random($7FFFFFFF).ToHexString + XTOOL_MAPSUF2)), S2);
            B := False;
            try
              SStream2.Size := LEntry.Size;
              B := xd3_decode(SStream0.Memory, I64, SStream1.Memory,
                SStream1.Size, SStream2.Memory, @Res, SStream2.Size,
                Integer(XD3_NOCOMPRESS)) = 0;
            finally
              SStream1.Free;
              SStream2.Free;
            end;
            if B then
            begin
              TFile.Delete(LFilename);
              TFile.Move(S2, LFilename);
            end
            else if FileExists(S2) then
              TFile.Delete(S2);
          end;
      end;
      Input.ReadBuffer(LEntry, SizeOf(TEntryStruct2));
    end;
  finally
    SStream0.Free;
    if FileExists(S1) then
      TFile.Delete(S1);
  end;   }
end;

end.
