PageRenderTime 67ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 1ms

/Utilities/Compression/ZipFile.cs

#
C# | 1404 lines | 842 code | 133 blank | 429 comment | 101 complexity | f98daee5f9f389405594378514151dd0 MD5 | raw file
Possible License(s): Apache-2.0
  1. // Based on Mike Krueger's SharpZipLib, Copyright (C) 2001 (GNU license).
  2. // Authors of the original java version: Jochen Hoenicke, John Leuner
  3. // See http://www.ISeeSharpCode.com for more information.
  4. using System;
  5. using System.Collections;
  6. using System.IO;
  7. using System.Runtime.CompilerServices;
  8. using System.Security.Cryptography;
  9. using System.Text;
  10. using Delta.Utilities.Compression.Checksums;
  11. using Delta.Utilities.Compression.Inflaters;
  12. using Delta.Utilities.Compression.Streams;
  13. using Delta.Utilities.Helpers;
  14. namespace Delta.Utilities.Compression
  15. {
  16. /// <summary>
  17. /// This class represents a Zip archive. You can ask for the contained
  18. /// entries, or get an input stream for a file entry. The entry is
  19. /// automatically decompressed.
  20. ///
  21. /// This class is thread safe: You can open input streams for arbitrary
  22. /// entries in different threads.
  23. /// </summary>
  24. /// <example>
  25. /// <code>
  26. /// using System;
  27. /// using System.Text;
  28. /// using System.Collections;
  29. /// using System.IO;
  30. ///
  31. /// using Delta.Utilities.Compression;
  32. ///
  33. /// class MainClass
  34. /// {
  35. /// static public void Main(string[] args)
  36. /// {
  37. /// ZipFile zFile = new ZipFile(args[0]);
  38. /// Console.WriteLine("Listing of : " + zFile.Name);
  39. /// Console.WriteLine("");
  40. /// Console.WriteLine("Raw Size Size Date Time Name");
  41. /// Console.WriteLine("-------- -------- -------- ------ ---------");
  42. /// foreach (ZipEntry e in zFile)
  43. /// {
  44. /// DateTime d = e.DateTime;
  45. /// Console.WriteLine("{0, -10}{1, -10}{2} {3} {4}",
  46. /// e.Size, e.CompressedSize,
  47. /// d.ToString("dd-MM-yy"), d.ToString("t"),
  48. /// e.Name);
  49. /// } // foreach
  50. /// } // Main(args)
  51. /// } // class MainClass
  52. /// </code>
  53. /// </example>
  54. public class ZipFile : IEnumerable, IDisposable
  55. {
  56. #region ZipEntryEnumeration Class
  57. /// <summary>
  58. /// Zip entry enumeration
  59. /// </summary>
  60. private class ZipEntryEnumeration : IEnumerator
  61. {
  62. #region Current (Public)
  63. /// <summary>
  64. /// Current
  65. /// </summary>
  66. /// <returns>Object</returns>
  67. public object Current
  68. {
  69. get
  70. {
  71. return array[ptr];
  72. }
  73. }
  74. #endregion
  75. #region Private
  76. #region array (Private)
  77. /// <summary>
  78. /// Array
  79. /// </summary>
  80. private readonly ZipEntry[] array;
  81. #endregion
  82. #region ptr (Private)
  83. /// <summary>
  84. /// Pointer to entry as an integer
  85. /// </summary>
  86. /// <returns>-</returns>
  87. private int ptr = -1;
  88. #endregion
  89. #endregion
  90. #region Constructors
  91. /// <summary>
  92. /// Create zip entry enumeration
  93. /// </summary>
  94. /// <param name="arr">Array</param>
  95. public ZipEntryEnumeration(ZipEntry[] arr)
  96. {
  97. array = arr;
  98. }
  99. #endregion
  100. #region IEnumerator Members
  101. /// <summary>
  102. /// Move next
  103. /// </summary>
  104. /// <returns>True if there is more to enumerate</returns>
  105. public bool MoveNext()
  106. {
  107. return (++ptr < array.Length);
  108. }
  109. /// <summary>
  110. /// Reset
  111. /// </summary>
  112. public void Reset()
  113. {
  114. ptr = -1;
  115. }
  116. #endregion
  117. }
  118. #endregion
  119. #region PartialInputStream Class
  120. /// <summary>
  121. /// Partial input stream
  122. /// </summary>
  123. /// <returns>Inflater input stream</returns>
  124. private class PartialInputStream : InflaterInputStream
  125. {
  126. #region IsEntryAvailable (Public)
  127. /// <summary>
  128. /// Available
  129. /// </summary>
  130. public override int IsEntryAvailable
  131. {
  132. get
  133. {
  134. long amount = end - filepos;
  135. if (amount > Int32.MaxValue)
  136. {
  137. return Int32.MaxValue;
  138. }
  139. return (int)amount;
  140. }
  141. }
  142. #endregion
  143. #region Private
  144. #region baseStream (Private)
  145. /// <summary>
  146. /// Base stream
  147. /// </summary>
  148. private readonly Stream baseStream;
  149. #endregion
  150. #region end (Private)
  151. /// <summary>
  152. /// File position
  153. /// </summary>
  154. private readonly long end;
  155. #endregion
  156. #region filepos (Private)
  157. /// <summary>
  158. /// File position
  159. /// </summary>
  160. private long filepos;
  161. #endregion
  162. #endregion
  163. #region Constructors
  164. /// <summary>
  165. /// Create partial input stream
  166. /// </summary>
  167. /// <param name="baseStream">Base stream</param>
  168. /// <param name="start">Start</param>
  169. /// <param name="len">Len</param>
  170. public PartialInputStream(Stream baseStream, long start, long len)
  171. : base(baseStream)
  172. {
  173. this.baseStream = baseStream;
  174. filepos = start;
  175. end = start + len;
  176. }
  177. #endregion
  178. #region ReadByte (Public)
  179. /// <summary>
  180. /// Read a byte from this stream.
  181. /// </summary>
  182. /// <returns>Returns the byte read or -1 on end of stream.</returns>
  183. public override int ReadByte()
  184. {
  185. if (filepos == end)
  186. {
  187. return -1; //ok
  188. }
  189. //why? This produces CA2002: Do not lock on objects with weak identity
  190. //lock (baseStream)
  191. //{
  192. baseStream.Seek(filepos++, SeekOrigin.Begin);
  193. return baseStream.ReadByte();
  194. //} // lock
  195. }
  196. #endregion
  197. #region Close (Public)
  198. /// <summary>
  199. /// Close this partial input stream.
  200. /// </summary>
  201. /// <remarks>
  202. /// The underlying stream is not closed.
  203. /// Close the parent ZipFile class to do that.
  204. /// </remarks>
  205. public override void Close()
  206. {
  207. // Do nothing at all!
  208. }
  209. #endregion
  210. #region Read (Public)
  211. /// <summary>
  212. /// Read
  213. /// </summary>
  214. /// <param name="b">B</param>
  215. /// <param name="off">Off</param>
  216. /// <param name="len">Len</param>
  217. /// <returns>Int</returns>
  218. public override int Read(byte[] b, int off, int len)
  219. {
  220. if (len > end - filepos)
  221. {
  222. len = (int)(end - filepos);
  223. if (len == 0)
  224. {
  225. return 0;
  226. }
  227. }
  228. //why? This produces CA2002: Do not lock on objects with weak identity
  229. //lock (baseStream)
  230. //{
  231. baseStream.Seek(filepos, SeekOrigin.Begin);
  232. int count = baseStream.Read(b, off, len);
  233. if (count > 0)
  234. {
  235. filepos += len;
  236. }
  237. return count;
  238. //} // lock
  239. }
  240. #endregion
  241. #region SkipBytes (Public)
  242. /// <summary>
  243. /// Skip bytes
  244. /// </summary>
  245. /// <param name="amount">Amount</param>
  246. /// <returns>Long</returns>
  247. public long SkipBytes(long amount)
  248. {
  249. if (amount < 0)
  250. {
  251. throw new ArgumentOutOfRangeException();
  252. }
  253. if (amount > end - filepos)
  254. {
  255. amount = end - filepos;
  256. }
  257. filepos += amount;
  258. return amount;
  259. }
  260. #endregion
  261. }
  262. #endregion
  263. #region Delegates
  264. /// <summary>
  265. /// Delegate for handling keys/password setting during
  266. /// compression / decompression.
  267. /// </summary>
  268. public delegate void KeysRequiredEventHandler(
  269. object sender,
  270. KeysRequiredEventArgs e
  271. );
  272. #endregion
  273. #region Open (Static)
  274. /// <summary>
  275. /// Opens a zip file with the given name for reading an returns it or
  276. /// 'null' if the file is invalid.
  277. /// </summary>
  278. /// <param name="zipFilePath">Zip file path</param>
  279. public static ZipFile Open(string zipFilePath)
  280. {
  281. if (FileHelper.Exists(zipFilePath) == false)
  282. {
  283. Log.Warning("Can't open the zip file '" + zipFilePath +
  284. "' because it doesn't exists");
  285. return null;
  286. } // if
  287. ZipFile zipFile = null;
  288. try
  289. {
  290. FileStream zipFileStream = FileHelper.Open(zipFilePath);
  291. zipFile = new ZipFile(zipFileStream)
  292. {
  293. FilePath = zipFilePath,
  294. };
  295. zipFile.ReadEntries();
  296. } // try
  297. catch (Exception ex)
  298. {
  299. Log.Warning("Couldn't open the zip file '" + zipFilePath +
  300. "' because of reason: '" + ex.Message + "'");
  301. zipFile.Close();
  302. zipFile = null;
  303. } // catch
  304. return zipFile;
  305. }
  306. #endregion
  307. #region Create (Static)
  308. /// <summary>
  309. /// Creates a zip file with the given name and returns it or 'null' if the
  310. /// file can't be created for some reason.
  311. /// </summary>
  312. /// <param name="newZipFilePath">New zip file path</param>
  313. /// <param name="overrideIfExists">Override if exists</param>
  314. /// <returns>Zip file</returns>
  315. public static ZipFile Create(string newZipFilePath,
  316. bool overrideIfExists = false)
  317. {
  318. FileMode fileCreationMode = FileMode.CreateNew;
  319. if (FileHelper.Exists(newZipFilePath))
  320. {
  321. if (overrideIfExists)
  322. {
  323. // Switch to "override file" mode
  324. fileCreationMode = FileMode.Create;
  325. } // if
  326. else
  327. {
  328. Log.Warning("Can't create zip file '" + newZipFilePath +
  329. "' because it already exists");
  330. return null;
  331. } // else
  332. } // if
  333. ZipFile zipFile = null;
  334. try
  335. {
  336. FileStream zipFileStream = FileHelper.Open(newZipFilePath,
  337. fileCreationMode, FileAccess.ReadWrite, FileShare.None);
  338. zipFile = new ZipFile(zipFileStream)
  339. {
  340. FilePath = newZipFilePath,
  341. };
  342. zipFile.zipOutput = new ZipOutputStream(zipFile.baseStream);
  343. // Compression: 0=none - 9=best
  344. zipFile.zipOutput.SetCompressionLevel(9);
  345. } // try
  346. catch (Exception ex)
  347. {
  348. Log.Warning("Couldn't create the zip file '" + newZipFilePath +
  349. "' because of reason: '" + ex.Message + "'");
  350. } // catch
  351. return zipFile;
  352. }
  353. #endregion
  354. #region ZipFileComment (Public)
  355. /// <summary>
  356. /// Gets the comment for the zip file.
  357. /// </summary>
  358. public string ZipFileComment
  359. {
  360. get
  361. {
  362. return comment;
  363. } // get
  364. }
  365. #endregion
  366. #region FilePath (Public)
  367. /// <summary>
  368. /// The path of this zip file.
  369. /// </summary>
  370. public string FilePath
  371. {
  372. get;
  373. private set;
  374. }
  375. #endregion
  376. #region Size (Public)
  377. /// <summary>
  378. /// Gets the size (in bytes) of this zip file.
  379. /// </summary>
  380. /// <exception cref="InvalidOperationException">
  381. /// The Zip file has been closed.
  382. /// </exception>
  383. public long Size
  384. {
  385. get
  386. {
  387. if (baseStream != null)
  388. {
  389. return baseStream.Length;
  390. } // if
  391. else
  392. {
  393. throw new InvalidOperationException("ZipFile is closed");
  394. } // else
  395. } // get
  396. }
  397. #endregion
  398. #region Password (Public)
  399. /// <summary>
  400. /// Password to be used for encrypting/decrypting files.
  401. /// </summary>
  402. /// <remarks>Set to null if no password is required.</remarks>
  403. public string Password
  404. {
  405. set
  406. {
  407. if (value == null ||
  408. value.Length == 0)
  409. {
  410. key = null;
  411. } // if
  412. else
  413. {
  414. key = ZipEncryption.GenerateKeys(Encoding.ASCII.GetBytes(value));
  415. } // else
  416. } // set
  417. }
  418. #endregion
  419. #region EntryByIndex (Public)
  420. /// <summary>
  421. /// Indexer property for ZipEntries
  422. /// </summary>
  423. [IndexerName("EntryByIndex")]
  424. public ZipEntry this[int index]
  425. {
  426. get
  427. {
  428. return entries[index].Clone();
  429. } // get
  430. }
  431. #endregion
  432. #region zipOutput (Public)
  433. /// <summary>
  434. /// Zip output
  435. /// </summary>
  436. public ZipOutputStream zipOutput;
  437. #endregion
  438. #region KeysRequired (Public)
  439. /// <summary>
  440. /// Event handler for handling encryption keys.
  441. /// </summary>
  442. public KeysRequiredEventHandler KeysRequired;
  443. #endregion
  444. #region Private
  445. #region baseStream (Private)
  446. /// <summary>
  447. /// Base Stream
  448. /// </summary>
  449. private readonly Stream baseStream;
  450. #endregion
  451. #region comment (Private)
  452. /// <summary>
  453. /// File Comment
  454. /// </summary>
  455. private string comment;
  456. #endregion
  457. #region isStreamOwner (Private)
  458. /// <summary>
  459. /// Get/set a flag indicating if the underlying stream is owned by the
  460. /// ZipFile instance. If the flag is true then the stream will be closed
  461. /// when <see cref="Close">Close</see> is called.
  462. /// </summary>
  463. /// <remarks>
  464. /// The default value is true in all cases.
  465. /// </remarks>
  466. private bool isStreamOwner = true;
  467. #endregion
  468. #region offsetOfFirstEntry (Private)
  469. /// <summary>
  470. /// Offset of first entry
  471. /// </summary>
  472. /// <returns>0</returns>
  473. private long offsetOfFirstEntry;
  474. #endregion
  475. #region entries (Private)
  476. /// <summary>
  477. /// Entries
  478. /// </summary>
  479. private ZipEntry[] entries;
  480. #endregion
  481. #region key (Private)
  482. /// <summary>
  483. /// The encryption key value.
  484. /// </summary>
  485. /// <returns>Null</returns>
  486. private byte[] key;
  487. #endregion
  488. #region HaveKeys (Private)
  489. /// <summary>
  490. /// Have keys
  491. /// </summary>
  492. /// <returns>Does this zip file entry have keys?</returns>
  493. private bool HaveKeys
  494. {
  495. get
  496. {
  497. return key != null;
  498. } // get
  499. }
  500. #endregion
  501. #endregion
  502. #region Constructors
  503. /// <summary>
  504. /// Opens a Zip file reading the given Stream
  505. /// </summary>
  506. /// <exception cref="ZipException">
  507. /// The file doesn't contain a valid zip archive.
  508. /// <para />
  509. /// The stream provided cannot seek.
  510. /// </exception>
  511. private ZipFile(Stream setStream)
  512. {
  513. baseStream = setStream;
  514. }
  515. #endregion
  516. #region IDisposable Members
  517. /// <summary>
  518. /// Dispose
  519. /// </summary>
  520. public void Dispose()
  521. {
  522. Close();
  523. }
  524. #endregion
  525. #region IEnumerable Members
  526. /// <summary>
  527. /// Returns an enumerator for the Zip entries in this Zip file.
  528. /// </summary>
  529. /// <exception cref="InvalidOperationException">
  530. /// The Zip file has been closed.
  531. /// </exception>
  532. public IEnumerator GetEnumerator()
  533. {
  534. if (entries == null)
  535. {
  536. throw new InvalidOperationException("ZipFile has closed");
  537. }
  538. return new ZipEntryEnumeration(entries);
  539. }
  540. #endregion
  541. #region Close (Public)
  542. /// <summary>
  543. /// Closes the ZipFile. If the stream is
  544. /// <see cref="isStreamOwner">owned</see> then this also closes the
  545. /// underlying input stream. Once closed, no further instance methods
  546. /// should be called.
  547. /// </summary>
  548. /// <exception cref="System.IO.IOException">
  549. /// An i/o error occurs.
  550. /// </exception>
  551. public void Close()
  552. {
  553. entries = null;
  554. if (isStreamOwner)
  555. {
  556. /*produces warning CA2002: Do not lock on objects with weak identity
  557. lock (baseStream)
  558. {
  559. baseStream.Close();
  560. } // lock
  561. */
  562. if (zipOutput != null)
  563. {
  564. zipOutput.Finish();
  565. zipOutput.Close();
  566. zipOutput = null;
  567. } // if (zipOutput)
  568. baseStream.Close();
  569. }
  570. }
  571. #endregion
  572. #region FindEntry (Public)
  573. /// <summary>
  574. /// Return the index of the entry with a matching name
  575. /// </summary>
  576. /// <param name="searchName">Entry name to find</param>
  577. /// <param name="ignoreCase">If true the comparison is case insensitive
  578. /// </param>
  579. /// <returns>The index position of the matching entry or -1 if not found
  580. /// </returns>
  581. /// <exception cref="InvalidOperationException">
  582. /// The Zip file has been closed.
  583. /// </exception>
  584. public int FindEntry(string searchName, bool ignoreCase)
  585. {
  586. if (entries == null)
  587. {
  588. throw new InvalidOperationException("ZipFile has been closed");
  589. } // if
  590. for (int index = 0; index < entries.Length; index++)
  591. {
  592. if (string.Compare(searchName, entries[index].Name, ignoreCase) == 0)
  593. {
  594. return index;
  595. } // if
  596. } // for
  597. return MathHelper.InvalidIndex;
  598. }
  599. #endregion
  600. #region GetEntry (Public)
  601. /// <summary>
  602. /// Searches for a zip entry in this archive with the given name.
  603. /// String comparisons are case insensitive
  604. /// </summary>
  605. /// <param name="searchName">
  606. /// The name to find. May contain directory components separated by
  607. /// slashes ('/').
  608. /// </param>
  609. /// <returns>
  610. /// The zip entry, or null if no entry with that name exists.
  611. /// </returns>
  612. /// <exception cref="InvalidOperationException">
  613. /// The Zip file has been closed.
  614. /// </exception>
  615. public ZipEntry GetEntry(string searchName)
  616. {
  617. if (entries == null)
  618. {
  619. throw new InvalidOperationException("ZipFile has been closed");
  620. } // if
  621. int index = FindEntry(searchName, true);
  622. return (index >= 0)
  623. ? entries[index].Clone()
  624. : null;
  625. }
  626. #endregion
  627. #region TestArchive (Public)
  628. /// <summary>
  629. /// Test an archive for integrity/validity
  630. /// </summary>
  631. /// <param name="testData">Perform low level data Crc check</param>
  632. /// <returns>true if the test passes, false otherwise</returns>
  633. public bool TestArchive(bool testData)
  634. {
  635. bool result = true;
  636. try
  637. {
  638. for (int index = 0; index < entries.Length; ++index)
  639. {
  640. long localOffset = CheckLocalHeader(this[index], true, true);
  641. if (testData)
  642. {
  643. Stream entryStream = GetInputStream(index);
  644. // Note: Test events for updating info, recording errors etc
  645. Crc32 crc = new Crc32();
  646. byte[] buffer = new byte[4096];
  647. int bytesRead;
  648. while ((bytesRead =
  649. entryStream.Read(buffer, 0, buffer.Length)) > 0)
  650. {
  651. crc.Update(buffer, 0, bytesRead);
  652. } // while
  653. if (this[index].Crc != crc.Value)
  654. {
  655. result = false;
  656. Log.Warning(
  657. "Crc failed, expected " + this[index].Crc +
  658. ", but calculated " + crc.Value);
  659. break;
  660. } // if
  661. } // if
  662. } // for
  663. } // try
  664. catch
  665. {
  666. result = false;
  667. } // catch
  668. return result;
  669. }
  670. #endregion
  671. #region GetInputStream (Public)
  672. /// <summary>
  673. /// Creates an input stream reading a zip entry
  674. /// </summary>
  675. /// <param name="entryIndex">The index of the entry to obtain an input
  676. /// stream for.</param>
  677. /// <returns>
  678. /// An input stream.
  679. /// </returns>
  680. /// <exception cref="InvalidOperationException">
  681. /// The ZipFile has already been closed
  682. /// </exception>
  683. /// <exception cref="Delta.Utilities.Compression.ZipException">
  684. /// The compression method for the entry is unknown
  685. /// </exception>
  686. /// <exception cref="IndexOutOfRangeException">
  687. /// The entry is not found in the ZipFile
  688. /// </exception>
  689. public Stream GetInputStream(int entryIndex)
  690. {
  691. if (entries == null)
  692. {
  693. throw new InvalidOperationException("ZipFile has closed");
  694. } // if
  695. long start = CheckLocalHeader(entries[entryIndex]);
  696. CompressionMethod method = entries[entryIndex].CompressionMethod;
  697. Stream istr = new PartialInputStream(
  698. baseStream, start, entries[entryIndex].CompressedSize);
  699. if (entries[entryIndex].IsCrypted)
  700. {
  701. istr = CreateAndInitDecryptionStream(istr, entries[entryIndex]);
  702. if (istr == null)
  703. {
  704. throw new ZipException("Unable to decrypt this entry");
  705. } // if
  706. } // if
  707. switch (method)
  708. {
  709. case CompressionMethod.Stored:
  710. return istr;
  711. case CompressionMethod.Deflated:
  712. return new InflaterInputStream(istr, new Inflater(true));
  713. default:
  714. throw new ZipException(
  715. "Unsupported compression method " + method);
  716. } // switch
  717. }
  718. #endregion
  719. #region Append (Public)
  720. public void Append(string filePath, string entryName = null)
  721. {
  722. if (FileHelper.Exists(filePath) == false)
  723. {
  724. throw new FileNotFoundException("Unable to add file '" + filePath +
  725. "' to zip '" + this +
  726. "' because the file was not found!");
  727. }
  728. if (zipOutput == null)
  729. {
  730. throw new InvalidOperationException("ZipFile has closed or was not " +
  731. "created for adding zip entries!");
  732. }
  733. if (entryName == null)
  734. {
  735. entryName = FileHelper.TryToUseRelativePath(filePath);
  736. }
  737. try
  738. {
  739. Stream fs = new FileStream(filePath, FileMode.Open,
  740. FileAccess.Read, FileShare.Read);
  741. byte[] buffer = new byte[fs.Length];
  742. fs.Read(buffer, 0, buffer.Length);
  743. fs.Close();
  744. ZipEntry newEntry = new ZipEntry(entryName);
  745. zipOutput.PutNextEntry(newEntry);
  746. zipOutput.Write(buffer, 0, buffer.Length);
  747. } // try
  748. catch (Exception ex)
  749. {
  750. Log.Warning("Could not save file '" + filePath +
  751. "' into zip (entryName=" + entryName + "): " + ex);
  752. } // catch
  753. }
  754. #endregion
  755. #region Methods (Private)
  756. #region OnKeysRequired
  757. /// <summary>
  758. /// Handles getting of encryption keys when required.
  759. /// </summary>
  760. /// <param name="fileName">The file for which encryptino keys are required.
  761. /// </param>
  762. private void OnKeysRequired(string fileName)
  763. {
  764. if (KeysRequired != null)
  765. {
  766. KeysRequiredEventArgs krea = new KeysRequiredEventArgs(fileName, key);
  767. KeysRequired(this, krea);
  768. key = krea.GetKey();
  769. }
  770. }
  771. #endregion
  772. #region ReadEntries
  773. /// <summary>
  774. /// Search for and read the central directory of a zip file filling the
  775. /// entries array. This is called exactly once by the constructors.
  776. /// </summary>
  777. /// <exception cref="System.IO.IOException">
  778. /// An i/o error occurs.
  779. /// </exception>
  780. /// <exception cref="Delta.Utilities.Compression.ZipException">
  781. /// The central directory is malformed or cannot be found
  782. /// </exception>
  783. private void ReadEntries()
  784. {
  785. // Search for the End Of Central Directory. When a zip comment is
  786. // present the directory may start earlier.
  787. //
  788. // Note: The search is limited to 64K which is the maximum size of a
  789. // trailing comment field to aid speed. This should be compatible with
  790. // both SFX and ZIP files but has only been tested for Zip files.
  791. // Need to confirm this is valid in all cases.
  792. // Could also speed this up by reading memory in larger blocks.
  793. if (baseStream.CanSeek == false)
  794. {
  795. throw new ZipException("ZipFile stream must be seekable");
  796. } // if
  797. long locatedCentralDirOffset = LocateBlockWithSignature(
  798. ZipConstants.EndSig, baseStream.Length,
  799. ZipConstants.EndHeader, 0xffff);
  800. if (locatedCentralDirOffset < 0)
  801. {
  802. throw new ZipException("Cannot find central directory");
  803. } // if
  804. //unused: int thisDiskNumber =
  805. ReadLeShort();
  806. //unused: int startCentralDirDisk =
  807. ReadLeShort();
  808. int entriesForThisDisk = ReadLeShort();
  809. int entriesForWholeCentralDir = ReadLeShort();
  810. int centralDirSize = ReadLeInt();
  811. int offsetOfCentralDir = ReadLeInt();
  812. int commentSize = ReadLeShort();
  813. byte[] zipComment = new byte[commentSize];
  814. baseStream.Read(zipComment, 0, zipComment.Length);
  815. comment = ZipConstants.ConvertToString(zipComment);
  816. /* Its seems possible that this is too strict, more digging required.
  817. if (thisDiskNumber != 0 ||
  818. startCentralDirDisk != 0 ||
  819. entriesForThisDisk != entriesForWholeCentralDir)
  820. {
  821. throw new ZipException("Spanned archives are not currently handled");
  822. }
  823. */
  824. entries = new ZipEntry[entriesForWholeCentralDir];
  825. // SFX support, find the offset of the first entry vis the start of the
  826. // stream. This applies to Zip files that are appended to the end of the
  827. // SFX stub. Zip files created by some archivers have the offsets altered
  828. // to reflect the true offsets and so dont require any adjustment here...
  829. if (offsetOfCentralDir < locatedCentralDirOffset - (4 + centralDirSize))
  830. {
  831. offsetOfFirstEntry = locatedCentralDirOffset -
  832. (4 + centralDirSize + offsetOfCentralDir);
  833. if (offsetOfFirstEntry <= 0)
  834. {
  835. throw new ZipException("Invalid SFX file");
  836. } // if
  837. } // if
  838. baseStream.Seek(offsetOfFirstEntry + offsetOfCentralDir,
  839. SeekOrigin.Begin);
  840. for (int entryId = 0; entryId < entriesForThisDisk; entryId++)
  841. {
  842. if (ReadLeInt() !=
  843. ZipConstants.CentralDirectorySig)
  844. {
  845. throw new ZipException("Wrong Central Directory signature");
  846. } // if
  847. int hostSystemId = ReadLeShort();
  848. int entryVersion = ReadLeShort();
  849. int bitFlags = ReadLeShort();
  850. int method = ReadLeShort();
  851. int dostime = ReadLeInt();
  852. int crc = ReadLeInt();
  853. int csize = ReadLeInt();
  854. int size = ReadLeInt();
  855. int nameLength = ReadLeShort();
  856. int extraLength = ReadLeShort();
  857. int commentLength = ReadLeShort();
  858. // Not currently used
  859. //unused: int diskStartNo =
  860. ReadLeShort();
  861. // Not currently used
  862. //unused: int internalAttributes =
  863. ReadLeShort();
  864. int externalAttributes = ReadLeInt();
  865. int offset = ReadLeInt();
  866. byte[] buffer = new byte[Math.Max(nameLength, commentLength)];
  867. baseStream.Read(buffer, 0, nameLength);
  868. string entryName = ZipConstants.ConvertToString(buffer, nameLength);
  869. ZipEntry entry = new ZipEntry(entryName, entryVersion, hostSystemId)
  870. {
  871. CompressionMethod = (CompressionMethod)method,
  872. Crc = crc & 0xffffffffL,
  873. Size = size & 0xffffffffL,
  874. CompressedSize = csize & 0xffffffffL,
  875. Flags = bitFlags,
  876. DosTime = (uint)dostime
  877. };
  878. if (extraLength > 0)
  879. {
  880. byte[] extra = new byte[extraLength];
  881. baseStream.Read(extra, 0, extraLength);
  882. entry.SetExtraData(extra);
  883. } // if
  884. if (commentLength > 0)
  885. {
  886. baseStream.Read(buffer, 0, commentLength);
  887. entry.Comment = ZipConstants.ConvertToString(buffer, commentLength);
  888. } // if
  889. entry.ZipFileIndex = entryId;
  890. entry.Offset = offset;
  891. entry.ExternalFileAttributes = externalAttributes;
  892. entries[entryId] = entry;
  893. } // for
  894. }
  895. #endregion
  896. #region ReadLeShort
  897. /// <summary>
  898. /// Read an unsigned short in little endian byte order.
  899. /// </summary>
  900. /// <returns>Returns the value read.</returns>
  901. /// <exception cref="IOException">
  902. /// An i/o error occurs.
  903. /// </exception>
  904. /// <exception cref="EndOfStreamException">
  905. /// The file ends prematurely
  906. /// </exception>
  907. private int ReadLeShort()
  908. {
  909. return baseStream.ReadByte() | baseStream.ReadByte() << 8;
  910. }
  911. #endregion
  912. #region ReadLeInt
  913. /// <summary>
  914. /// Read an int in little endian byte order.
  915. /// </summary>
  916. /// <returns>Returns the value read.</returns>
  917. /// <exception cref="IOException">
  918. /// An i/o error occurs.
  919. /// </exception>
  920. /// <exception cref="System.IO.EndOfStreamException">
  921. /// The file ends prematurely
  922. /// </exception>
  923. private int ReadLeInt()
  924. {
  925. return ReadLeShort() | ReadLeShort() << 16;
  926. }
  927. #endregion
  928. #region LocateBlockWithSignature
  929. /// <summary>
  930. /// NOTE this returns the offset of the first byte after the signature.
  931. /// </summary>
  932. /// <param name="signature">Signature</param>
  933. /// <param name="endLocation">End location</param>
  934. /// <param name="minimumBlockSize">Minimum block size</param>
  935. /// <param name="maximumVariableData">Maximum variable data</param>
  936. /// <returns>Long</returns>
  937. private long LocateBlockWithSignature(int signature, long endLocation,
  938. int minimumBlockSize, int maximumVariableData)
  939. {
  940. long pos = endLocation - minimumBlockSize;
  941. if (pos < 0)
  942. {
  943. return MathHelper.InvalidIndex;
  944. } // if
  945. long giveUpMarker = Math.Max(pos - maximumVariableData, 0);
  946. // Note: this loop could be optimized for speed.
  947. do
  948. {
  949. if (pos < giveUpMarker)
  950. {
  951. return MathHelper.InvalidIndex;
  952. }
  953. baseStream.Seek(pos--, SeekOrigin.Begin);
  954. } while (ReadLeInt() != signature);
  955. return baseStream.Position;
  956. }
  957. #endregion
  958. #region CheckLocalHeader
  959. /// <summary>
  960. /// Checks, if the local header of the entry at index i matches the
  961. /// central directory, and returns the offset to the data.
  962. /// </summary>
  963. /// <param name="entry">
  964. /// The entry to test against
  965. /// </param>
  966. /// <param name="fullTest">
  967. /// False by default. If set to true, the check will be extremely picky
  968. /// about the testing, otherwise be relaxed.
  969. /// </param>
  970. /// <param name="extractTest">
  971. /// Apply extra testing to see if the entry can be extracted by the
  972. /// library. True by default.
  973. /// </param>
  974. /// <returns>
  975. /// The start offset of the (compressed) data.
  976. /// </returns>
  977. /// <exception cref="System.IO.EndOfStreamException">
  978. /// The stream ends prematurely
  979. /// </exception>
  980. /// <exception cref="Delta.Utilities.Compression.ZipException">
  981. /// The local header signature is invalid, the entry and central header
  982. /// file name lengths are different or the local and entry compression
  983. /// methods dont match.
  984. /// </exception>
  985. private long CheckLocalHeader(ZipEntry entry, bool fullTest = false,
  986. bool extractTest = true)
  987. {
  988. baseStream.Seek(offsetOfFirstEntry + entry.Offset, SeekOrigin.Begin);
  989. if (ReadLeInt() !=
  990. ZipConstants.LocalSignature)
  991. {
  992. throw new ZipException(
  993. "Wrong local header signature");
  994. } // if
  995. // version required to extract
  996. short shortValue = (short)ReadLeShort();
  997. if (extractTest && shortValue > ZipConstants.VersionMadeBy)
  998. {
  999. throw new ZipException(
  1000. "Version required to extract this entry not supported (" +
  1001. shortValue + ")");
  1002. } // if
  1003. // general purpose bit flags.
  1004. short localFlags = (short)ReadLeShort();
  1005. if (extractTest)
  1006. {
  1007. if ((localFlags &
  1008. (int)
  1009. (GeneralBitFlags.Patched |
  1010. GeneralBitFlags.StrongEncryption |
  1011. GeneralBitFlags.EnhancedCompress |
  1012. GeneralBitFlags.HeaderMasked)) != 0)
  1013. {
  1014. throw new ZipException(
  1015. "The library doesnt support the zip version required to " +
  1016. "extract this entry");
  1017. } // if
  1018. } // if
  1019. if (localFlags != entry.Flags)
  1020. {
  1021. throw new ZipException(
  1022. "Central header/local header flags mismatch");
  1023. } // if
  1024. if (entry.CompressionMethod !=
  1025. (CompressionMethod)ReadLeShort())
  1026. {
  1027. throw new ZipException(
  1028. "Central header/local header compression method mismatch");
  1029. } // if
  1030. // file time
  1031. shortValue = (short)ReadLeShort();
  1032. // file date
  1033. shortValue = (short)ReadLeShort();
  1034. // Crc
  1035. int intValue = ReadLeInt();
  1036. if (fullTest)
  1037. {
  1038. if ((localFlags &
  1039. (int)GeneralBitFlags.Descriptor) == 0)
  1040. {
  1041. if (intValue != (int)entry.Crc)
  1042. {
  1043. throw new ZipException(
  1044. "Central header/local header crc mismatch");
  1045. }
  1046. } // if
  1047. } // if
  1048. // compressed Size
  1049. intValue = ReadLeInt();
  1050. // uncompressed size
  1051. intValue = ReadLeInt();
  1052. // Note: make test more correct... can't compare lengths as was done
  1053. // originally as this can fail for MBCS strings. Assuming a code page
  1054. // at this point is not valid? Best is to store the name length in
  1055. // the ZipEntry probably.
  1056. int storedNameLength = ReadLeShort();
  1057. if (entry.Name.Length > storedNameLength)
  1058. {
  1059. throw new ZipException("file name length mismatch");
  1060. } // if
  1061. int extraLen = storedNameLength + ReadLeShort();
  1062. return
  1063. offsetOfFirstEntry + entry.Offset + ZipConstants.LocalHeader +
  1064. extraLen;
  1065. }
  1066. #endregion
  1067. #region ReadFully
  1068. /// <summary>
  1069. /// Refactor this, its done elsewhere as well
  1070. /// </summary>
  1071. /// <param name="stream">S</param>
  1072. /// <param name="outBuf">Out buf</param>
  1073. private void ReadFully(Stream stream, byte[] outBuf)
  1074. {
  1075. int offset = 0;
  1076. int length = outBuf.Length;
  1077. while (length > 0)
  1078. {
  1079. int count = stream.Read(outBuf, offset, length);
  1080. if (count <= 0)
  1081. {
  1082. throw new ZipException("Unexpected EOF");
  1083. }
  1084. offset += count;
  1085. length -= count;
  1086. }
  1087. }
  1088. #endregion
  1089. #region CheckClassicPassword
  1090. /// <summary>
  1091. /// Check classic password
  1092. /// </summary>
  1093. /// <param name="classicCryptoStream">Classic crypto stream</param>
  1094. /// <param name="entry">Entry</param>
  1095. private void CheckClassicPassword(CryptoStream classicCryptoStream,
  1096. ZipEntry entry)
  1097. {
  1098. byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize];
  1099. ReadFully(classicCryptoStream, cryptbuffer);
  1100. if ((entry.Flags & (int)GeneralBitFlags.Descriptor) == 0)
  1101. {
  1102. if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] !=
  1103. (byte)(entry.Crc >> 24))
  1104. {
  1105. throw new ZipException("Invalid password");
  1106. } // if
  1107. } // if
  1108. else
  1109. {
  1110. if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] !=
  1111. (byte)((entry.DosTime >> 8) & 0xff))
  1112. {
  1113. throw new ZipException("Invalid password");
  1114. } // if
  1115. } // else
  1116. }
  1117. #endregion
  1118. #region CreateAndInitDecryptionStream
  1119. /// <summary>
  1120. /// Create and init decryption stream
  1121. /// </summary>
  1122. /// <param name="setBaseStream">Base stream</param>
  1123. /// <param name="entry">Entry</param>
  1124. /// <returns>Stream</returns>
  1125. private Stream CreateAndInitDecryptionStream(Stream setBaseStream,
  1126. ZipEntry entry)
  1127. {
  1128. CryptoStream result = null;
  1129. if (entry.Version < ZipConstants.VersionStrongEncryption ||
  1130. (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0)
  1131. {
  1132. ZipEncryptionManaged classicManaged = new ZipEncryptionManaged();
  1133. OnKeysRequired(entry.Name);
  1134. if (HaveKeys == false)
  1135. {
  1136. throw new ZipException(
  1137. "No password available for encrypted stream");
  1138. } // if
  1139. result = new CryptoStream(setBaseStream,
  1140. classicManaged.CreateDecryptor(key, null), CryptoStreamMode.Read);
  1141. CheckClassicPassword(result, entry);
  1142. } // if
  1143. else
  1144. {
  1145. throw new ZipException(
  1146. "Decryption method not supported");
  1147. } // else
  1148. return result;
  1149. }
  1150. #endregion
  1151. #region WriteEncryptionHeader
  1152. /// <summary>
  1153. /// Write encryption header
  1154. /// </summary>
  1155. /// <param name="stream">Stream</param>
  1156. /// <param name="crcValue">Crc value</param>
  1157. private void WriteEncryptionHeader(Stream stream, long crcValue)
  1158. {
  1159. byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize];
  1160. Random rnd = new Random();
  1161. rnd.NextBytes(cryptBuffer);
  1162. cryptBuffer[11] = (byte)(crcValue >> 24);
  1163. stream.Write(cryptBuffer, 0, cryptBuffer.Length);
  1164. }
  1165. #endregion
  1166. #region CreateAndInitEncryptionStream
  1167. /// <summary>
  1168. /// Create and init encryption stream
  1169. /// </summary>
  1170. /// <param name="setBaseStream">Base stream</param>
  1171. /// <param name="entry">Entry</param>
  1172. /// <returns>Stream</returns>
  1173. private Stream CreateAndInitEncryptionStream(Stream setBaseStream,
  1174. ZipEntry entry)
  1175. {
  1176. CryptoStream result = null;
  1177. if (entry.Version < ZipConstants.VersionStrongEncryption ||
  1178. (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0)
  1179. {
  1180. ZipEncryptionManaged classicManaged = new ZipEncryptionManaged();
  1181. OnKeysRequired(entry.Name);
  1182. if (HaveKeys == false)
  1183. {
  1184. throw new ZipException(
  1185. "No password available for encrypted stream");
  1186. } // if
  1187. result = new CryptoStream(setBaseStream,
  1188. classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write);
  1189. if (entry.Crc < 0 ||
  1190. (entry.Flags & 8) != 0)
  1191. {
  1192. WriteEncryptionHeader(result, entry.DosTime << 16);
  1193. } // if
  1194. else
  1195. {
  1196. WriteEncryptionHeader(result, entry.Crc);
  1197. } // else
  1198. } // if
  1199. return result;
  1200. }
  1201. #endregion
  1202. #region GetOutputStream
  1203. /// <summary>
  1204. /// Gets an output stream for the specified <see cref="ZipEntry"/>
  1205. /// </summary>
  1206. /// <param name="entry">The entry to get an output stream for.</param>
  1207. /// <param name="fileName"></param>
  1208. /// <returns>The output stream obtained for the entry.</returns>
  1209. private Stream GetOutputStream(ZipEntry entry, string fileName)
  1210. {
  1211. baseStream.Seek(0, SeekOrigin.End);
  1212. Stream result = File.OpenWrite(fileName);
  1213. if (entry.IsCrypted)
  1214. {
  1215. result = CreateAndInitEncryptionStream(result, entry);
  1216. }
  1217. switch (entry.CompressionMethod)
  1218. {
  1219. case CompressionMethod.Stored:
  1220. break;
  1221. case CompressionMethod.Deflated:
  1222. result = new DeflaterOutputStream(result);
  1223. break;
  1224. default:
  1225. throw new ZipException(
  1226. "Unknown compression method " + entry.CompressionMethod);
  1227. } // switch
  1228. return result;
  1229. }
  1230. #endregion
  1231. #region GetInputStream
  1232. /// <summary>
  1233. /// Creates an input stream reading the given zip entry as
  1234. /// uncompressed data. Normally zip entry should be an entry
  1235. /// returned by GetEntry().
  1236. /// </summary>
  1237. /// <returns>
  1238. /// the input stream.
  1239. /// </returns>
  1240. /// <exception cref="InvalidOperationException">
  1241. /// The ZipFile has already been closed
  1242. /// </exception>
  1243. /// <exception cref="Delta.Utilities.Compression.ZipException">
  1244. /// The compression method for the entry is unknown
  1245. /// </exception>
  1246. /// <exception cref="IndexOutOfRangeException">
  1247. /// The entry is not found in the ZipFile
  1248. /// </exception>
  1249. private Stream GetInputStream(ZipEntry entry)
  1250. {
  1251. if (entries == null)
  1252. {
  1253. throw new InvalidOperationException("ZipFile has closed");
  1254. } // if
  1255. int index = entry.ZipFileIndex;
  1256. if (index < 0 ||
  1257. index >= entries.Length ||
  1258. entries[index].Name != entry.Name)
  1259. {
  1260. index = FindEntry(entry.Name, true);
  1261. if (index < 0)
  1262. {
  1263. throw new IndexOutOfRangeException();
  1264. } // if
  1265. } // if
  1266. return GetInputStream(index);
  1267. }
  1268. #endregion
  1269. #endregion
  1270. }
  1271. }