ArchiveExtractCallback.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. namespace SevenZip
  2. {
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.IO;
  7. #if UNMANAGED
  8. /// <summary>
  9. /// Archive extraction callback to handle the process of unpacking files
  10. /// </summary>
  11. internal sealed class ArchiveExtractCallback : CallbackBase, IArchiveExtractCallback, ICryptoGetTextPassword, IDisposable
  12. {
  13. private List<uint> _actualIndexes;
  14. private IInArchive _archive;
  15. /// <summary>
  16. /// For Compressing event.
  17. /// </summary>
  18. private long _bytesCount;
  19. private long _bytesWritten;
  20. private long _bytesWrittenOld;
  21. private string _directory;
  22. /// <summary>
  23. /// Rate of the done work from [0, 1].
  24. /// </summary>
  25. private float _doneRate;
  26. private SevenZipExtractor _extractor;
  27. private FakeOutStreamWrapper _fakeStream;
  28. private uint? _fileIndex;
  29. private int _filesCount;
  30. private OutStreamWrapper _fileStream;
  31. private bool _directoryStructure;
  32. private int _currentIndex;
  33. private const int MemoryPressure = 64 * 1024 * 1024; //64mb seems to be the maximum value
  34. #region Constructors
  35. /// <summary>
  36. /// Initializes a new instance of the ArchiveExtractCallback class
  37. /// </summary>
  38. /// <param name="archive">IInArchive interface for the archive</param>
  39. /// <param name="directory">Directory where files are to be unpacked to</param>
  40. /// <param name="filesCount">The archive files count</param>'
  41. /// <param name="extractor">The owner of the callback</param>
  42. /// <param name="actualIndexes">The list of actual indexes (solid archives support)</param>
  43. /// <param name="directoryStructure">The value indicating whether to preserve directory structure of extracted files.</param>
  44. public ArchiveExtractCallback(IInArchive archive, string directory, int filesCount, bool directoryStructure,
  45. List<uint> actualIndexes, SevenZipExtractor extractor)
  46. {
  47. Init(archive, directory, filesCount, directoryStructure, actualIndexes, extractor);
  48. }
  49. /// <summary>
  50. /// Initializes a new instance of the ArchiveExtractCallback class
  51. /// </summary>
  52. /// <param name="archive">IInArchive interface for the archive</param>
  53. /// <param name="directory">Directory where files are to be unpacked to</param>
  54. /// <param name="filesCount">The archive files count</param>
  55. /// <param name="password">Password for the archive</param>
  56. /// <param name="extractor">The owner of the callback</param>
  57. /// <param name="actualIndexes">The list of actual indexes (solid archives support)</param>
  58. /// <param name="directoryStructure">The value indicating whether to preserve directory structure of extracted files.</param>
  59. public ArchiveExtractCallback(IInArchive archive, string directory, int filesCount, bool directoryStructure,
  60. List<uint> actualIndexes, string password, SevenZipExtractor extractor)
  61. : base(password)
  62. {
  63. Init(archive, directory, filesCount, directoryStructure, actualIndexes, extractor);
  64. }
  65. /// <summary>
  66. /// Initializes a new instance of the ArchiveExtractCallback class
  67. /// </summary>
  68. /// <param name="archive">IInArchive interface for the archive</param>
  69. /// <param name="stream">The stream where files are to be unpacked to</param>
  70. /// <param name="filesCount">The archive files count</param>
  71. /// <param name="fileIndex">The file index for the stream</param>
  72. /// <param name="extractor">The owner of the callback</param>
  73. public ArchiveExtractCallback(IInArchive archive, Stream stream, int filesCount, uint fileIndex, SevenZipExtractor extractor)
  74. {
  75. Init(archive, stream, filesCount, fileIndex, extractor);
  76. }
  77. /// <summary>
  78. /// Initializes a new instance of the ArchiveExtractCallback class
  79. /// </summary>
  80. /// <param name="archive">IInArchive interface for the archive</param>
  81. /// <param name="stream">The stream where files are to be unpacked to</param>
  82. /// <param name="filesCount">The archive files count</param>
  83. /// <param name="fileIndex">The file index for the stream</param>
  84. /// <param name="password">Password for the archive</param>
  85. /// <param name="extractor">The owner of the callback</param>
  86. public ArchiveExtractCallback(IInArchive archive, Stream stream, int filesCount, uint fileIndex, string password, SevenZipExtractor extractor)
  87. : base(password)
  88. {
  89. Init(archive, stream, filesCount, fileIndex, extractor);
  90. }
  91. private void Init(IInArchive archive, string directory, int filesCount, bool directoryStructure, List<uint> actualIndexes, SevenZipExtractor extractor)
  92. {
  93. CommonInit(archive, filesCount, extractor);
  94. _directory = directory;
  95. _actualIndexes = actualIndexes;
  96. _directoryStructure = directoryStructure;
  97. if (!directory.EndsWith("" + Path.DirectorySeparatorChar, StringComparison.CurrentCulture))
  98. {
  99. _directory += Path.DirectorySeparatorChar;
  100. }
  101. }
  102. private void Init(IInArchive archive, Stream stream, int filesCount, uint fileIndex, SevenZipExtractor extractor)
  103. {
  104. CommonInit(archive, filesCount, extractor);
  105. _fileStream = new OutStreamWrapper(stream, false);
  106. _fileStream.BytesWritten += IntEventArgsHandler;
  107. _fileIndex = fileIndex;
  108. }
  109. private void CommonInit(IInArchive archive, int filesCount, SevenZipExtractor extractor)
  110. {
  111. _archive = archive;
  112. _filesCount = filesCount;
  113. _fakeStream = new FakeOutStreamWrapper();
  114. _fakeStream.BytesWritten += IntEventArgsHandler;
  115. _extractor = extractor;
  116. GC.AddMemoryPressure(MemoryPressure);
  117. }
  118. #endregion
  119. /// <summary>
  120. /// Occurs when a new file is going to be unpacked
  121. /// </summary>
  122. /// <remarks>Occurs when 7-zip engine requests for an output stream for a new file to unpack in</remarks>
  123. public event EventHandler<FileInfoEventArgs> FileExtractionStarted;
  124. /// <summary>
  125. /// Occurs when a file has been successfully unpacked
  126. /// </summary>
  127. public event EventHandler<FileInfoEventArgs> FileExtractionFinished;
  128. /// <summary>
  129. /// Occurs when the archive is opened and 7-zip sends the size of unpacked data
  130. /// </summary>
  131. public event EventHandler<OpenEventArgs> Open;
  132. /// <summary>
  133. /// Occurs when the extraction is performed
  134. /// </summary>
  135. public event EventHandler<ProgressEventArgs> Extracting;
  136. /// <summary>
  137. /// Occurs during the extraction when a file already exists
  138. /// </summary>
  139. public event EventHandler<FileOverwriteEventArgs> FileExists;
  140. private void IntEventArgsHandler(object sender, IntEventArgs e)
  141. {
  142. // If _bytesCount is not set, we can't update the progress.
  143. if (_bytesCount == 0)
  144. {
  145. return;
  146. }
  147. var pold = (int)(_bytesWrittenOld * 100 / _bytesCount);
  148. _bytesWritten += e.Value;
  149. var pnow = (int)(_bytesWritten * 100 / _bytesCount);
  150. if (pnow > pold)
  151. {
  152. if (pnow > 100)
  153. {
  154. pold = pnow = 0;
  155. }
  156. _bytesWrittenOld = _bytesWritten;
  157. Extracting?.Invoke(this, new ProgressEventArgs((byte)pnow, (byte)(pnow - pold)));
  158. }
  159. }
  160. #region IArchiveExtractCallback Members
  161. /// <summary>
  162. /// Gives the size of the unpacked archive files
  163. /// </summary>
  164. /// <param name="total">Size of the unpacked archive files (in bytes)</param>
  165. public void SetTotal(ulong total)
  166. {
  167. _bytesCount = (long)total;
  168. Open?.Invoke(this, new OpenEventArgs(total));
  169. }
  170. public void SetCompleted(ref ulong completeValue) { }
  171. /// <summary>
  172. /// Sets output stream for writing unpacked data
  173. /// </summary>
  174. /// <param name="index">Current file index</param>
  175. /// <param name="outStream">Output stream pointer</param>
  176. /// <param name="askExtractMode">Extraction mode</param>
  177. /// <returns>0 if OK</returns>
  178. public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode)
  179. {
  180. outStream = null;
  181. if (Canceled)
  182. {
  183. return -1;
  184. }
  185. _currentIndex = (int)index;
  186. if (askExtractMode == AskMode.Extract)
  187. {
  188. var fileName = _directory;
  189. if (!_fileIndex.HasValue)
  190. {
  191. // Extraction to a file
  192. if (_actualIndexes == null || _actualIndexes.Contains(index))
  193. {
  194. var data = new PropVariant();
  195. _archive.GetProperty(index, ItemPropId.Path, ref data);
  196. var entryName = NativeMethods.SafeCast(data, "");
  197. #region Get entryName
  198. if (string.IsNullOrEmpty(entryName))
  199. {
  200. if (_filesCount == 1)
  201. {
  202. var archName = Path.GetFileName(_extractor.FileName);
  203. archName = archName.Substring(0,
  204. archName.LastIndexOf('.'));
  205. if (!archName.EndsWith(".tar",
  206. StringComparison.OrdinalIgnoreCase))
  207. {
  208. archName += ".tar";
  209. }
  210. entryName = archName;
  211. }
  212. else
  213. {
  214. entryName = "[no name] " + index.ToString(CultureInfo.InvariantCulture);
  215. }
  216. }
  217. #endregion
  218. try
  219. {
  220. fileName = Path.Combine(RemoveIllegalCharacters(_directory, true), RemoveIllegalCharacters(_directoryStructure ? entryName : Path.GetFileName(entryName)));
  221. if (string.IsNullOrEmpty(fileName))
  222. {
  223. throw new SevenZipArchiveException("Some archive name is null or empty.");
  224. }
  225. }
  226. catch (Exception e)
  227. {
  228. AddException(e);
  229. outStream = _fakeStream;
  230. return 0;
  231. }
  232. _archive.GetProperty(index, ItemPropId.IsDirectory, ref data);
  233. if (!NativeMethods.SafeCast(data, false))
  234. {
  235. _archive.GetProperty(index, ItemPropId.LastWriteTime, ref data);
  236. var time = NativeMethods.SafeCast(data, DateTime.MinValue);
  237. if (File.Exists(fileName))
  238. {
  239. var fnea = new FileOverwriteEventArgs(fileName);
  240. FileExists?.Invoke(this, fnea);
  241. if (fnea.Cancel)
  242. {
  243. Canceled = true;
  244. return -1;
  245. }
  246. if (string.IsNullOrEmpty(fnea.FileName))
  247. {
  248. outStream = _fakeStream;
  249. }
  250. else
  251. {
  252. fileName = fnea.FileName;
  253. }
  254. }
  255. _doneRate += 1.0f / _filesCount;
  256. var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[(int) index], PercentDoneEventArgs.ProducePercentDone(_doneRate));
  257. FileExtractionStarted?.Invoke(this, iea);
  258. if (iea.Cancel)
  259. {
  260. Canceled = true;
  261. return -1;
  262. }
  263. if (iea.Skip)
  264. {
  265. outStream = _fakeStream;
  266. return 0;
  267. }
  268. CreateDirectory(fileName);
  269. try
  270. {
  271. _fileStream = new OutStreamWrapper(File.Create(fileName), fileName, time, true);
  272. }
  273. catch (Exception e)
  274. {
  275. AddException(e is FileNotFoundException
  276. ? new IOException($"The file \"{fileName}\" was not extracted due to the File.Create fail.")
  277. : e);
  278. outStream = _fakeStream;
  279. return 0;
  280. }
  281. _fileStream.BytesWritten += IntEventArgsHandler;
  282. outStream = _fileStream;
  283. }
  284. else
  285. {
  286. _doneRate += 1.0f / _filesCount;
  287. var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[(int)index], PercentDoneEventArgs.ProducePercentDone(_doneRate));
  288. FileExtractionStarted?.Invoke(this, iea);
  289. if (iea.Cancel)
  290. {
  291. Canceled = true;
  292. return -1;
  293. }
  294. if (iea.Skip)
  295. {
  296. outStream = _fakeStream;
  297. return 0;
  298. }
  299. if (!Directory.Exists(fileName))
  300. {
  301. try
  302. {
  303. Directory.CreateDirectory(fileName);
  304. }
  305. catch (Exception e)
  306. {
  307. AddException(e);
  308. }
  309. outStream = _fakeStream;
  310. }
  311. }
  312. }
  313. else
  314. {
  315. outStream = _fakeStream;
  316. }
  317. }
  318. else
  319. {
  320. // Extraction to a stream.
  321. if (index == _fileIndex)
  322. {
  323. outStream = _fileStream;
  324. _fileIndex = null;
  325. }
  326. else
  327. {
  328. outStream = _fakeStream;
  329. }
  330. }
  331. }
  332. return 0;
  333. }
  334. /// <inheritdoc />
  335. public void PrepareOperation(AskMode askExtractMode) { }
  336. /// <inheritdoc />
  337. public void SetOperationResult(OperationResult operationResult)
  338. {
  339. if (operationResult != OperationResult.Ok && ReportErrors)
  340. {
  341. switch (operationResult)
  342. {
  343. case OperationResult.CrcError:
  344. AddException(new ExtractionFailedException("File is corrupted. Crc check has failed."));
  345. break;
  346. case OperationResult.DataError:
  347. AddException(new ExtractionFailedException("File is corrupted. Data error has occured."));
  348. break;
  349. case OperationResult.UnsupportedMethod:
  350. AddException(new ExtractionFailedException("Unsupported method error has occured."));
  351. break;
  352. case OperationResult.Unavailable:
  353. AddException(new ExtractionFailedException("File is unavailable."));
  354. break;
  355. case OperationResult.UnexpectedEnd:
  356. AddException(new ExtractionFailedException("Unexpected end of file."));
  357. break;
  358. case OperationResult.DataAfterEnd:
  359. AddException(new ExtractionFailedException("Data after end of archive."));
  360. break;
  361. case OperationResult.IsNotArc:
  362. AddException(new ExtractionFailedException("File is not archive."));
  363. break;
  364. case OperationResult.HeadersError:
  365. AddException(new ExtractionFailedException("Archive headers error."));
  366. break;
  367. case OperationResult.WrongPassword:
  368. AddException(new ExtractionFailedException("Wrong password."));
  369. break;
  370. default:
  371. AddException(new ExtractionFailedException($"Unexpected operation result: {operationResult}"));
  372. break;
  373. }
  374. }
  375. else
  376. {
  377. if (_fileStream != null && !_fileIndex.HasValue)
  378. {
  379. try
  380. {
  381. _fileStream.BytesWritten -= IntEventArgsHandler;
  382. _fileStream.Dispose();
  383. }
  384. catch (ObjectDisposedException) { }
  385. _fileStream = null;
  386. }
  387. var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[_currentIndex], PercentDoneEventArgs.ProducePercentDone(_doneRate));
  388. FileExtractionFinished?.Invoke(this, iea);
  389. if (iea.Cancel)
  390. {
  391. Canceled = true;
  392. }
  393. }
  394. }
  395. #endregion
  396. /// <inheritdoc />
  397. public int CryptoGetTextPassword(out string password)
  398. {
  399. password = Password;
  400. return 0;
  401. }
  402. /// <inheritdoc />
  403. public void Dispose()
  404. {
  405. GC.RemoveMemoryPressure(MemoryPressure);
  406. if (_fileStream != null)
  407. {
  408. try
  409. {
  410. _fileStream.Dispose();
  411. }
  412. catch (ObjectDisposedException) { }
  413. _fileStream = null;
  414. }
  415. if (_fakeStream != null)
  416. {
  417. try
  418. {
  419. _fakeStream.Dispose();
  420. }
  421. catch (ObjectDisposedException) { }
  422. _fakeStream = null;
  423. }
  424. }
  425. /// <summary>
  426. /// Ensures that the directory to the file name is valid and creates intermediate directories if necessary
  427. /// </summary>
  428. /// <param name="fileName">File name</param>
  429. private static void CreateDirectory(string fileName)
  430. {
  431. var destinationDirectory = Path.GetDirectoryName(fileName);
  432. if (!string.IsNullOrEmpty(destinationDirectory))
  433. {
  434. Directory.CreateDirectory(destinationDirectory);
  435. }
  436. }
  437. /// <summary>
  438. /// removes the invalid character in file path.
  439. /// </summary>
  440. /// <param name="str"></param>
  441. /// <param name="isDirectory"></param>
  442. /// <returns></returns>
  443. private static string RemoveIllegalCharacters(string str, bool isDirectory = false)
  444. {
  445. var splitFileName = new List<string>(str.Split(Path.DirectorySeparatorChar));
  446. foreach (var chr in Path.GetInvalidFileNameChars())
  447. {
  448. for (var i = 0; i < splitFileName.Count; i++)
  449. {
  450. if (isDirectory && chr == ':' && i == 0)
  451. {
  452. continue;
  453. }
  454. if (string.IsNullOrEmpty(splitFileName[i]))
  455. {
  456. continue;
  457. }
  458. while (splitFileName[i].IndexOf(chr) > -1)
  459. {
  460. splitFileName[i] = splitFileName[i].Replace(chr, '_');
  461. }
  462. }
  463. }
  464. if (str.StartsWith(new string(Path.DirectorySeparatorChar, 2), StringComparison.CurrentCultureIgnoreCase))
  465. {
  466. splitFileName.RemoveAt(0);
  467. splitFileName.RemoveAt(0);
  468. splitFileName[0] = new string(Path.DirectorySeparatorChar, 2) + splitFileName[0];
  469. }
  470. return string.Join(new string(Path.DirectorySeparatorChar, 1), splitFileName.ToArray());
  471. }
  472. }
  473. #endif
  474. }