unit SFX;

interface

uses
  Winapi.Windows;

function ProcessArchive(const ExtPath: string): Boolean;

implementation

uses
  System.Classes,
  System.SysUtils,
  System.Zip;

var
  StartOfFile: Int64 = -1;

function FindFirstHeader(s: TStream): BOOLEAN;
var
  DOSHeader: TImageDosHeader;
  PEHeader: TImageFileHeader;
  OptHeader: TImageOptionalHeader;
  magic: DWord;
  n: Integer;
  HEADER_SIGNATURE: DWord;
begin
  // We store the local header signature in the file one greater than
  // the real value, so we don't falsely detect a zip file entry inside
  // the SFX code of the .EXE file

  Result := False;

  HEADER_SIGNATURE:=$04034B51; // intentionally 1 higher than the real sig
  dec(HEADER_SIGNATURE);       // fix the sig in RAM only

  s.Seek(0, TSeekOrigin.soBeginning);
  s.Read(DOSHeader, sizeof(DOSHeader));
  s.Seek(DOSHeader._lfanew, TSeekOrigin.soBeginning);

  s.Read(magic, 4);
  if magic <> $4550 then Exit;

  s.Read(PEHeader, sizeof(PEHeader));
  s.Read(OptHeader, sizeof(OptHeader));

  n := OptHeader.FileAlignment;

  StartOfFile := -1;

  // Scan file by alignment chunks for PK header
  while n < s.Size do
  begin
    s.Seek(n, TSeekOrigin.soBeginning);
    s.Read(magic, 4);
    if magic = HEADER_SIGNATURE then
    begin
      StartOfFile := n;
      Exit(True);
    end;
    Inc(n, OptHeader.FileAlignment);
  end;

  Result := StartOfFile >= 0;
end;

//
// This is not good for very large archives. We are assuming the archive
// is small enough to load into memory which should be fine on all modern PCs
// for Keyman and Keyman Developer. If files get bigger than a couple
// of hundred megs, then we should consider saving to temp disk file instead.
//
// All this is due to RSP-17889. If this gets fixed, we can eliminate the
// copy to TMemoryStream - and probably even the FindFirstHeader call
//
// 2020-07-07: RSP-17889 checked against Delphi 10.4 Sydney and not yet fixed.
// https://quality.embarcadero.com/browse/RSP-17889

function ProcessArchive(const ExtPath: string): Boolean;
var
  fs: TFileStream;
  ms: TMemoryStream;
begin
  try
    fs := TFileStream.Create(ParamStr(0), fmOpenRead or fmShareDenyWrite);
    ms := TMemoryStream.Create;
    try
      if not FindFirstHeader(fs) then
        Exit(False);

      fs.Seek(StartOfFile, TSeekOrigin.soBeginning);

      ms.CopyFrom(fs, fs.Size - StartOfFile);
      ms.Position := 0;

      with TZipFile.Create do
      try
        Open(ms, zmRead);
        ExtractAll(ExtPath);
      finally
        Free;
      end;
    finally
      fs.Free;
      ms.Free;
    end;
  except
    on E:Exception do
    begin
      raise Exception.Create(
        'Failed to extract setup archive. '+
        'You may have run out of disk space or there may be a '+
        'problem with the source files.'#13#10#13#10+
        'The error received was: '+E.Message);
    end;
  end;
  Result := True;
end;

end.
