318 lines
9.6 KiB
ObjectPascal
318 lines
9.6 KiB
ObjectPascal
unit Compiler.CompressionHandler;
|
|
|
|
{
|
|
Inno Setup
|
|
Copyright (C) 1997-2025 Jordan Russell
|
|
Portions by Martijn Laan
|
|
For conditions of distribution and use, see LICENSE.TXT.
|
|
|
|
Compression handler used by TSetupCompiler
|
|
}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes,
|
|
SHA256, ChaCha20, Shared.Struct, Shared.Int64Em, Shared.FileClass, Compression.Base,
|
|
Compiler.SetupCompiler;
|
|
|
|
type
|
|
TCompressionHandler = class
|
|
private
|
|
FCachedCompressors: TList;
|
|
FCompiler: TSetupCompiler;
|
|
FCompressor: TCustomCompressor;
|
|
FChunkBytesRead: Integer64;
|
|
FChunkBytesWritten: Integer64;
|
|
FChunkEncrypted: Boolean;
|
|
FChunkFirstSlice: Integer;
|
|
FChunkStarted: Boolean;
|
|
FChunkStartOffset: Longint;
|
|
FCryptContext: TChaCha20Context;
|
|
FCurSlice: Integer;
|
|
FDestFile: TFile;
|
|
FDestFileIsDiskSlice: Boolean;
|
|
FInitialBytesCompressedSoFar: Integer64;
|
|
FSliceBaseOffset: Cardinal;
|
|
FSliceBytesLeft: Cardinal;
|
|
procedure EndSlice;
|
|
procedure NewSlice(const Filename: String);
|
|
public
|
|
constructor Create(ACompiler: TSetupCompiler; const InitialSliceFilename: String);
|
|
destructor Destroy; override;
|
|
procedure CompressFile(const SourceFile: TFile; Bytes: Integer64;
|
|
const CallOptimize: Boolean; out SHA256Sum: TSHA256Digest);
|
|
procedure EndChunk;
|
|
procedure Finish;
|
|
procedure NewChunk(const ACompressorClass: TCustomCompressorClass;
|
|
const ACompressLevel: Integer; const ACompressorProps: TCompressorProps;
|
|
const AUseEncryption: Boolean; const ACryptKey: TSetupEncryptionKey);
|
|
procedure ProgressProc(BytesProcessed: Cardinal);
|
|
function ReserveBytesOnSlice(const Bytes: Cardinal): Boolean;
|
|
procedure WriteProc(const Buf; BufSize: Longint);
|
|
property ChunkBytesRead: Integer64 read FChunkBytesRead;
|
|
property ChunkBytesWritten: Integer64 read FChunkBytesWritten;
|
|
property ChunkEncrypted: Boolean read FChunkEncrypted;
|
|
property ChunkFirstSlice: Integer read FChunkFirstSlice;
|
|
property ChunkStartOffset: Longint read FChunkStartOffset;
|
|
property ChunkStarted: Boolean read FChunkStarted;
|
|
property CurSlice: Integer read FCurSlice;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
SysUtils, Compiler.Messages, Compiler.HelperFunc;
|
|
|
|
constructor TCompressionHandler.Create(ACompiler: TSetupCompiler;
|
|
const InitialSliceFilename: String);
|
|
begin
|
|
inherited Create;
|
|
FCompiler := ACompiler;
|
|
FCurSlice := -1;
|
|
FCachedCompressors := TList.Create;
|
|
NewSlice(InitialSliceFilename);
|
|
end;
|
|
|
|
destructor TCompressionHandler.Destroy;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
if Assigned(FCachedCompressors) then begin
|
|
for I := FCachedCompressors.Count-1 downto 0 do
|
|
TCustomCompressor(FCachedCompressors[I]).Free;
|
|
FreeAndNil(FCachedCompressors);
|
|
end;
|
|
FreeAndNil(FDestFile);
|
|
inherited;
|
|
end;
|
|
|
|
procedure TCompressionHandler.Finish;
|
|
begin
|
|
EndChunk;
|
|
EndSlice;
|
|
end;
|
|
|
|
procedure TCompressionHandler.EndSlice;
|
|
var
|
|
DiskSliceHeader: TDiskSliceHeader;
|
|
begin
|
|
if Assigned(FDestFile) then begin
|
|
if FDestFileIsDiskSlice then begin
|
|
DiskSliceHeader.TotalSize := FDestFile.Size.Lo;
|
|
FDestFile.Seek(SizeOf(DiskSliceID));
|
|
FDestFile.WriteBuffer(DiskSliceHeader, SizeOf(DiskSliceHeader));
|
|
end;
|
|
FreeAndNil(FDestFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TCompressionHandler.NewSlice(const Filename: String);
|
|
|
|
function GenerateSliceFilename(const Compiler: TSetupCompiler;
|
|
const ASlice: Integer): String;
|
|
begin
|
|
var SlicesPerDisk := Compiler.GetSlicesPerDisk;
|
|
var OutputBaseFilename := Compiler.GetOutputBaseFilename;
|
|
var Major := ASlice div SlicesPerDisk + 1;
|
|
var Minor := ASlice mod SlicesPerDisk;
|
|
if SlicesPerDisk = 1 then
|
|
Result := Format('%s-%d.bin', [OutputBaseFilename, Major])
|
|
else
|
|
Result := Format('%s-%d%s.bin', [OutputBaseFilename, Major,
|
|
Chr(Ord('a') + Minor)]);
|
|
end;
|
|
|
|
begin
|
|
var DiskSliceSize := FCompiler.GetDiskSliceSize;
|
|
EndSlice;
|
|
Inc(FCurSlice);
|
|
if (FCurSlice > 0) and not FCompiler.GetDiskSpanning then
|
|
FCompiler.AbortCompileFmt(SCompilerMustUseDiskSpanning,
|
|
[DiskSliceSize]);
|
|
if Filename = '' then begin
|
|
FDestFileIsDiskSlice := True;
|
|
FDestFile := TFile.Create(FCompiler.GetOutputDir +
|
|
GenerateSliceFilename(FCompiler, FCurSlice), fdCreateAlways, faReadWrite, fsNone);
|
|
FDestFile.WriteBuffer(DiskSliceID, SizeOf(DiskSliceID));
|
|
var DiskHeader: TDiskSliceHeader;
|
|
DiskHeader.TotalSize := 0;
|
|
FDestFile.WriteBuffer(DiskHeader, SizeOf(DiskHeader));
|
|
FSliceBaseOffset := 0;
|
|
FSliceBytesLeft := DiskSliceSize - (SizeOf(DiskSliceID) + SizeOf(DiskHeader));
|
|
end
|
|
else begin
|
|
FDestFileIsDiskSlice := False;
|
|
FDestFile := TFile.Create(Filename, fdOpenExisting, faReadWrite, fsNone);
|
|
FDestFile.SeekToEnd;
|
|
FSliceBaseOffset := FDestFile.Position.Lo;
|
|
FSliceBytesLeft := Cardinal(DiskSliceSize) - FSliceBaseOffset;
|
|
end;
|
|
end;
|
|
|
|
function TCompressionHandler.ReserveBytesOnSlice(const Bytes: Cardinal): Boolean;
|
|
begin
|
|
if FSliceBytesLeft >= Bytes then begin
|
|
Dec(FSliceBytesLeft, Bytes);
|
|
Result := True;
|
|
end
|
|
else
|
|
Result := False;
|
|
end;
|
|
|
|
procedure TCompressionHandler.NewChunk(const ACompressorClass: TCustomCompressorClass;
|
|
const ACompressLevel: Integer; const ACompressorProps: TCompressorProps;
|
|
const AUseEncryption: Boolean; const ACryptKey: TSetupEncryptionKey);
|
|
|
|
procedure SelectCompressor;
|
|
var
|
|
I: Integer;
|
|
C: TCustomCompressor;
|
|
begin
|
|
{ No current compressor, or changing compressor classes? }
|
|
if (FCompressor = nil) or (FCompressor.ClassType <> ACompressorClass) then begin
|
|
FCompressor := nil;
|
|
{ Search cache for requested class }
|
|
for I := FCachedCompressors.Count-1 downto 0 do begin
|
|
C := FCachedCompressors[I];
|
|
if C.ClassType = ACompressorClass then begin
|
|
FCompressor := C;
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
if FCompressor = nil then begin
|
|
FCachedCompressors.Expand;
|
|
FCompressor := ACompressorClass.Create(WriteProc, ProgressProc,
|
|
ACompressLevel, ACompressorProps);
|
|
FCachedCompressors.Add(FCompressor);
|
|
end;
|
|
end;
|
|
|
|
procedure InitEncryption;
|
|
begin
|
|
{ Create a unique nonce from the base nonce }
|
|
var Nonce := FCompiler.GetEncryptionBaseNonce;
|
|
Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
|
|
Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
|
|
|
|
XChaCha20Init(FCryptContext, ACryptKey[0], Length(ACryptKey), Nonce, SizeOf(Nonce), 0);
|
|
end;
|
|
|
|
var
|
|
MinBytesLeft: Cardinal;
|
|
begin
|
|
EndChunk;
|
|
|
|
{ If there isn't enough room left to start a new chunk on the current slice,
|
|
start a new slice }
|
|
MinBytesLeft := SizeOf(ZLIBID);
|
|
Inc(MinBytesLeft); { for at least one byte of data }
|
|
if FSliceBytesLeft < MinBytesLeft then
|
|
NewSlice('');
|
|
|
|
FChunkFirstSlice := FCurSlice;
|
|
FChunkStartOffset := FDestFile.Position.Lo - FSliceBaseOffset;
|
|
FDestFile.WriteBuffer(ZLIBID, SizeOf(ZLIBID));
|
|
Dec(FSliceBytesLeft, SizeOf(ZLIBID));
|
|
FChunkBytesRead := To64(0);
|
|
FChunkBytesWritten := To64(0);
|
|
FInitialBytesCompressedSoFar := FCompiler.GetBytesCompressedSoFar;
|
|
|
|
SelectCompressor;
|
|
|
|
FChunkEncrypted := AUseEncryption;
|
|
if AUseEncryption then
|
|
InitEncryption;
|
|
|
|
FChunkStarted := True;
|
|
end;
|
|
|
|
procedure TCompressionHandler.EndChunk;
|
|
begin
|
|
if not FChunkStarted then
|
|
Exit;
|
|
FChunkStarted := False;
|
|
|
|
FCompressor.Finish;
|
|
{ In case we didn't get a ProgressProc call after the final block: }
|
|
FCompiler.SetBytesCompressedSoFar(FInitialBytesCompressedSoFar);
|
|
FCompiler.AddBytesCompressedSoFar(FChunkBytesRead);
|
|
FCompiler.CallIdleProc;
|
|
end;
|
|
|
|
procedure TCompressionHandler.CompressFile(const SourceFile: TFile;
|
|
Bytes: Integer64; const CallOptimize: Boolean; out SHA256Sum: TSHA256Digest);
|
|
var
|
|
Context: TSHA256Context;
|
|
AddrOffset: LongWord;
|
|
BufSize: Cardinal;
|
|
Buf: array[0..65535] of Byte;
|
|
{ ^ *must* be the same buffer size used in Setup (TFileExtractor), otherwise
|
|
the TransformCallInstructions call will break }
|
|
begin
|
|
SHA256Init(Context);
|
|
AddrOffset := 0;
|
|
while True do begin
|
|
BufSize := SizeOf(Buf);
|
|
if (Bytes.Hi = 0) and (Bytes.Lo < BufSize) then
|
|
BufSize := Bytes.Lo;
|
|
if BufSize = 0 then
|
|
Break;
|
|
|
|
SourceFile.ReadBuffer(Buf, BufSize);
|
|
Inc64(FChunkBytesRead, BufSize);
|
|
Dec64(Bytes, BufSize);
|
|
SHA256Update(Context, Buf, BufSize);
|
|
if CallOptimize then begin
|
|
TransformCallInstructions(Buf, BufSize, True, AddrOffset);
|
|
Inc(AddrOffset, BufSize); { may wrap, but OK }
|
|
end;
|
|
FCompressor.Compress(Buf, BufSize);
|
|
end;
|
|
SHA256Sum := SHA256Final(Context);
|
|
end;
|
|
|
|
procedure TCompressionHandler.WriteProc(const Buf; BufSize: Longint);
|
|
var
|
|
P, P2: Pointer;
|
|
S: Cardinal;
|
|
begin
|
|
FCompiler.CallIdleProc;
|
|
P := @Buf;
|
|
while BufSize > 0 do begin
|
|
S := BufSize;
|
|
if FSliceBytesLeft = 0 then
|
|
NewSlice('');
|
|
if S > Cardinal(FSliceBytesLeft) then
|
|
S := FSliceBytesLeft;
|
|
|
|
if not FChunkEncrypted then
|
|
FDestFile.WriteBuffer(P^, S)
|
|
else begin
|
|
{ Using encryption. Can't modify Buf in place so allocate a new,
|
|
temporary buffer. }
|
|
GetMem(P2, S);
|
|
try
|
|
XChaCha20Crypt(FCryptContext, P^, P2^, S);
|
|
FDestFile.WriteBuffer(P2^, S)
|
|
finally
|
|
FreeMem(P2);
|
|
end;
|
|
end;
|
|
|
|
Inc64(FChunkBytesWritten, S);
|
|
Inc(Cardinal(P), S);
|
|
Dec(BufSize, S);
|
|
Dec(FSliceBytesLeft, S);
|
|
end;
|
|
end;
|
|
|
|
procedure TCompressionHandler.ProgressProc(BytesProcessed: Cardinal);
|
|
begin
|
|
FCompiler.AddBytesCompressedSoFar(BytesProcessed);
|
|
FCompiler.CallIdleProc;
|
|
end;
|
|
|
|
end.
|