Setup.FileExtractor.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. unit Setup.FileExtractor;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. TFileExtractor class to extract (=decrypt, decompress, and/or verify) Setup files
  8. }
  9. interface
  10. uses
  11. Windows, SysUtils, Shared.FileClass, Compression.Base,
  12. Shared.Struct, ChaCha20;
  13. type
  14. TExtractorProgressProc = procedure(const Bytes: Cardinal);
  15. TFileExtractor = class
  16. private
  17. FDecompressor: array[Boolean] of TCustomDecompressor;
  18. FSourceF: TFile;
  19. FOpenedSlice, FChunkFirstSlice, FChunkLastSlice: Integer;
  20. FChunkStartOffset: Int64;
  21. FChunkBytesLeft, FChunkDecompressedBytesRead: Int64;
  22. FNeedReset: Boolean;
  23. FChunkCompressed, FChunkEncrypted: Boolean;
  24. FCryptContext: TChaCha20Context;
  25. FCryptKey: TSetupEncryptionKey;
  26. FCryptKeySet: Boolean;
  27. FEntered: Integer;
  28. procedure DecompressBytes(var Buffer; Count: Cardinal);
  29. class function FindSliceFilename(const ASlice: Integer): String;
  30. procedure OpenSlice(const ASlice: Integer);
  31. function ReadProc(var Buf; Count: Cardinal): Cardinal;
  32. procedure SetCryptKey(const Value: TSetupEncryptionKey);
  33. public
  34. constructor Create(ADecompressorClass: TCustomDecompressorClass);
  35. destructor Destroy; override;
  36. procedure DecompressFile(const FL: TSetupFileLocationEntry; const DestF: TFile;
  37. const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
  38. procedure SeekTo(const FL: TSetupFileLocationEntry;
  39. const ProgressProc: TExtractorProgressProc);
  40. property CryptKey: TSetupEncryptionKey write SetCryptKey;
  41. end;
  42. function FileExtractor: TFileExtractor;
  43. procedure FreeFileExtractor;
  44. implementation
  45. uses
  46. PathFunc, Shared.CommonFunc, Setup.MainFunc, SetupLdrAndSetup.Messages,
  47. Shared.SetupMessageIDs, Setup.InstFunc, Compression.Zlib, Compression.bzlib,
  48. Compression.LZMADecompressor, SHA256, Setup.LoggingFunc, Setup.NewDiskForm;
  49. var
  50. FFileExtractor: TFileExtractor;
  51. function FileExtractor: TFileExtractor;
  52. const
  53. DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
  54. (TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
  55. TLZMA2Decompressor);
  56. begin
  57. if FFileExtractor = nil then
  58. FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
  59. Result := FFileExtractor;
  60. end;
  61. procedure FreeFileExtractor;
  62. begin
  63. FreeAndNil(FFileExtractor);
  64. end;
  65. procedure SourceIsCorrupted(const AReason: String);
  66. begin
  67. Log('Source file corrupted: ' + AddPeriod(AReason));
  68. raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
  69. end;
  70. { TFileExtractor }
  71. constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
  72. begin
  73. inherited Create;
  74. FOpenedSlice := -1;
  75. FChunkFirstSlice := -1;
  76. { Create one 'decompressor' for use with uncompressed chunks, and another
  77. for use with compressed chunks }
  78. FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
  79. FDecompressor[True] := ADecompressorClass.Create(ReadProc);
  80. end;
  81. destructor TFileExtractor.Destroy;
  82. begin
  83. FSourceF.Free;
  84. FDecompressor[True].Free;
  85. FDecompressor[False].Free;
  86. inherited;
  87. end;
  88. procedure TFileExtractor.SetCryptKey(const Value: TSetupEncryptionKey);
  89. begin
  90. FCryptKey := Value;
  91. FCryptKeySet := True;
  92. end;
  93. var
  94. LastSourceDir: String;
  95. class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
  96. var
  97. Major, Minor: Integer;
  98. Prefix, F1, Path: String;
  99. begin
  100. Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
  101. Major := ASlice div SetupHeader.SlicesPerDisk + 1;
  102. Minor := ASlice mod SetupHeader.SlicesPerDisk;
  103. if SetupHeader.SlicesPerDisk = 1 then
  104. F1 := Format('%s-%d.bin', [Prefix, Major])
  105. else
  106. F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
  107. if LastSourceDir <> '' then begin
  108. Result := AddBackslash(LastSourceDir) + F1;
  109. if NewFileExists(Result) then Exit;
  110. end;
  111. Result := AddBackslash(SourceDir) + F1;
  112. {$IFDEF DEBUG}
  113. { Also see Setup.MainFunc's InitializeSetup }
  114. Result := Result.Replace('SetupCustomStyle', 'Setup');
  115. {$ENDIF}
  116. if NewFileExists(Result) then Exit;
  117. Path := SourceDir;
  118. LogFmt('Asking user for new disk containing "%s".', [F1]);
  119. if SelectDisk(Major, F1, Path) then begin
  120. LastSourceDir := Path;
  121. Result := AddBackslash(Path) + F1;
  122. end
  123. else
  124. Abort;
  125. end;
  126. procedure TFileExtractor.OpenSlice(const ASlice: Integer);
  127. var
  128. Filename: String;
  129. TestDiskSliceID: TDiskSliceID;
  130. DiskSliceHeader: TDiskSliceHeader;
  131. begin
  132. if FOpenedSlice = ASlice then
  133. Exit;
  134. FOpenedSlice := -1;
  135. FreeAndNil(FSourceF);
  136. if SetupLdrOffset1 = 0 then
  137. Filename := FindSliceFilename(ASlice)
  138. else
  139. Filename := SetupLdrOriginalFilename;
  140. FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
  141. if SetupLdrOffset1 = 0 then begin
  142. if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
  143. SourceIsCorrupted('Invalid slice header (1)');
  144. if TestDiskSliceID <> DiskSliceID then
  145. SourceIsCorrupted('Invalid slice header (2)');
  146. if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
  147. SourceIsCorrupted('Invalid slice header (3)');
  148. if FSourceF.Size <> DiskSliceHeader.TotalSize then
  149. SourceIsCorrupted('Invalid slice header (4)');
  150. end;
  151. FOpenedSlice := ASlice;
  152. end;
  153. procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
  154. begin
  155. try
  156. FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
  157. except
  158. { If DecompressInto raises an exception, force a decompressor reset &
  159. re-seek the next time SeekTo is called by setting FNeedReset to True.
  160. We don't want to get stuck in an endless loop with the decompressor
  161. in e.g. a data error state. Also, we have no way of knowing if
  162. DecompressInto successfully decompressed some of the requested bytes
  163. before the exception was raised. }
  164. FNeedReset := True;
  165. raise;
  166. end;
  167. Inc(FChunkDecompressedBytesRead, Count);
  168. end;
  169. procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
  170. const ProgressProc: TExtractorProgressProc);
  171. procedure InitDecryption;
  172. begin
  173. { Recreate the unique nonce from the base nonce }
  174. var Nonce := SetupEncryptionHeader.BaseNonce;
  175. Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
  176. Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
  177. XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
  178. end;
  179. procedure Discard(Count: Int64);
  180. var
  181. Buf: array[0..65535] of Byte;
  182. begin
  183. try
  184. while Count > 0 do begin
  185. var BufSize: Cardinal := SizeOf(Buf);
  186. if Count < BufSize then
  187. BufSize := Cardinal(Count);
  188. DecompressBytes(Buf, BufSize);
  189. Dec(Count, BufSize);
  190. if Assigned(ProgressProc) then
  191. ProgressProc(0);
  192. end;
  193. except
  194. on E: ECompressDataError do
  195. SourceIsCorrupted(E.Message);
  196. end;
  197. end;
  198. var
  199. TestCompID: TCompID;
  200. begin
  201. if FEntered <> 0 then
  202. InternalError('Cannot call file extractor recursively');
  203. Inc(FEntered);
  204. try
  205. if (floChunkEncrypted in FL.Flags) and not FCryptKeySet then
  206. InternalError('Cannot read an encrypted file before the key has been set');
  207. { Is the file in a different chunk than the current one?
  208. Or, is the file in a part of the current chunk that we've already passed?
  209. Or, did a previous decompression operation fail, necessitating a reset? }
  210. if (FChunkFirstSlice <> FL.FirstSlice) or
  211. (FChunkStartOffset <> FL.StartOffset) or
  212. (FL.ChunkSuboffset < FChunkDecompressedBytesRead) or
  213. FNeedReset then begin
  214. FChunkFirstSlice := -1;
  215. FDecompressor[floChunkCompressed in FL.Flags].Reset;
  216. FNeedReset := False;
  217. OpenSlice(FL.FirstSlice);
  218. FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
  219. if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
  220. SourceIsCorrupted('Failed to read CompID');
  221. if Longint(TestCompID) <> Longint(ZLIBID) then
  222. SourceIsCorrupted('Invalid CompID');
  223. FChunkFirstSlice := FL.FirstSlice;
  224. FChunkLastSlice := FL.LastSlice;
  225. FChunkStartOffset := FL.StartOffset;
  226. FChunkBytesLeft := FL.ChunkCompressedSize;
  227. FChunkDecompressedBytesRead := 0;
  228. FChunkCompressed := floChunkCompressed in FL.Flags;
  229. FChunkEncrypted := floChunkEncrypted in FL.Flags;
  230. if floChunkEncrypted in FL.Flags then
  231. InitDecryption;
  232. end;
  233. { Need to seek forward in the chunk? }
  234. if FL.ChunkSuboffset > FChunkDecompressedBytesRead then begin
  235. var Diff := FL.ChunkSuboffset;
  236. Dec(Diff, FChunkDecompressedBytesRead);
  237. Discard(Diff);
  238. end;
  239. finally
  240. Dec(FEntered);
  241. end;
  242. end;
  243. function TFileExtractor.ReadProc(var Buf; Count: Cardinal): Cardinal;
  244. var
  245. Buffer: Pointer;
  246. Left, Res: Cardinal;
  247. begin
  248. if FChunkBytesLeft < 0 then { sanity checks }
  249. InternalError('TFileExtractor.ReadProc: Negative FChunkBytesLeft');
  250. Buffer := @Buf;
  251. Left := Count;
  252. if FChunkBytesLeft < Left then
  253. Left := Cardinal(FChunkBytesLeft);
  254. Result := Left;
  255. while Left <> 0 do begin
  256. Res := FSourceF.Read(Buffer^, Left);
  257. Dec(FChunkBytesLeft, Res);
  258. { Decrypt the data after reading from the file }
  259. if FChunkEncrypted then
  260. XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
  261. if Left = Res then
  262. Break
  263. else begin
  264. Dec(Left, Res);
  265. Inc(PByte(Buffer), Res);
  266. { Go to next disk }
  267. if FOpenedSlice >= FChunkLastSlice then
  268. { Already on the last slice, so the file must be corrupted... }
  269. SourceIsCorrupted('Already on last slice');
  270. OpenSlice(FOpenedSlice + 1);
  271. end;
  272. end;
  273. end;
  274. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  275. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  276. const VerifyChecksum: Boolean);
  277. var
  278. Context: TSHA256Context;
  279. AddrOffset: UInt32;
  280. Buf: array[0..65535] of Byte;
  281. { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
  282. otherwise the TransformCallInstructions call will break }
  283. begin
  284. if FEntered <> 0 then
  285. InternalError('Cannot call file extractor recursively');
  286. Inc(FEntered);
  287. try
  288. var BytesLeft := FL.OriginalSize;
  289. if BytesLeft < 0 then { sanity check }
  290. InternalError('TFileExtractor.DecompressFile: Negative size');
  291. { To avoid file system fragmentation, preallocate all of the bytes in the
  292. destination file }
  293. DestF.Seek(BytesLeft);
  294. DestF.Truncate;
  295. DestF.Seek(0);
  296. SHA256Init(Context);
  297. try
  298. AddrOffset := 0;
  299. while BytesLeft > 0 do begin
  300. var BufSize: Cardinal := SizeOf(Buf);
  301. if BytesLeft < BufSize then
  302. BufSize := Cardinal(BytesLeft);
  303. DecompressBytes(Buf, BufSize);
  304. if floCallInstructionOptimized in FL.Flags then begin
  305. TransformCallInstructions(Buf, BufSize, False, AddrOffset);
  306. Inc(AddrOffset, BufSize); { may wrap, but OK }
  307. end;
  308. Dec(BytesLeft, BufSize);
  309. SHA256Update(Context, Buf, BufSize);
  310. DestF.WriteBuffer(Buf, BufSize);
  311. if Assigned(ProgressProc) then
  312. ProgressProc(BufSize);
  313. end;
  314. except
  315. on E: ECompressDataError do
  316. SourceIsCorrupted(E.Message);
  317. end;
  318. if VerifyChecksum and not SHA256DigestsEqual(SHA256Final(Context), FL.SHA256Sum) then
  319. SourceIsCorrupted('SHA-256 hash mismatch');
  320. finally
  321. Dec(FEntered);
  322. end;
  323. end;
  324. end.