Setup.FileExtractor.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. unit Setup.FileExtractor;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2010 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, Shared.Compress,
  12. Shared.Struct, Shared.ArcFour;
  13. type
  14. TExtractorProgressProc = procedure(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: TArcFourContext;
  25. FCryptKey: String;
  26. FEntered: Integer;
  27. procedure DecompressBytes(var Buffer; Count: Cardinal);
  28. class function FindSliceFilename(const ASlice: Integer): String;
  29. procedure OpenSlice(const ASlice: Integer);
  30. function ReadProc(var Buf; Count: Longint): Longint;
  31. public
  32. constructor Create(ADecompressorClass: TCustomDecompressorClass);
  33. destructor Destroy; override;
  34. procedure DecompressFile(const FL: TSetupFileLocationEntry; const DestF: TFile;
  35. const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
  36. procedure SeekTo(const FL: TSetupFileLocationEntry;
  37. const ProgressProc: TExtractorProgressProc);
  38. property CryptKey: String write FCryptKey;
  39. end;
  40. function FileExtractor: TFileExtractor;
  41. procedure FreeFileExtractor;
  42. implementation
  43. uses
  44. PathFunc, Shared.CommonFunc, Setup.MainForm, Setup.Messages, Shared.SetupMessageIDs,
  45. Setup.InstFunc, Shared.Compress.Zlib, Shared.Compress.bzlib,
  46. Setup.LZMADecompressor, SHA1, Setup.LoggingFunc, Setup.NewDiskForm;
  47. var
  48. FFileExtractor: TFileExtractor;
  49. function FileExtractor: TFileExtractor;
  50. const
  51. DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
  52. (TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
  53. TLZMA2Decompressor);
  54. begin
  55. if FFileExtractor = nil then
  56. FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
  57. Result := FFileExtractor;
  58. end;
  59. procedure FreeFileExtractor;
  60. begin
  61. FreeAndNil(FFileExtractor);
  62. end;
  63. procedure SourceIsCorrupted(const AReason: String);
  64. begin
  65. Log('Source file corrupted: ' + AddPeriod(AReason));
  66. raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
  67. end;
  68. { TFileExtractor }
  69. constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
  70. begin
  71. inherited Create;
  72. FOpenedSlice := -1;
  73. FChunkFirstSlice := -1;
  74. { Create one 'decompressor' for use with uncompressed chunks, and another
  75. for use with compressed chunks }
  76. FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
  77. FDecompressor[True] := ADecompressorClass.Create(ReadProc);
  78. end;
  79. destructor TFileExtractor.Destroy;
  80. begin
  81. FSourceF.Free;
  82. FDecompressor[True].Free;
  83. FDecompressor[False].Free;
  84. inherited;
  85. end;
  86. var
  87. LastSourceDir: String;
  88. class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
  89. var
  90. Major, Minor: Integer;
  91. Prefix, F1, F2, Path: String;
  92. begin
  93. Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
  94. Major := ASlice div SetupHeader.SlicesPerDisk + 1;
  95. Minor := ASlice mod SetupHeader.SlicesPerDisk;
  96. if SetupHeader.SlicesPerDisk = 1 then
  97. F1 := Format('%s-%d.bin', [Prefix, Major])
  98. else
  99. F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
  100. F2 := Format('..\DISK%d\', [Major]) + F1;
  101. if LastSourceDir <> '' then begin
  102. Result := AddBackslash(LastSourceDir) + F1;
  103. if NewFileExists(Result) then Exit;
  104. end;
  105. Result := AddBackslash(SourceDir) + F1;
  106. if NewFileExists(Result) then Exit;
  107. if LastSourceDir <> '' then begin
  108. Result := PathExpand(AddBackslash(LastSourceDir) + F2);
  109. if NewFileExists(Result) then Exit;
  110. end;
  111. Result := PathExpand(AddBackslash(SourceDir) + F2);
  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.Lo <> 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. var
  169. Salt: TSetupSalt;
  170. Context: TSHA1Context;
  171. Hash: TSHA1Digest;
  172. begin
  173. { Read the salt }
  174. if FSourceF.Read(Salt, SizeOf(Salt)) <> SizeOf(Salt) then
  175. SourceIsCorrupted('Failed to read salt');
  176. { Initialize the key, which is the SHA-1 hash of the salt plus FCryptKey }
  177. SHA1Init(Context);
  178. SHA1Update(Context, Salt, SizeOf(Salt));
  179. SHA1Update(Context, Pointer(FCryptKey)^, Length(FCryptKey)*SizeOf(FCryptKey[1]));
  180. Hash := SHA1Final(Context);
  181. ArcFourInit(FCryptContext, Hash, SizeOf(Hash));
  182. { The compiler discards the first 1000 bytes for extra security,
  183. so we must as well }
  184. ArcFourDiscard(FCryptContext, 1000);
  185. end;
  186. procedure Discard(Count: Integer64);
  187. var
  188. Buf: array[0..65535] of Byte;
  189. BufSize: Cardinal;
  190. begin
  191. try
  192. while True do begin
  193. BufSize := SizeOf(Buf);
  194. if (Count.Hi = 0) and (Count.Lo < BufSize) then
  195. BufSize := Count.Lo;
  196. if BufSize = 0 then
  197. Break;
  198. DecompressBytes(Buf, BufSize);
  199. Dec64(Count, BufSize);
  200. if Assigned(ProgressProc) then
  201. ProgressProc(0);
  202. end;
  203. except
  204. on E: ECompressDataError do
  205. SourceIsCorrupted(E.Message);
  206. end;
  207. end;
  208. var
  209. TestCompID: TCompID;
  210. Diff: Integer64;
  211. begin
  212. if FEntered <> 0 then
  213. InternalError('Cannot call file extractor recursively');
  214. Inc(FEntered);
  215. try
  216. if (foChunkEncrypted in FL.Flags) and (FCryptKey = '') then
  217. InternalError('Cannot read an encrypted file before the key has been set');
  218. { Is the file in a different chunk than the current one?
  219. Or, is the file in a part of the current chunk that we've already passed?
  220. Or, did a previous decompression operation fail, necessitating a reset? }
  221. if (FChunkFirstSlice <> FL.FirstSlice) or
  222. (FChunkStartOffset <> FL.StartOffset) or
  223. (Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) < 0) or
  224. FNeedReset then begin
  225. FChunkFirstSlice := -1;
  226. FDecompressor[foChunkCompressed in FL.Flags].Reset;
  227. FNeedReset := False;
  228. OpenSlice(FL.FirstSlice);
  229. FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
  230. if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
  231. SourceIsCorrupted('Failed to read CompID');
  232. if Longint(TestCompID) <> Longint(ZLIBID) then
  233. SourceIsCorrupted('Invalid CompID');
  234. if foChunkEncrypted in FL.Flags then
  235. InitDecryption;
  236. FChunkFirstSlice := FL.FirstSlice;
  237. FChunkLastSlice := FL.LastSlice;
  238. FChunkStartOffset := FL.StartOffset;
  239. FChunkBytesLeft := FL.ChunkCompressedSize;
  240. FChunkDecompressedBytesRead.Hi := 0;
  241. FChunkDecompressedBytesRead.Lo := 0;
  242. FChunkCompressed := foChunkCompressed in FL.Flags;
  243. FChunkEncrypted := foChunkEncrypted in FL.Flags;
  244. end;
  245. { Need to seek forward in the chunk? }
  246. if Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) > 0 then begin
  247. Diff := FL.ChunkSuboffset;
  248. Dec6464(Diff, FChunkDecompressedBytesRead);
  249. Discard(Diff);
  250. end;
  251. finally
  252. Dec(FEntered);
  253. end;
  254. end;
  255. function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
  256. var
  257. Buffer: Pointer;
  258. Left, Res: Cardinal;
  259. begin
  260. Buffer := @Buf;
  261. Left := Count;
  262. if (FChunkBytesLeft.Hi = 0) and (FChunkBytesLeft.Lo < Left) then
  263. Left := FChunkBytesLeft.Lo;
  264. Result := Left;
  265. while Left <> 0 do begin
  266. Res := FSourceF.Read(Buffer^, Left);
  267. Dec64(FChunkBytesLeft, Res);
  268. { Decrypt the data after reading from the file }
  269. if FChunkEncrypted then
  270. ArcFourCrypt(FCryptContext, Buffer^, Buffer^, Res);
  271. if Left = Res then
  272. Break
  273. else begin
  274. Dec(Left, Res);
  275. Inc(Longint(Buffer), Res);
  276. { Go to next disk }
  277. if FOpenedSlice >= FChunkLastSlice then
  278. { Already on the last slice, so the file must be corrupted... }
  279. SourceIsCorrupted('Already on last slice');
  280. OpenSlice(FOpenedSlice + 1);
  281. end;
  282. end;
  283. end;
  284. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  285. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  286. const VerifyChecksum: Boolean);
  287. var
  288. BytesLeft: Integer64;
  289. Context: TSHA1Context;
  290. AddrOffset: LongWord;
  291. BufSize: Cardinal;
  292. Buf: array[0..65535] of Byte;
  293. { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
  294. otherwise the TransformCallInstructions call will break }
  295. begin
  296. if FEntered <> 0 then
  297. InternalError('Cannot call file extractor recursively');
  298. Inc(FEntered);
  299. try
  300. BytesLeft := FL.OriginalSize;
  301. { To avoid file system fragmentation, preallocate all of the bytes in the
  302. destination file }
  303. DestF.Seek64(BytesLeft);
  304. DestF.Truncate;
  305. DestF.Seek(0);
  306. SHA1Init(Context);
  307. try
  308. AddrOffset := 0;
  309. while True do begin
  310. BufSize := SizeOf(Buf);
  311. if (BytesLeft.Hi = 0) and (BytesLeft.Lo < BufSize) then
  312. BufSize := BytesLeft.Lo;
  313. if BufSize = 0 then
  314. Break;
  315. DecompressBytes(Buf, BufSize);
  316. if foCallInstructionOptimized in FL.Flags then begin
  317. TransformCallInstructions(Buf, BufSize, False, AddrOffset);
  318. Inc(AddrOffset, BufSize); { may wrap, but OK }
  319. end;
  320. Dec64(BytesLeft, BufSize);
  321. SHA1Update(Context, Buf, BufSize);
  322. DestF.WriteBuffer(Buf, BufSize);
  323. if Assigned(ProgressProc) then
  324. ProgressProc(BufSize);
  325. end;
  326. except
  327. on E: ECompressDataError do
  328. SourceIsCorrupted(E.Message);
  329. end;
  330. if VerifyChecksum and not SHA1DigestsEqual(SHA1Final(Context), FL.SHA1Sum) then
  331. SourceIsCorrupted('SHA-1 hash mismatch');
  332. finally
  333. Dec(FEntered);
  334. end;
  335. end;
  336. end.