Setup.FileExtractor.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. unit Setup.FileExtractor;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2026 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; static;
  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. begin
  199. if FEntered <> 0 then
  200. InternalError('Cannot call file extractor recursively');
  201. Inc(FEntered);
  202. try
  203. if (floChunkEncrypted in FL.Flags) and not FCryptKeySet then
  204. InternalError('Cannot read an encrypted file before the key has been set');
  205. { Is the file in a different chunk than the current one?
  206. Or, is the file in a part of the current chunk that we've already passed?
  207. Or, did a previous decompression operation fail, necessitating a reset? }
  208. if (FChunkFirstSlice <> FL.FirstSlice) or
  209. (FChunkStartOffset <> FL.StartOffset) or
  210. (FL.ChunkSuboffset < FChunkDecompressedBytesRead) or
  211. FNeedReset then begin
  212. FChunkFirstSlice := -1;
  213. FDecompressor[floChunkCompressed in FL.Flags].Reset;
  214. FNeedReset := False;
  215. OpenSlice(FL.FirstSlice);
  216. FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
  217. var TestCompID: TCompID;
  218. if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
  219. SourceIsCorrupted('Failed to read CompID');
  220. if TestCompID <> ZLIBID then
  221. SourceIsCorrupted('Invalid CompID');
  222. FChunkFirstSlice := FL.FirstSlice;
  223. FChunkLastSlice := FL.LastSlice;
  224. FChunkStartOffset := FL.StartOffset;
  225. FChunkBytesLeft := FL.ChunkCompressedSize;
  226. FChunkDecompressedBytesRead := 0;
  227. FChunkCompressed := floChunkCompressed in FL.Flags;
  228. FChunkEncrypted := floChunkEncrypted in FL.Flags;
  229. if floChunkEncrypted in FL.Flags then
  230. InitDecryption;
  231. end;
  232. { Need to seek forward in the chunk? }
  233. if FL.ChunkSuboffset > FChunkDecompressedBytesRead then begin
  234. var Diff := FL.ChunkSuboffset;
  235. Dec(Diff, FChunkDecompressedBytesRead);
  236. Discard(Diff);
  237. end;
  238. finally
  239. Dec(FEntered);
  240. end;
  241. end;
  242. function TFileExtractor.ReadProc(var Buf; Count: Cardinal): Cardinal;
  243. var
  244. Buffer: Pointer;
  245. Left, Res: Cardinal;
  246. begin
  247. if FChunkBytesLeft < 0 then { sanity checks }
  248. InternalError('TFileExtractor.ReadProc: Negative FChunkBytesLeft');
  249. Buffer := @Buf;
  250. Left := Count;
  251. if FChunkBytesLeft < Left then
  252. Left := Cardinal(FChunkBytesLeft);
  253. Result := Left;
  254. while Left <> 0 do begin
  255. Res := FSourceF.Read(Buffer^, Left);
  256. Dec(FChunkBytesLeft, Res);
  257. { Decrypt the data after reading from the file }
  258. if FChunkEncrypted then
  259. XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
  260. if Left = Res then
  261. Break
  262. else begin
  263. Dec(Left, Res);
  264. Inc(PByte(Buffer), Res);
  265. { Go to next disk }
  266. if FOpenedSlice >= FChunkLastSlice then
  267. { Already on the last slice, so the file must be corrupted... }
  268. SourceIsCorrupted('Already on last slice');
  269. OpenSlice(FOpenedSlice + 1);
  270. end;
  271. end;
  272. end;
  273. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  274. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  275. const VerifyChecksum: Boolean);
  276. var
  277. Context: TSHA256Context;
  278. AddrOffset: UInt32;
  279. Buf: array[0..65535] of Byte;
  280. { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
  281. otherwise the TransformCallInstructions call will break }
  282. begin
  283. if FEntered <> 0 then
  284. InternalError('Cannot call file extractor recursively');
  285. Inc(FEntered);
  286. try
  287. var BytesLeft := FL.OriginalSize;
  288. if BytesLeft < 0 then { sanity check }
  289. InternalError('TFileExtractor.DecompressFile: Negative size');
  290. { To avoid file system fragmentation, preallocate all of the bytes in the
  291. destination file }
  292. DestF.Seek(BytesLeft);
  293. DestF.Truncate;
  294. DestF.Seek(0);
  295. SHA256Init(Context);
  296. try
  297. AddrOffset := 0;
  298. while BytesLeft > 0 do begin
  299. var BufSize: Cardinal := SizeOf(Buf);
  300. if BytesLeft < BufSize then
  301. BufSize := Cardinal(BytesLeft);
  302. DecompressBytes(Buf, BufSize);
  303. if floCallInstructionOptimized in FL.Flags then begin
  304. TransformCallInstructions(Buf, BufSize, False, AddrOffset);
  305. Inc(AddrOffset, BufSize); { may wrap, but OK }
  306. end;
  307. Dec(BytesLeft, BufSize);
  308. SHA256Update(Context, Buf, BufSize);
  309. DestF.WriteBuffer(Buf, BufSize);
  310. if Assigned(ProgressProc) then
  311. ProgressProc(BufSize);
  312. end;
  313. except
  314. on E: ECompressDataError do
  315. SourceIsCorrupted(E.Message);
  316. end;
  317. if VerifyChecksum and not SHA256DigestsEqual(SHA256Final(Context), FL.SHA256Sum) then
  318. SourceIsCorrupted('SHA-256 hash mismatch');
  319. finally
  320. Dec(FEntered);
  321. end;
  322. end;
  323. end.