ZipWriter.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using SharpCompress.Common;
  6. using SharpCompress.Common.Zip;
  7. using SharpCompress.Common.Zip.Headers;
  8. using SharpCompress.Compressors;
  9. using SharpCompress.Compressors.BZip2;
  10. using SharpCompress.Compressors.Deflate;
  11. using SharpCompress.Compressors.LZMA;
  12. using SharpCompress.Compressors.PPMd;
  13. using SharpCompress.Converters;
  14. using SharpCompress.IO;
  15. namespace SharpCompress.Writers.Zip
  16. {
  17. public class ZipWriter : AbstractWriter
  18. {
  19. private readonly CompressionType compressionType;
  20. private readonly CompressionLevel compressionLevel;
  21. private readonly List<ZipCentralDirectoryEntry> entries = new List<ZipCentralDirectoryEntry>();
  22. private readonly string zipComment;
  23. private long streamPosition;
  24. private PpmdProperties ppmdProps;
  25. private readonly bool isZip64;
  26. public ZipWriter(Stream destination, ZipWriterOptions zipWriterOptions)
  27. : base(ArchiveType.Zip, zipWriterOptions)
  28. {
  29. zipComment = zipWriterOptions.ArchiveComment ?? string.Empty;
  30. isZip64 = zipWriterOptions.UseZip64;
  31. if (destination.CanSeek)
  32. {
  33. streamPosition = destination.Position;
  34. }
  35. compressionType = zipWriterOptions.CompressionType;
  36. compressionLevel = zipWriterOptions.DeflateCompressionLevel;
  37. if (WriterOptions.LeaveStreamOpen)
  38. {
  39. destination = new NonDisposingStream(destination);
  40. }
  41. InitalizeStream(destination);
  42. }
  43. private PpmdProperties PpmdProperties
  44. {
  45. get
  46. {
  47. if (ppmdProps == null)
  48. {
  49. ppmdProps = new PpmdProperties();
  50. }
  51. return ppmdProps;
  52. }
  53. }
  54. protected override void Dispose(bool isDisposing)
  55. {
  56. if (isDisposing)
  57. {
  58. ulong size = 0;
  59. foreach (ZipCentralDirectoryEntry entry in entries)
  60. {
  61. size += entry.Write(OutputStream);
  62. }
  63. WriteEndRecord(size);
  64. }
  65. base.Dispose(isDisposing);
  66. }
  67. private static ZipCompressionMethod ToZipCompressionMethod(CompressionType compressionType)
  68. {
  69. switch (compressionType)
  70. {
  71. case CompressionType.None:
  72. {
  73. return ZipCompressionMethod.None;
  74. }
  75. case CompressionType.Deflate:
  76. {
  77. return ZipCompressionMethod.Deflate;
  78. }
  79. case CompressionType.BZip2:
  80. {
  81. return ZipCompressionMethod.BZip2;
  82. }
  83. case CompressionType.LZMA:
  84. {
  85. return ZipCompressionMethod.LZMA;
  86. }
  87. case CompressionType.PPMd:
  88. {
  89. return ZipCompressionMethod.PPMd;
  90. }
  91. default:
  92. throw new InvalidFormatException("Invalid compression method: " + compressionType);
  93. }
  94. }
  95. public override void Write(string entryPath, Stream source, DateTime? modificationTime)
  96. {
  97. Write(entryPath, source, new ZipWriterEntryOptions()
  98. {
  99. ModificationDateTime = modificationTime
  100. });
  101. }
  102. public void Write(string entryPath, Stream source, ZipWriterEntryOptions zipWriterEntryOptions)
  103. {
  104. using (Stream output = WriteToStream(entryPath, zipWriterEntryOptions))
  105. {
  106. source.TransferTo(output);
  107. }
  108. }
  109. public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
  110. {
  111. var compression = ToZipCompressionMethod(options.CompressionType ?? compressionType);
  112. entryPath = NormalizeFilename(entryPath);
  113. options.ModificationDateTime = options.ModificationDateTime ?? DateTime.Now;
  114. options.EntryComment = options.EntryComment ?? string.Empty;
  115. var entry = new ZipCentralDirectoryEntry(compression, entryPath, (ulong)streamPosition, WriterOptions.ArchiveEncoding)
  116. {
  117. Comment = options.EntryComment,
  118. ModificationTime = options.ModificationDateTime
  119. };
  120. // Use the archive default setting for zip64 and allow overrides
  121. var useZip64 = isZip64;
  122. if (options.EnableZip64.HasValue)
  123. useZip64 = options.EnableZip64.Value;
  124. var headersize = (uint)WriteHeader(entryPath, options, entry, useZip64);
  125. streamPosition += headersize;
  126. return new ZipWritingStream(this, OutputStream, entry, compression,
  127. options.DeflateCompressionLevel ?? compressionLevel);
  128. }
  129. private string NormalizeFilename(string filename)
  130. {
  131. filename = filename.Replace('\\', '/');
  132. int pos = filename.IndexOf(':');
  133. if (pos >= 0)
  134. {
  135. filename = filename.Remove(0, pos + 1);
  136. }
  137. return filename.Trim('/');
  138. }
  139. private int WriteHeader(string filename, ZipWriterEntryOptions zipWriterEntryOptions, ZipCentralDirectoryEntry entry, bool useZip64)
  140. {
  141. // We err on the side of caution until the zip specification clarifies how to support this
  142. if (!OutputStream.CanSeek && useZip64)
  143. throw new NotSupportedException("Zip64 extensions are not supported on non-seekable streams");
  144. var explicitZipCompressionInfo = ToZipCompressionMethod(zipWriterEntryOptions.CompressionType ?? compressionType);
  145. byte[] encodedFilename = WriterOptions.ArchiveEncoding.Encode(filename);
  146. OutputStream.Write(DataConverter.LittleEndian.GetBytes(ZipHeaderFactory.ENTRY_HEADER_BYTES), 0, 4);
  147. if (explicitZipCompressionInfo == ZipCompressionMethod.Deflate)
  148. {
  149. if (OutputStream.CanSeek && useZip64)
  150. OutputStream.Write(new byte[] { 45, 0 }, 0, 2); //smallest allowed version for zip64
  151. else
  152. OutputStream.Write(new byte[] { 20, 0 }, 0, 2); //older version which is more compatible
  153. }
  154. else
  155. {
  156. OutputStream.Write(new byte[] { 63, 0 }, 0, 2); //version says we used PPMd or LZMA
  157. }
  158. HeaderFlags flags = Equals(WriterOptions.ArchiveEncoding.GetEncoding(), Encoding.UTF8) ? HeaderFlags.Efs : 0;
  159. if (!OutputStream.CanSeek)
  160. {
  161. flags |= HeaderFlags.UsePostDataDescriptor;
  162. if (explicitZipCompressionInfo == ZipCompressionMethod.LZMA)
  163. {
  164. flags |= HeaderFlags.Bit1; // eos marker
  165. }
  166. }
  167. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)flags), 0, 2);
  168. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)explicitZipCompressionInfo), 0, 2); // zipping method
  169. OutputStream.Write(DataConverter.LittleEndian.GetBytes(zipWriterEntryOptions.ModificationDateTime.DateTimeToDosTime()), 0, 4);
  170. // zipping date and time
  171. OutputStream.Write(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0, 12);
  172. // unused CRC, un/compressed size, updated later
  173. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)encodedFilename.Length), 0, 2); // filename length
  174. var extralength = 0;
  175. if (OutputStream.CanSeek && useZip64)
  176. extralength = 2 + 2 + 8 + 8;
  177. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)extralength), 0, 2); // extra length
  178. OutputStream.Write(encodedFilename, 0, encodedFilename.Length);
  179. if (extralength != 0)
  180. {
  181. OutputStream.Write(new byte[extralength], 0, extralength); // reserve space for zip64 data
  182. entry.Zip64HeaderOffset = (ushort)(6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length);
  183. }
  184. return 6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length + extralength;
  185. }
  186. private void WriteFooter(uint crc, uint compressed, uint uncompressed)
  187. {
  188. OutputStream.Write(DataConverter.LittleEndian.GetBytes(crc), 0, 4);
  189. OutputStream.Write(DataConverter.LittleEndian.GetBytes(compressed), 0, 4);
  190. OutputStream.Write(DataConverter.LittleEndian.GetBytes(uncompressed), 0, 4);
  191. }
  192. private void WriteEndRecord(ulong size)
  193. {
  194. byte[] encodedComment = WriterOptions.ArchiveEncoding.Encode(zipComment);
  195. var zip64 = isZip64 || entries.Count > ushort.MaxValue || streamPosition >= uint.MaxValue || size >= uint.MaxValue;
  196. var sizevalue = size >= uint.MaxValue ? uint.MaxValue : (uint)size;
  197. var streampositionvalue = streamPosition >= uint.MaxValue ? uint.MaxValue : (uint)streamPosition;
  198. if (zip64)
  199. {
  200. var recordlen = 2 + 2 + 4 + 4 + 8 + 8 + 8 + 8;
  201. // Write zip64 end of central directory record
  202. OutputStream.Write(new byte[] { 80, 75, 6, 6 }, 0, 4);
  203. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ulong)recordlen), 0, 8); // Size of zip64 end of central directory record
  204. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)0), 0, 2); // Made by
  205. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)45), 0, 2); // Version needed
  206. OutputStream.Write(DataConverter.LittleEndian.GetBytes((uint)0), 0, 4); // Disk number
  207. OutputStream.Write(DataConverter.LittleEndian.GetBytes((uint)0), 0, 4); // Central dir disk
  208. // TODO: entries.Count is int, so max 2^31 files
  209. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ulong)entries.Count), 0, 8); // Entries in this disk
  210. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ulong)entries.Count), 0, 8); // Total entries
  211. OutputStream.Write(DataConverter.LittleEndian.GetBytes(size), 0, 8); // Central Directory size
  212. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ulong)streamPosition), 0, 8); // Disk offset
  213. // Write zip64 end of central directory locator
  214. OutputStream.Write(new byte[] { 80, 75, 6, 7 }, 0, 4);
  215. OutputStream.Write(DataConverter.LittleEndian.GetBytes(0uL), 0, 4); // Entry disk
  216. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ulong)streamPosition + size), 0, 8); // Offset to the zip64 central directory
  217. OutputStream.Write(DataConverter.LittleEndian.GetBytes(0u), 0, 4); // Number of disks
  218. streamPosition += recordlen + (4 + 4 + 8 + 4);
  219. streampositionvalue = streamPosition >= uint.MaxValue ? uint.MaxValue : (uint)streampositionvalue;
  220. }
  221. // Write normal end of central directory record
  222. OutputStream.Write(new byte[] { 80, 75, 5, 6, 0, 0, 0, 0 }, 0, 8);
  223. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)entries.Count), 0, 2);
  224. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)entries.Count), 0, 2);
  225. OutputStream.Write(DataConverter.LittleEndian.GetBytes(sizevalue), 0, 4);
  226. OutputStream.Write(DataConverter.LittleEndian.GetBytes((uint)streampositionvalue), 0, 4);
  227. OutputStream.Write(DataConverter.LittleEndian.GetBytes((ushort)encodedComment.Length), 0, 2);
  228. OutputStream.Write(encodedComment, 0, encodedComment.Length);
  229. }
  230. #region Nested type: ZipWritingStream
  231. internal class ZipWritingStream : Stream
  232. {
  233. private readonly CRC32 crc = new CRC32();
  234. private readonly ZipCentralDirectoryEntry entry;
  235. private readonly Stream originalStream;
  236. private readonly Stream writeStream;
  237. private readonly ZipWriter writer;
  238. private readonly ZipCompressionMethod zipCompressionMethod;
  239. private readonly CompressionLevel compressionLevel;
  240. private CountingWritableSubStream counting;
  241. private ulong decompressed;
  242. // Flag to prevent throwing exceptions on Dispose
  243. private bool limitsExceeded;
  244. private bool isDisposed;
  245. internal ZipWritingStream(ZipWriter writer, Stream originalStream, ZipCentralDirectoryEntry entry,
  246. ZipCompressionMethod zipCompressionMethod, CompressionLevel compressionLevel)
  247. {
  248. this.writer = writer;
  249. this.originalStream = originalStream;
  250. this.writer = writer;
  251. this.entry = entry;
  252. this.zipCompressionMethod = zipCompressionMethod;
  253. this.compressionLevel = compressionLevel;
  254. writeStream = GetWriteStream(originalStream);
  255. }
  256. public override bool CanRead => false;
  257. public override bool CanSeek => false;
  258. public override bool CanWrite => true;
  259. public override long Length => throw new NotSupportedException();
  260. public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
  261. private Stream GetWriteStream(Stream writeStream)
  262. {
  263. counting = new CountingWritableSubStream(writeStream);
  264. Stream output = counting;
  265. switch (zipCompressionMethod)
  266. {
  267. case ZipCompressionMethod.None:
  268. {
  269. return output;
  270. }
  271. case ZipCompressionMethod.Deflate:
  272. {
  273. return new DeflateStream(counting, CompressionMode.Compress, compressionLevel);
  274. }
  275. case ZipCompressionMethod.BZip2:
  276. {
  277. return new BZip2Stream(counting, CompressionMode.Compress, false);
  278. }
  279. case ZipCompressionMethod.LZMA:
  280. {
  281. counting.WriteByte(9);
  282. counting.WriteByte(20);
  283. counting.WriteByte(5);
  284. counting.WriteByte(0);
  285. LzmaStream lzmaStream = new LzmaStream(new LzmaEncoderProperties(!originalStream.CanSeek),
  286. false, counting);
  287. counting.Write(lzmaStream.Properties, 0, lzmaStream.Properties.Length);
  288. return lzmaStream;
  289. }
  290. case ZipCompressionMethod.PPMd:
  291. {
  292. counting.Write(writer.PpmdProperties.Properties, 0, 2);
  293. return new PpmdStream(writer.PpmdProperties, counting, true);
  294. }
  295. default:
  296. {
  297. throw new NotSupportedException("CompressionMethod: " + zipCompressionMethod);
  298. }
  299. }
  300. }
  301. protected override void Dispose(bool disposing)
  302. {
  303. if (isDisposed)
  304. {
  305. return;
  306. }
  307. isDisposed = true;
  308. base.Dispose(disposing);
  309. if (disposing)
  310. {
  311. writeStream.Dispose();
  312. if (limitsExceeded)
  313. {
  314. // We have written invalid data into the archive,
  315. // so we destroy it now, instead of allowing the user to continue
  316. // with a defunct archive
  317. originalStream.Dispose();
  318. return;
  319. }
  320. entry.Crc = (uint)crc.Crc32Result;
  321. entry.Compressed = counting.Count;
  322. entry.Decompressed = decompressed;
  323. var zip64 = entry.Compressed >= uint.MaxValue || entry.Decompressed >= uint.MaxValue;
  324. var compressedvalue = zip64 ? uint.MaxValue : (uint)counting.Count;
  325. var decompressedvalue = zip64 ? uint.MaxValue : (uint)entry.Decompressed;
  326. if (originalStream.CanSeek)
  327. {
  328. originalStream.Position = (long)(entry.HeaderOffset + 6);
  329. originalStream.WriteByte(0);
  330. originalStream.Position = (long)(entry.HeaderOffset + 14);
  331. writer.WriteFooter(entry.Crc, compressedvalue, decompressedvalue);
  332. // Ideally, we should not throw from Dispose()
  333. // We should not get here as the Write call checks the limits
  334. if (zip64 && entry.Zip64HeaderOffset == 0)
  335. throw new NotSupportedException("Attempted to write a stream that is larger than 4GiB without setting the zip64 option");
  336. // If we have pre-allocated space for zip64 data,
  337. // fill it out, even if it is not required
  338. if (entry.Zip64HeaderOffset != 0)
  339. {
  340. originalStream.Position = (long)(entry.HeaderOffset + entry.Zip64HeaderOffset);
  341. originalStream.Write(DataConverter.LittleEndian.GetBytes((ushort)0x0001), 0, 2);
  342. originalStream.Write(DataConverter.LittleEndian.GetBytes((ushort)(8 + 8)), 0, 2);
  343. originalStream.Write(DataConverter.LittleEndian.GetBytes(entry.Decompressed), 0, 8);
  344. originalStream.Write(DataConverter.LittleEndian.GetBytes(entry.Compressed), 0, 8);
  345. }
  346. originalStream.Position = writer.streamPosition + (long)entry.Compressed;
  347. writer.streamPosition += (long)entry.Compressed;
  348. }
  349. else
  350. {
  351. // We have a streaming archive, so we should add a post-data-descriptor,
  352. // but we cannot as it does not hold the zip64 values
  353. // Throwing an exception until the zip specification is clarified
  354. // Ideally, we should not throw from Dispose()
  355. // We should not get here as the Write call checks the limits
  356. if (zip64)
  357. throw new NotSupportedException("Streams larger than 4GiB are not supported for non-seekable streams");
  358. originalStream.Write(DataConverter.LittleEndian.GetBytes(ZipHeaderFactory.POST_DATA_DESCRIPTOR), 0, 4);
  359. writer.WriteFooter(entry.Crc,
  360. (uint)compressedvalue,
  361. (uint)decompressedvalue);
  362. writer.streamPosition += (long)entry.Compressed + 16;
  363. }
  364. writer.entries.Add(entry);
  365. }
  366. }
  367. public override void Flush()
  368. {
  369. writeStream.Flush();
  370. }
  371. public override int Read(byte[] buffer, int offset, int count)
  372. {
  373. throw new NotSupportedException();
  374. }
  375. public override long Seek(long offset, SeekOrigin origin)
  376. {
  377. throw new NotSupportedException();
  378. }
  379. public override void SetLength(long value)
  380. {
  381. throw new NotSupportedException();
  382. }
  383. public override void Write(byte[] buffer, int offset, int count)
  384. {
  385. // We check the limits first, because we can keep the archive consistent
  386. // if we can prevent the writes from happening
  387. if (entry.Zip64HeaderOffset == 0)
  388. {
  389. // Pre-check, the counting.Count is not exact, as we do not know the size before having actually compressed it
  390. if (limitsExceeded || ((decompressed + (uint)count) > uint.MaxValue) || (counting.Count + (uint)count) > uint.MaxValue)
  391. throw new NotSupportedException("Attempted to write a stream that is larger than 4GiB without setting the zip64 option");
  392. }
  393. decompressed += (uint)count;
  394. crc.SlurpBlock(buffer, offset, count);
  395. writeStream.Write(buffer, offset, count);
  396. if (entry.Zip64HeaderOffset == 0)
  397. {
  398. // Post-check, this is accurate
  399. if ((decompressed > uint.MaxValue) || counting.Count > uint.MaxValue)
  400. {
  401. // We have written the data, so the archive is now broken
  402. // Throwing the exception here, allows us to avoid
  403. // throwing an exception in Dispose() which is discouraged
  404. // as it can mask other errors
  405. limitsExceeded = true;
  406. throw new NotSupportedException("Attempted to write a stream that is larger than 4GiB without setting the zip64 option");
  407. }
  408. }
  409. }
  410. }
  411. #endregion Nested type: ZipWritingStream
  412. }
  413. }