Extract.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. unit Extract;
  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
  8. $jrsoftware: issrc/Projects/Extract.pas,v 1.30 2010/03/13 18:51:37 jr Exp $
  9. }
  10. interface
  11. uses
  12. Windows, SysUtils, Int64Em, FileClass, Compress, Struct, 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, CmnFunc2, Main, Msgs, MsgIDs, InstFunc, CompressZlib, bzlib,
  45. LZMADecomp, SHA1, Logging, NewDisk;
  46. var
  47. FFileExtractor: TFileExtractor;
  48. function FileExtractor: TFileExtractor;
  49. const
  50. DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
  51. (TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
  52. TLZMA2Decompressor);
  53. begin
  54. if FFileExtractor = nil then
  55. FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
  56. Result := FFileExtractor;
  57. end;
  58. procedure FreeFileExtractor;
  59. begin
  60. FreeAndNil(FFileExtractor);
  61. end;
  62. procedure SourceIsCorrupted(const AReason: String);
  63. begin
  64. Log('Source file corrupted: ' + AddPeriod(AReason));
  65. raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
  66. end;
  67. { TFileExtractor }
  68. constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
  69. begin
  70. inherited Create;
  71. FOpenedSlice := -1;
  72. FChunkFirstSlice := -1;
  73. { Create one 'decompressor' for use with uncompressed chunks, and another
  74. for use with compressed chunks }
  75. FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
  76. FDecompressor[True] := ADecompressorClass.Create(ReadProc);
  77. end;
  78. destructor TFileExtractor.Destroy;
  79. begin
  80. FSourceF.Free;
  81. FDecompressor[True].Free;
  82. FDecompressor[False].Free;
  83. inherited;
  84. end;
  85. var
  86. LastSourceDir: String;
  87. class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
  88. var
  89. Major, Minor: Integer;
  90. Prefix, F1, F2, Path: String;
  91. begin
  92. Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
  93. Major := ASlice div SetupHeader.SlicesPerDisk + 1;
  94. Minor := ASlice mod SetupHeader.SlicesPerDisk;
  95. if SetupHeader.SlicesPerDisk = 1 then
  96. F1 := Format('%s-%d.bin', [Prefix, Major])
  97. else
  98. F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
  99. F2 := Format('..\DISK%d\', [Major]) + F1;
  100. if LastSourceDir <> '' then begin
  101. Result := AddBackslash(LastSourceDir) + F1;
  102. if NewFileExists(Result) then Exit;
  103. end;
  104. Result := AddBackslash(SourceDir) + F1;
  105. if NewFileExists(Result) then Exit;
  106. if LastSourceDir <> '' then begin
  107. Result := PathExpand(AddBackslash(LastSourceDir) + F2);
  108. if NewFileExists(Result) then Exit;
  109. end;
  110. Result := PathExpand(AddBackslash(SourceDir) + F2);
  111. if NewFileExists(Result) then Exit;
  112. Path := SourceDir;
  113. LogFmt('Asking user for new disk containing "%s".', [F1]);
  114. if SelectDisk(Major, F1, Path) then begin
  115. LastSourceDir := Path;
  116. Result := AddBackslash(Path) + F1;
  117. end
  118. else
  119. Abort;
  120. end;
  121. procedure TFileExtractor.OpenSlice(const ASlice: Integer);
  122. var
  123. Filename: String;
  124. TestDiskSliceID: TDiskSliceID;
  125. DiskSliceHeader: TDiskSliceHeader;
  126. begin
  127. if FOpenedSlice = ASlice then
  128. Exit;
  129. FOpenedSlice := -1;
  130. FreeAndNil(FSourceF);
  131. if SetupLdrOffset1 = 0 then
  132. Filename := FindSliceFilename(ASlice)
  133. else
  134. Filename := SetupLdrOriginalFilename;
  135. FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
  136. if SetupLdrOffset1 = 0 then begin
  137. if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
  138. SourceIsCorrupted('Invalid slice header (1)');
  139. if TestDiskSliceID <> DiskSliceID then
  140. SourceIsCorrupted('Invalid slice header (2)');
  141. if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
  142. SourceIsCorrupted('Invalid slice header (3)');
  143. if FSourceF.Size.Lo <> DiskSliceHeader.TotalSize then
  144. SourceIsCorrupted('Invalid slice header (4)');
  145. end;
  146. FOpenedSlice := ASlice;
  147. end;
  148. procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
  149. begin
  150. try
  151. FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
  152. except
  153. { If DecompressInto raises an exception, force a decompressor reset &
  154. re-seek the next time SeekTo is called by setting FNeedReset to True.
  155. We don't want to get stuck in an endless loop with the decompressor
  156. in e.g. a data error state. Also, we have no way of knowing if
  157. DecompressInto successfully decompressed some of the requested bytes
  158. before the exception was raised. }
  159. FNeedReset := True;
  160. raise;
  161. end;
  162. Inc64(FChunkDecompressedBytesRead, Count);
  163. end;
  164. procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
  165. const ProgressProc: TExtractorProgressProc);
  166. procedure InitDecryption;
  167. var
  168. Salt: TSetupSalt;
  169. Context: TSHA1Context;
  170. Hash: TSHA1Digest;
  171. begin
  172. { Read the salt }
  173. if FSourceF.Read(Salt, SizeOf(Salt)) <> SizeOf(Salt) then
  174. SourceIsCorrupted('Failed to read salt');
  175. { Initialize the key, which is the SHA-1 hash of the salt plus FCryptKey }
  176. SHA1Init(Context);
  177. SHA1Update(Context, Salt, SizeOf(Salt));
  178. SHA1Update(Context, Pointer(FCryptKey)^, Length(FCryptKey)*SizeOf(FCryptKey[1]));
  179. Hash := SHA1Final(Context);
  180. ArcFourInit(FCryptContext, Hash, SizeOf(Hash));
  181. { The compiler discards the first 1000 bytes for extra security,
  182. so we must as well }
  183. ArcFourDiscard(FCryptContext, 1000);
  184. end;
  185. procedure Discard(Count: Integer64);
  186. var
  187. Buf: array[0..65535] of Byte;
  188. BufSize: Cardinal;
  189. begin
  190. try
  191. while True do begin
  192. BufSize := SizeOf(Buf);
  193. if (Count.Hi = 0) and (Count.Lo < BufSize) then
  194. BufSize := Count.Lo;
  195. if BufSize = 0 then
  196. Break;
  197. DecompressBytes(Buf, BufSize);
  198. Dec64(Count, BufSize);
  199. if Assigned(ProgressProc) then
  200. ProgressProc(0);
  201. end;
  202. except
  203. on E: ECompressDataError do
  204. SourceIsCorrupted(E.Message);
  205. end;
  206. end;
  207. var
  208. TestCompID: TCompID;
  209. Diff: Integer64;
  210. begin
  211. if FEntered <> 0 then
  212. InternalError('Cannot call file extractor recursively');
  213. Inc(FEntered);
  214. try
  215. if (foChunkEncrypted in FL.Flags) and (FCryptKey = '') then
  216. InternalError('Cannot read an encrypted file before the key has been set');
  217. { Is the file in a different chunk than the current one?
  218. Or, is the file in a part of the current chunk that we've already passed?
  219. Or, did a previous decompression operation fail, necessitating a reset? }
  220. if (FChunkFirstSlice <> FL.FirstSlice) or
  221. (FChunkStartOffset <> FL.StartOffset) or
  222. (Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) < 0) or
  223. FNeedReset then begin
  224. FChunkFirstSlice := -1;
  225. FDecompressor[foChunkCompressed in FL.Flags].Reset;
  226. FNeedReset := False;
  227. OpenSlice(FL.FirstSlice);
  228. FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
  229. if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
  230. SourceIsCorrupted('Failed to read CompID');
  231. if Longint(TestCompID) <> Longint(ZLIBID) then
  232. SourceIsCorrupted('Invalid CompID');
  233. if foChunkEncrypted in FL.Flags then
  234. InitDecryption;
  235. FChunkFirstSlice := FL.FirstSlice;
  236. FChunkLastSlice := FL.LastSlice;
  237. FChunkStartOffset := FL.StartOffset;
  238. FChunkBytesLeft := FL.ChunkCompressedSize;
  239. FChunkDecompressedBytesRead.Hi := 0;
  240. FChunkDecompressedBytesRead.Lo := 0;
  241. FChunkCompressed := foChunkCompressed in FL.Flags;
  242. FChunkEncrypted := foChunkEncrypted in FL.Flags;
  243. end;
  244. { Need to seek forward in the chunk? }
  245. if Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) > 0 then begin
  246. Diff := FL.ChunkSuboffset;
  247. Dec6464(Diff, FChunkDecompressedBytesRead);
  248. Discard(Diff);
  249. end;
  250. finally
  251. Dec(FEntered);
  252. end;
  253. end;
  254. function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
  255. var
  256. Buffer: Pointer;
  257. Left, Res: Cardinal;
  258. begin
  259. Buffer := @Buf;
  260. Left := Count;
  261. if (FChunkBytesLeft.Hi = 0) and (FChunkBytesLeft.Lo < Left) then
  262. Left := FChunkBytesLeft.Lo;
  263. Result := Left;
  264. while Left <> 0 do begin
  265. Res := FSourceF.Read(Buffer^, Left);
  266. Dec64(FChunkBytesLeft, Res);
  267. { Decrypt the data after reading from the file }
  268. if FChunkEncrypted then
  269. ArcFourCrypt(FCryptContext, Buffer^, Buffer^, Res);
  270. if Left = Res then
  271. Break
  272. else begin
  273. Dec(Left, Res);
  274. Inc(Longint(Buffer), Res);
  275. { Go to next disk }
  276. if FOpenedSlice >= FChunkLastSlice then
  277. { Already on the last slice, so the file must be corrupted... }
  278. SourceIsCorrupted('Already on last slice');
  279. OpenSlice(FOpenedSlice + 1);
  280. end;
  281. end;
  282. end;
  283. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  284. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  285. const VerifyChecksum: Boolean);
  286. var
  287. BytesLeft: Integer64;
  288. Context: TSHA1Context;
  289. AddrOffset: LongWord;
  290. BufSize: Cardinal;
  291. Buf: array[0..65535] of Byte;
  292. { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
  293. otherwise the TransformCallInstructions call will break }
  294. begin
  295. if FEntered <> 0 then
  296. InternalError('Cannot call file extractor recursively');
  297. Inc(FEntered);
  298. try
  299. BytesLeft := FL.OriginalSize;
  300. { To avoid file system fragmentation, preallocate all of the bytes in the
  301. destination file }
  302. DestF.Seek64(BytesLeft);
  303. DestF.Truncate;
  304. DestF.Seek(0);
  305. SHA1Init(Context);
  306. try
  307. AddrOffset := 0;
  308. while True do begin
  309. BufSize := SizeOf(Buf);
  310. if (BytesLeft.Hi = 0) and (BytesLeft.Lo < BufSize) then
  311. BufSize := BytesLeft.Lo;
  312. if BufSize = 0 then
  313. Break;
  314. DecompressBytes(Buf, BufSize);
  315. if foCallInstructionOptimized in FL.Flags then begin
  316. TransformCallInstructions(Buf, BufSize, False, AddrOffset);
  317. Inc(AddrOffset, BufSize); { may wrap, but OK }
  318. end;
  319. Dec64(BytesLeft, BufSize);
  320. SHA1Update(Context, Buf, BufSize);
  321. DestF.WriteBuffer(Buf, BufSize);
  322. if Assigned(ProgressProc) then
  323. ProgressProc(BufSize);
  324. end;
  325. except
  326. on E: ECompressDataError do
  327. SourceIsCorrupted(E.Message);
  328. end;
  329. if VerifyChecksum and not SHA1DigestsEqual(SHA1Final(Context), FL.SHA1Sum) then
  330. SourceIsCorrupted('SHA-1 hash mismatch');
  331. finally
  332. Dec(FEntered);
  333. end;
  334. end;
  335. end.