123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- unit Setup.FileExtractor;
- {
- Inno Setup
- Copyright (C) 1997-2025 Jordan Russell
- Portions by Martijn Laan
- For conditions of distribution and use, see LICENSE.TXT.
- TFileExtractor class to extract (=decrypt, decompress, and/or verify) Setup files
- }
- interface
- uses
- Windows, SysUtils, Shared.Int64Em, Shared.FileClass, Compression.Base,
- Shared.Struct, ChaCha20;
- type
- TExtractorProgressProc = procedure(const Bytes: Cardinal);
- TFileExtractor = class
- private
- FDecompressor: array[Boolean] of TCustomDecompressor;
- FSourceF: TFile;
- FOpenedSlice, FChunkFirstSlice, FChunkLastSlice: Integer;
- FChunkStartOffset: Longint;
- FChunkBytesLeft, FChunkDecompressedBytesRead: Integer64;
- FNeedReset: Boolean;
- FChunkCompressed, FChunkEncrypted: Boolean;
- FCryptContext: TChaCha20Context;
- FCryptKey: TSetupEncryptionKey;
- FCryptKeySet: Boolean;
- FEntered: Integer;
- procedure DecompressBytes(var Buffer; Count: Cardinal);
- class function FindSliceFilename(const ASlice: Integer): String;
- procedure OpenSlice(const ASlice: Integer);
- function ReadProc(var Buf; Count: Longint): Longint;
- procedure SetCryptKey(const Value: TSetupEncryptionKey);
- public
- constructor Create(ADecompressorClass: TCustomDecompressorClass);
- destructor Destroy; override;
- procedure DecompressFile(const FL: TSetupFileLocationEntry; const DestF: TFile;
- const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
- procedure SeekTo(const FL: TSetupFileLocationEntry;
- const ProgressProc: TExtractorProgressProc);
- property CryptKey: TSetupEncryptionKey write SetCryptKey;
- end;
- function FileExtractor: TFileExtractor;
- procedure FreeFileExtractor;
- implementation
- uses
- PathFunc, Shared.CommonFunc, Setup.MainFunc, SetupLdrAndSetup.Messages,
- Shared.SetupMessageIDs, Setup.InstFunc, Compression.Zlib, Compression.bzlib,
- Compression.LZMADecompressor, SHA256, Setup.LoggingFunc, Setup.NewDiskForm;
- var
- FFileExtractor: TFileExtractor;
- function FileExtractor: TFileExtractor;
- const
- DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
- (TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
- TLZMA2Decompressor);
- begin
- if FFileExtractor = nil then
- FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
- Result := FFileExtractor;
- end;
- procedure FreeFileExtractor;
- begin
- FreeAndNil(FFileExtractor);
- end;
- procedure SourceIsCorrupted(const AReason: String);
- begin
- Log('Source file corrupted: ' + AddPeriod(AReason));
- raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
- end;
- { TFileExtractor }
- constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
- begin
- inherited Create;
- FOpenedSlice := -1;
- FChunkFirstSlice := -1;
- { Create one 'decompressor' for use with uncompressed chunks, and another
- for use with compressed chunks }
- FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
- FDecompressor[True] := ADecompressorClass.Create(ReadProc);
- end;
- destructor TFileExtractor.Destroy;
- begin
- FSourceF.Free;
- FDecompressor[True].Free;
- FDecompressor[False].Free;
- inherited;
- end;
- procedure TFileExtractor.SetCryptKey(const Value: TSetupEncryptionKey);
- begin
- FCryptKey := Value;
- FCryptKeySet := True;
- end;
- var
- LastSourceDir: String;
- class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
- var
- Major, Minor: Integer;
- Prefix, F1, Path: String;
- begin
- Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
- Major := ASlice div SetupHeader.SlicesPerDisk + 1;
- Minor := ASlice mod SetupHeader.SlicesPerDisk;
- if SetupHeader.SlicesPerDisk = 1 then
- F1 := Format('%s-%d.bin', [Prefix, Major])
- else
- F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
- if LastSourceDir <> '' then begin
- Result := AddBackslash(LastSourceDir) + F1;
- if NewFileExists(Result) then Exit;
- end;
- Result := AddBackslash(SourceDir) + F1;
- if NewFileExists(Result) then Exit;
- Path := SourceDir;
- LogFmt('Asking user for new disk containing "%s".', [F1]);
- if SelectDisk(Major, F1, Path) then begin
- LastSourceDir := Path;
- Result := AddBackslash(Path) + F1;
- end
- else
- Abort;
- end;
- procedure TFileExtractor.OpenSlice(const ASlice: Integer);
- var
- Filename: String;
- TestDiskSliceID: TDiskSliceID;
- DiskSliceHeader: TDiskSliceHeader;
- begin
- if FOpenedSlice = ASlice then
- Exit;
- FOpenedSlice := -1;
- FreeAndNil(FSourceF);
- if SetupLdrOffset1 = 0 then
- Filename := FindSliceFilename(ASlice)
- else
- Filename := SetupLdrOriginalFilename;
- FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
- if SetupLdrOffset1 = 0 then begin
- if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
- SourceIsCorrupted('Invalid slice header (1)');
- if TestDiskSliceID <> DiskSliceID then
- SourceIsCorrupted('Invalid slice header (2)');
- if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
- SourceIsCorrupted('Invalid slice header (3)');
- if FSourceF.Size <> DiskSliceHeader.TotalSize then
- SourceIsCorrupted('Invalid slice header (4)');
- end;
- FOpenedSlice := ASlice;
- end;
- procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
- begin
- try
- FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
- except
- { If DecompressInto raises an exception, force a decompressor reset &
- re-seek the next time SeekTo is called by setting FNeedReset to True.
- We don't want to get stuck in an endless loop with the decompressor
- in e.g. a data error state. Also, we have no way of knowing if
- DecompressInto successfully decompressed some of the requested bytes
- before the exception was raised. }
- FNeedReset := True;
- raise;
- end;
- Inc64(FChunkDecompressedBytesRead, Count);
- end;
- procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
- const ProgressProc: TExtractorProgressProc);
- procedure InitDecryption;
- begin
- { Recreate the unique nonce from the base nonce }
- var Nonce := SetupEncryptionHeader.BaseNonce;
- Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
- Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
- XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
- end;
- procedure Discard(Count: Integer64);
- var
- Buf: array[0..65535] of Byte;
- BufSize: Cardinal;
- begin
- try
- while True do begin
- BufSize := SizeOf(Buf);
- if (Count.Hi = 0) and (Count.Lo < BufSize) then
- BufSize := Count.Lo;
- if BufSize = 0 then
- Break;
- DecompressBytes(Buf, BufSize);
- Dec64(Count, BufSize);
- if Assigned(ProgressProc) then
- ProgressProc(0);
- end;
- except
- on E: ECompressDataError do
- SourceIsCorrupted(E.Message);
- end;
- end;
- var
- TestCompID: TCompID;
- Diff: Integer64;
- begin
- if FEntered <> 0 then
- InternalError('Cannot call file extractor recursively');
- Inc(FEntered);
- try
- if (floChunkEncrypted in FL.Flags) and not FCryptKeySet then
- InternalError('Cannot read an encrypted file before the key has been set');
- { Is the file in a different chunk than the current one?
- Or, is the file in a part of the current chunk that we've already passed?
- Or, did a previous decompression operation fail, necessitating a reset? }
- if (FChunkFirstSlice <> FL.FirstSlice) or
- (FChunkStartOffset <> FL.StartOffset) or
- (Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) < 0) or
- FNeedReset then begin
- FChunkFirstSlice := -1;
- FDecompressor[floChunkCompressed in FL.Flags].Reset;
- FNeedReset := False;
- OpenSlice(FL.FirstSlice);
- FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
- if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
- SourceIsCorrupted('Failed to read CompID');
- if Longint(TestCompID) <> Longint(ZLIBID) then
- SourceIsCorrupted('Invalid CompID');
- FChunkFirstSlice := FL.FirstSlice;
- FChunkLastSlice := FL.LastSlice;
- FChunkStartOffset := FL.StartOffset;
- FChunkBytesLeft := FL.ChunkCompressedSize;
- FChunkDecompressedBytesRead := To64(0);
- FChunkCompressed := floChunkCompressed in FL.Flags;
- FChunkEncrypted := floChunkEncrypted in FL.Flags;
- if floChunkEncrypted in FL.Flags then
- InitDecryption;
- end;
- { Need to seek forward in the chunk? }
- if Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) > 0 then begin
- Diff := FL.ChunkSuboffset;
- Dec6464(Diff, FChunkDecompressedBytesRead);
- Discard(Diff);
- end;
- finally
- Dec(FEntered);
- end;
- end;
- function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
- var
- Buffer: Pointer;
- Left, Res: Cardinal;
- begin
- Buffer := @Buf;
- Left := Count;
- if (FChunkBytesLeft.Hi = 0) and (FChunkBytesLeft.Lo < Left) then
- Left := FChunkBytesLeft.Lo;
- Result := Left;
- while Left <> 0 do begin
- Res := FSourceF.Read(Buffer^, Left);
- Dec64(FChunkBytesLeft, Res);
- { Decrypt the data after reading from the file }
- if FChunkEncrypted then
- XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
- if Left = Res then
- Break
- else begin
- Dec(Left, Res);
- Inc(Longint(Buffer), Res);
- { Go to next disk }
- if FOpenedSlice >= FChunkLastSlice then
- { Already on the last slice, so the file must be corrupted... }
- SourceIsCorrupted('Already on last slice');
- OpenSlice(FOpenedSlice + 1);
- end;
- end;
- end;
- procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
- const DestF: TFile; const ProgressProc: TExtractorProgressProc;
- const VerifyChecksum: Boolean);
- var
- BytesLeft: Integer64;
- Context: TSHA256Context;
- AddrOffset: LongWord;
- BufSize: Cardinal;
- Buf: array[0..65535] of Byte;
- { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
- otherwise the TransformCallInstructions call will break }
- begin
- if FEntered <> 0 then
- InternalError('Cannot call file extractor recursively');
- Inc(FEntered);
- try
- BytesLeft := FL.OriginalSize;
- { To avoid file system fragmentation, preallocate all of the bytes in the
- destination file }
- DestF.Seek64(BytesLeft);
- DestF.Truncate;
- DestF.Seek(0);
- SHA256Init(Context);
- try
- AddrOffset := 0;
- while True do begin
- BufSize := SizeOf(Buf);
- if (BytesLeft.Hi = 0) and (BytesLeft.Lo < BufSize) then
- BufSize := BytesLeft.Lo;
- if BufSize = 0 then
- Break;
- DecompressBytes(Buf, BufSize);
- if floCallInstructionOptimized in FL.Flags then begin
- TransformCallInstructions(Buf, BufSize, False, AddrOffset);
- Inc(AddrOffset, BufSize); { may wrap, but OK }
- end;
- Dec64(BytesLeft, BufSize);
- SHA256Update(Context, Buf, BufSize);
- DestF.WriteBuffer(Buf, BufSize);
- if Assigned(ProgressProc) then
- ProgressProc(BufSize);
- end;
- except
- on E: ECompressDataError do
- SourceIsCorrupted(E.Message);
- end;
- if VerifyChecksum and not SHA256DigestsEqual(SHA256Final(Context), FL.SHA256Sum) then
- SourceIsCorrupted('SHA-256 hash mismatch');
- finally
- Dec(FEntered);
- end;
- end;
- end.
|