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.Int64Em, 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: Longint;
  21. FChunkBytesLeft, FChunkDecompressedBytesRead: Integer64;
  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: Longint): Longint;
  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. if NewFileExists(Result) then Exit;
  113. Path := SourceDir;
  114. LogFmt('Asking user for new disk containing "%s".', [F1]);
  115. if SelectDisk(Major, F1, Path) then begin
  116. LastSourceDir := Path;
  117. Result := AddBackslash(Path) + F1;
  118. end
  119. else
  120. Abort;
  121. end;
  122. procedure TFileExtractor.OpenSlice(const ASlice: Integer);
  123. var
  124. Filename: String;
  125. TestDiskSliceID: TDiskSliceID;
  126. DiskSliceHeader: TDiskSliceHeader;
  127. begin
  128. if FOpenedSlice = ASlice then
  129. Exit;
  130. FOpenedSlice := -1;
  131. FreeAndNil(FSourceF);
  132. if SetupLdrOffset1 = 0 then
  133. Filename := FindSliceFilename(ASlice)
  134. else
  135. Filename := SetupLdrOriginalFilename;
  136. FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
  137. if SetupLdrOffset1 = 0 then begin
  138. if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
  139. SourceIsCorrupted('Invalid slice header (1)');
  140. if TestDiskSliceID <> DiskSliceID then
  141. SourceIsCorrupted('Invalid slice header (2)');
  142. if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
  143. SourceIsCorrupted('Invalid slice header (3)');
  144. if FSourceF.Size <> DiskSliceHeader.TotalSize then
  145. SourceIsCorrupted('Invalid slice header (4)');
  146. end;
  147. FOpenedSlice := ASlice;
  148. end;
  149. procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
  150. begin
  151. try
  152. FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
  153. except
  154. { If DecompressInto raises an exception, force a decompressor reset &
  155. re-seek the next time SeekTo is called by setting FNeedReset to True.
  156. We don't want to get stuck in an endless loop with the decompressor
  157. in e.g. a data error state. Also, we have no way of knowing if
  158. DecompressInto successfully decompressed some of the requested bytes
  159. before the exception was raised. }
  160. FNeedReset := True;
  161. raise;
  162. end;
  163. Inc64(FChunkDecompressedBytesRead, Count);
  164. end;
  165. procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
  166. const ProgressProc: TExtractorProgressProc);
  167. procedure InitDecryption;
  168. begin
  169. { Recreate the unique nonce from the base nonce }
  170. var Nonce := SetupEncryptionHeader.BaseNonce;
  171. Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
  172. Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
  173. XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
  174. end;
  175. procedure Discard(Count: Integer64);
  176. var
  177. Buf: array[0..65535] of Byte;
  178. BufSize: Cardinal;
  179. begin
  180. try
  181. while True do begin
  182. BufSize := SizeOf(Buf);
  183. if (Count.Hi = 0) and (Count.Lo < BufSize) then
  184. BufSize := Count.Lo;
  185. if BufSize = 0 then
  186. Break;
  187. DecompressBytes(Buf, BufSize);
  188. Dec64(Count, BufSize);
  189. if Assigned(ProgressProc) then
  190. ProgressProc(0);
  191. end;
  192. except
  193. on E: ECompressDataError do
  194. SourceIsCorrupted(E.Message);
  195. end;
  196. end;
  197. var
  198. TestCompID: TCompID;
  199. Diff: Integer64;
  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. (Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) < 0) 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 := To64(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 Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) > 0 then begin
  235. Diff := FL.ChunkSuboffset;
  236. Dec6464(Diff, FChunkDecompressedBytesRead);
  237. Discard(Diff);
  238. end;
  239. finally
  240. Dec(FEntered);
  241. end;
  242. end;
  243. function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
  244. var
  245. Buffer: Pointer;
  246. Left, Res: Cardinal;
  247. begin
  248. Buffer := @Buf;
  249. Left := Count;
  250. if (FChunkBytesLeft.Hi = 0) and (FChunkBytesLeft.Lo < Left) then
  251. Left := FChunkBytesLeft.Lo;
  252. Result := Left;
  253. while Left <> 0 do begin
  254. Res := FSourceF.Read(Buffer^, Left);
  255. Dec64(FChunkBytesLeft, Res);
  256. { Decrypt the data after reading from the file }
  257. if FChunkEncrypted then
  258. XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
  259. if Left = Res then
  260. Break
  261. else begin
  262. Dec(Left, Res);
  263. Inc(Longint(Buffer), Res);
  264. { Go to next disk }
  265. if FOpenedSlice >= FChunkLastSlice then
  266. { Already on the last slice, so the file must be corrupted... }
  267. SourceIsCorrupted('Already on last slice');
  268. OpenSlice(FOpenedSlice + 1);
  269. end;
  270. end;
  271. end;
  272. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  273. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  274. const VerifyChecksum: Boolean);
  275. var
  276. BytesLeft: Integer64;
  277. Context: TSHA256Context;
  278. AddrOffset: LongWord;
  279. BufSize: Cardinal;
  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. BytesLeft := FL.OriginalSize;
  289. { To avoid file system fragmentation, preallocate all of the bytes in the
  290. destination file }
  291. DestF.Seek64(BytesLeft);
  292. DestF.Truncate;
  293. DestF.Seek(0);
  294. SHA256Init(Context);
  295. try
  296. AddrOffset := 0;
  297. while True do begin
  298. BufSize := SizeOf(Buf);
  299. if (BytesLeft.Hi = 0) and (BytesLeft.Lo < BufSize) then
  300. BufSize := BytesLeft.Lo;
  301. if BufSize = 0 then
  302. Break;
  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. Dec64(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.