Inno-Setup-issrc/Projects/Src/Compiler.CompressionHandler.pas
Martijn Laan 487b78d6dc
Cleanup.
2025-06-11 20:46:41 +02:00

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.