3 // Copyright (C) 2001 Mike Krueger
4 // Copyright (C) 2004 John Reilly
6 // This file was translated from java, it was part of the GNU Classpath
7 // Copyright (C) 2001 Free Software Foundation, Inc.
9 // This program is free software; you can redistribute it and/or
10 // modify it under the terms of the GNU General Public License
11 // as published by the Free Software Foundation; either version 2
12 // of the License, or (at your option) any later version.
14 // This program is distributed in the hope that it will be useful,
15 // but WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 // GNU General Public License for more details.
19 // You should have received a copy of the GNU General Public License
20 // along with this program; if not, write to the Free Software
21 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23 // Linking this library statically or dynamically with other modules is
24 // making a combined work based on this library. Thus, the terms and
25 // conditions of the GNU General Public License cover the whole
28 // As a special exception, the copyright holders of this library give you
29 // permission to link this library with independent modules to produce an
30 // executable, regardless of the license terms of these independent
31 // modules, and to copy and distribute the resulting executable under
32 // terms of your choice, provided that you also meet, for each linked
33 // independent module, the terms and conditions of the license of that
34 // module. An independent module is a module which is not derived from
35 // or based on this library. If you modify this library, you may extend
36 // this exception to your version of the library, but you are not
37 // obligated to do so. If you do not wish to do so, delete this
38 // exception statement from your version.
41 using System.Collections.Generic;
43 using ICSharpCode.SharpZipLib.Silverlight.Checksums;
44 using ICSharpCode.SharpZipLib.Silverlight.Zip.Compression;
45 using ICSharpCode.SharpZipLib.Silverlight.Zip.Compression.Streams;
46 using ICSharpCode.SharpZipLib.Zip;
48 namespace ICSharpCode.SharpZipLib.Silverlight.Zip
51 /// This is a DeflaterOutputStream that writes the files into a zip
52 /// archive one after another. It has a special method to start a new
53 /// zip entry. The zip entries contains information about the file name
54 /// size, compressed size, CRC, etc.
56 /// It includes support for Stored and Deflated entries.
57 /// This class is not thread safe.
59 /// <br/>Author of the original java version : Jochen Hoenicke
61 /// <example> This sample shows how to create a zip file
66 /// using ICSharpCode.SharpZipLib.Core;
67 /// using ICSharpCode.SharpZipLib.Zip;
71 /// public static void Main(string[] args)
73 /// string[] filenames = Directory.GetFiles(args[0]);
74 /// byte[] buffer = new byte[4096];
76 /// using ( ZipOutputStream s = new ZipOutputStream(File.Create(args[1])) ) {
78 /// s.SetLevel(9); // 0 - store only to 9 - means best compression
80 /// foreach (string file in filenames) {
81 /// ZipEntry entry = new ZipEntry(file);
82 /// s.PutNextEntry(entry);
84 /// using (FileStream fs = File.OpenRead(file)) {
85 /// StreamUtils.Copy(fs, s, buffer);
93 public class ZipOutputStream : DeflaterOutputStream
98 /// Creates a new Zip output stream, writing a zip archive.
100 /// <param name="baseOutputStream">
101 /// The output stream to which the archive contents are written.
103 public ZipOutputStream(Stream baseOutputStream)
104 : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true))
111 /// Gets a flag value of true if the central header has been added for this archive; false if it has not been added.
113 /// <remarks>No further entries can be added once this has been done.</remarks>
114 public bool IsFinished
116 get { return entries == null; }
120 /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries.
122 /// <remarks>Older archivers may not understand Zip64 extensions.
123 /// If backwards compatability is an issue be careful when adding <see cref="ZipEntry.Size">entries</see> to an archive.
124 /// Setting this property to off is workable but less desirable as in those circumstances adding a file
125 /// larger then 4GB will fail.</remarks>
126 public UseZip64 UseZip64
128 get { return useZip64_; }
129 set { useZip64_ = value; }
133 /// Set the zip file comment.
135 /// <param name="comment">
136 /// The comment text for the entire archive.
138 /// <exception name ="ArgumentOutOfRangeException">
139 /// The converted comment is longer than 0xffff bytes.
141 public void SetComment(string comment)
143 // TODO: Its not yet clear how to handle unicode comments here.
144 var commentBytes = ZipConstants.ConvertToArray(comment);
145 if (commentBytes.Length > 0xffff)
147 throw new ArgumentOutOfRangeException("comment");
149 zipComment = commentBytes;
153 /// Sets the compression level. The new level will be activated
156 /// <param name="level">The new compression level (1 to 9).</param>
157 /// <exception cref="ArgumentOutOfRangeException">
158 /// Level specified is not supported.
160 /// <see cref="Deflater"/>
161 public void SetLevel(int level)
163 deflater_.SetLevel(level);
164 defaultCompressionLevel = level;
168 /// Get the current deflater compression level
170 /// <returns>The current compression level</returns>
171 public int GetLevel()
173 return deflater_.GetLevel();
177 /// Write an unsigned short in little endian byte order.
179 private void WriteLeShort(int value)
183 baseOutputStream_.WriteByte((byte) (value & 0xff));
184 baseOutputStream_.WriteByte((byte) ((value >> 8) & 0xff));
189 /// Write an int in little endian byte order.
191 private void WriteLeInt(int value)
196 WriteLeShort(value >> 16);
201 /// Write an int in little endian byte order.
203 private void WriteLeLong(long value)
207 WriteLeInt((int) value);
208 WriteLeInt((int) (value >> 32));
213 /// Starts a new Zip entry. It automatically closes the previous
214 /// entry if present.
215 /// All entry elements bar name are optional, but must be correct if present.
216 /// If the compression method is stored and the output is not patchable
217 /// the compression for that entry is automatically changed to deflate level 0
219 /// <param name="entry">
222 /// <exception cref="System.ArgumentNullException">
223 /// if entry passed is null.
225 /// <exception cref="System.IO.IOException">
226 /// if an I/O error occured.
228 /// <exception cref="System.InvalidOperationException">
229 /// if stream was finished
231 /// <exception cref="ZipException">
232 /// Too many entries in the Zip file<br/>
233 /// Entry name is too long<br/>
234 /// Finish has already been called<br/>
236 public void PutNextEntry(ZipEntry entry)
240 throw new ArgumentNullException("entry");
245 throw new InvalidOperationException("ZipOutputStream was finished");
248 if (curEntry != null)
253 if (entries.Count == int.MaxValue)
255 throw new ZipException("Too many entries for Zip file");
258 var method = entry.CompressionMethod;
259 var compressionLevel = defaultCompressionLevel;
261 // Clear flags that the library manages internally
262 entry.Flags &= (int) GeneralBitFlags.UnicodeText;
263 patchEntryHeader = false;
264 var headerInfoAvailable = true;
266 if (method == CompressionMethod.Stored)
268 // Cant store values in a data descriptor as you cant extract stored files
269 // if the length isnt known.
271 if (entry.CompressedSize >= 0)
275 entry.Size = entry.CompressedSize;
277 else if (entry.Size != entry.CompressedSize)
279 throw new ZipException("Method STORED, but compressed size != size");
286 entry.CompressedSize = entry.Size;
290 if (entry.Size < 0 || entry.Crc < 0)
294 headerInfoAvailable = false;
298 // Can't patch entries so storing is not possible.
299 method = CompressionMethod.Deflated;
300 compressionLevel = 0;
305 if (method == CompressionMethod.Deflated)
309 // No need to compress - no data.
310 entry.CompressedSize = entry.Size;
312 method = CompressionMethod.Stored;
314 else if ((entry.CompressedSize < 0) || (entry.Size < 0) || (entry.Crc < 0))
316 headerInfoAvailable = false;
320 if (headerInfoAvailable == false)
322 if (CanPatchEntries == false)
324 // Only way to record size and compressed size is to append a data descriptor
325 // after compressed data.
330 patchEntryHeader = true;
334 if (Password != null)
336 entry.IsCrypted = true;
339 // Need to append a data descriptor as the crc isnt available for use
340 // with encryption, the date is used instead. Setting the flag
341 // indicates this to the decompressor.
346 entry.Offset = _offset;
347 entry.CompressionMethod = method;
352 if ((useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic)))
357 // Write the local file header
358 WriteLeInt(ZipConstants.LocalHeaderSignature);
360 WriteLeShort(entry.Version);
361 WriteLeShort(entry.Flags);
362 WriteLeShort((byte) method);
363 WriteLeInt((int) entry.DosTime);
365 // TODO: Refactor header writing. Its done in several places.
366 if (headerInfoAvailable)
368 WriteLeInt((int) entry.Crc);
369 if (entry.LocalHeaderRequiresZip64)
376 WriteLeInt(entry.IsCrypted
377 ? (int) entry.CompressedSize + ZipConstants.CryptoHeaderSize
378 : (int) entry.CompressedSize);
379 WriteLeInt((int) entry.Size);
384 if (patchEntryHeader)
386 crcPatchPos = baseOutputStream_.Position;
388 WriteLeInt(0); // Crc
390 if (patchEntryHeader)
392 sizePatchPos = baseOutputStream_.Position;
395 // For local header both sizes appear in Zip64 Extended Information
396 if (entry.LocalHeaderRequiresZip64 && patchEntryHeader)
403 WriteLeInt(0); // Compressed size
404 WriteLeInt(0); // Uncompressed size
408 var name = ZipConstants.ConvertToArray(entry.Flags, entry.Name);
410 if (name.Length > 0xFFFF)
412 throw new ZipException("Entry name too long.");
415 var ed = new ZipExtraData(entry.ExtraData);
417 if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader))
420 if (headerInfoAvailable)
422 ed.AddLeLong(entry.Size);
423 ed.AddLeLong(entry.CompressedSize);
434 throw new ZipException("Internal error cant find extra data");
437 if (patchEntryHeader)
439 sizePatchPos = ed.CurrentReadIndex;
447 var extra = ed.GetEntryData();
449 WriteLeShort(name.Length);
450 WriteLeShort(extra.Length);
454 baseOutputStream_.Write(name, 0, name.Length);
457 if (entry.LocalHeaderRequiresZip64 && patchEntryHeader)
459 sizePatchPos += baseOutputStream_.Position;
462 if (extra.Length > 0)
464 baseOutputStream_.Write(extra, 0, extra.Length);
467 _offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length;
469 // Activate the entry.
472 if (method == CompressionMethod.Deflated)
475 deflater_.SetLevel(compressionLevel);
483 // so testing Zip will says its ok
484 WriteEncryptionHeader(entry.DosTime << 16);
488 WriteEncryptionHeader(entry.Crc);
494 /// Closes the current entry, updating header and footer information as required
496 /// <exception cref="System.IO.IOException">
497 /// An I/O error occurs.
499 /// <exception cref="System.InvalidOperationException">
500 /// No entry is active.
502 public void CloseEntry()
504 if (curEntry == null)
506 throw new InvalidOperationException("No open entry");
509 // First finish the deflater, if appropriate
510 if (curMethod == CompressionMethod.Deflated)
515 var csize = (curMethod == CompressionMethod.Deflated) ? deflater_.TotalOut : size;
517 if (curEntry.Size < 0)
519 curEntry.Size = size;
521 else if (curEntry.Size != size)
523 throw new ZipException("size was " + size + ", but I expected " + curEntry.Size);
526 if (curEntry.CompressedSize < 0)
528 curEntry.CompressedSize = csize;
530 else if (curEntry.CompressedSize != csize)
532 throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize);
535 if (curEntry.Crc < 0)
537 curEntry.Crc = crc.Value;
539 else if (curEntry.Crc != crc.Value)
541 throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc);
546 if (curEntry.IsCrypted)
548 curEntry.CompressedSize += ZipConstants.CryptoHeaderSize;
551 // Patch the header if possible
552 if (patchEntryHeader)
554 patchEntryHeader = false;
556 var curPos = baseOutputStream_.Position;
557 baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin);
558 WriteLeInt((int) curEntry.Crc);
560 if (curEntry.LocalHeaderRequiresZip64)
562 if (sizePatchPos == -1)
564 throw new ZipException("Entry requires zip64 but this has been turned off");
567 baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin);
568 WriteLeLong(curEntry.Size);
569 WriteLeLong(curEntry.CompressedSize);
573 WriteLeInt((int) curEntry.CompressedSize);
574 WriteLeInt((int) curEntry.Size);
576 baseOutputStream_.Seek(curPos, SeekOrigin.Begin);
579 // Add data descriptor if flagged as required
580 if ((curEntry.Flags & 8) != 0)
582 WriteLeInt(ZipConstants.DataDescriptorSignature);
583 WriteLeInt(unchecked((int) curEntry.Crc));
585 if (curEntry.LocalHeaderRequiresZip64)
587 WriteLeLong(curEntry.CompressedSize);
588 WriteLeLong(curEntry.Size);
589 _offset += ZipConstants.Zip64DataDescriptorSize;
593 WriteLeInt((int) curEntry.CompressedSize);
594 WriteLeInt((int) curEntry.Size);
595 _offset += ZipConstants.DataDescriptorSize;
599 entries.Add(curEntry);
603 private void WriteEncryptionHeader(long crcValue)
605 _offset += ZipConstants.CryptoHeaderSize;
607 InitializePassword(Password);
609 var cryptBuffer = new byte[ZipConstants.CryptoHeaderSize];
610 var rnd = new Random();
611 rnd.NextBytes(cryptBuffer);
612 cryptBuffer[11] = (byte) (crcValue >> 24);
614 EncryptBlock(cryptBuffer, 0, cryptBuffer.Length);
615 baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length);
619 /// Writes the given buffer to the current entry.
621 /// <param name="buffer">The buffer containing data to write.</param>
622 /// <param name="offset">The offset of the first byte to write.</param>
623 /// <param name="count">The number of bytes to write.</param>
624 /// <exception cref="ZipException">Archive size is invalid</exception>
625 /// <exception cref="System.InvalidOperationException">No entry is active.</exception>
626 public override void Write(byte[] buffer, int offset, int count)
628 if (curEntry == null)
630 throw new InvalidOperationException("No open entry.");
635 throw new ArgumentNullException("buffer");
640 throw new ArgumentOutOfRangeException("offset", "Cannot be negative");
645 throw new ArgumentOutOfRangeException("count", "Cannot be negative");
648 if ((buffer.Length - offset) < count)
650 throw new ArgumentException("Invalid offset/count combination");
653 crc.Update(buffer, offset, count);
658 case CompressionMethod.Deflated:
659 base.Write(buffer, offset, count);
662 case CompressionMethod.Stored:
663 if (Password != null)
665 CopyAndEncrypt(buffer, offset, count);
669 baseOutputStream_.Write(buffer, offset, count);
675 private void CopyAndEncrypt(byte[] buffer, int offset, int count)
677 const int CopyBufferSize = 4096;
678 var localBuffer = new byte[CopyBufferSize];
681 var bufferCount = (count < CopyBufferSize) ? count : CopyBufferSize;
683 Array.Copy(buffer, offset, localBuffer, 0, bufferCount);
684 EncryptBlock(localBuffer, 0, bufferCount);
685 baseOutputStream_.Write(localBuffer, 0, bufferCount);
686 count -= bufferCount;
687 offset += bufferCount;
692 /// Finishes the stream. This will write the central directory at the
693 /// end of the zip file and flush the stream.
696 /// This is automatically called when the stream is closed.
698 /// <exception cref="System.IO.IOException">
699 /// An I/O error occurs.
701 /// <exception cref="ZipException">
702 /// Comment exceeds the maximum length<br/>
703 /// Entry name exceeds the maximum length
705 public override void Finish()
712 if (curEntry != null)
717 long numEntries = entries.Count;
718 long sizeEntries = 0;
720 foreach (var entry in entries)
722 WriteLeInt(ZipConstants.CentralHeaderSignature);
723 WriteLeShort(ZipConstants.VersionMadeBy);
724 WriteLeShort(entry.Version);
725 WriteLeShort(entry.Flags);
726 WriteLeShort((short) entry.CompressionMethod);
727 WriteLeInt((int) entry.DosTime);
728 WriteLeInt((int) entry.Crc);
730 if (entry.IsZip64Forced() ||
731 (entry.CompressedSize >= uint.MaxValue))
737 WriteLeInt((int) entry.CompressedSize);
740 if (entry.IsZip64Forced() ||
741 (entry.Size >= uint.MaxValue))
747 WriteLeInt((int) entry.Size);
750 var name = ZipConstants.ConvertToArray(entry.Flags, entry.Name);
752 if (name.Length > 0xffff)
754 throw new ZipException("Name too long.");
757 var ed = new ZipExtraData(entry.ExtraData);
759 if (entry.CentralHeaderRequiresZip64)
762 if (entry.IsZip64Forced() ||
763 (entry.Size >= 0xffffffff))
765 ed.AddLeLong(entry.Size);
768 if (entry.IsZip64Forced() ||
769 (entry.CompressedSize >= 0xffffffff))
771 ed.AddLeLong(entry.CompressedSize);
774 if (entry.Offset >= 0xffffffff)
776 ed.AddLeLong(entry.Offset);
786 var extra = ed.GetEntryData();
789 (entry.Comment != null)
791 ZipConstants.ConvertToArray(entry.Flags, entry.Comment)
795 if (entryComment.Length > 0xffff)
797 throw new ZipException("Comment too long.");
800 WriteLeShort(name.Length);
801 WriteLeShort(extra.Length);
802 WriteLeShort(entryComment.Length);
803 WriteLeShort(0); // disk number
804 WriteLeShort(0); // internal file attributes
805 // external file attributes
807 if (entry.ExternalFileAttributes != -1)
809 WriteLeInt(entry.ExternalFileAttributes);
813 if (entry.IsDirectory)
815 // mark entry as directory (from nikolam.AT.perfectinfo.com)
824 if (entry.Offset >= uint.MaxValue)
830 WriteLeInt((int) entry.Offset);
835 baseOutputStream_.Write(name, 0, name.Length);
838 if (extra.Length > 0)
840 baseOutputStream_.Write(extra, 0, extra.Length);
843 if (entryComment.Length > 0)
845 baseOutputStream_.Write(entryComment, 0, entryComment.Length);
848 sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length;
851 using (var zhs = new ZipHelperStream(baseOutputStream_))
853 zhs.WriteEndOfCentralDirectory(numEntries, sizeEntries, _offset, zipComment);
859 #region Instance Fields
862 /// Used to track the crc of data added to entries.
864 private readonly Crc32 crc = new Crc32();
867 /// Position to patch crc
869 private long crcPatchPos = -1;
872 /// The current entry being added.
874 private ZipEntry curEntry;
876 private CompressionMethod curMethod = CompressionMethod.Deflated;
877 private int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION;
880 /// The entries for the archive.
882 private List<ZipEntry> entries = new List<ZipEntry>();
885 /// Offset to be recorded for each entry in the central header.
887 private long _offset;
890 /// Flag indicating that header patching is required for the current entry.
892 private bool patchEntryHeader;
895 /// Used to track the size of data for an entry during writing.
900 /// Position to patch size.
902 private long sizePatchPos = -1;
904 // Default is dynamic which is not backwards compatible and can cause problems
905 // with XP's built in compression which cant read Zip64 archives.
906 // However it does avoid the situation were a large file is added and cannot be completed correctly.
907 // NOTE: Setting the size for entries before they are added is the best solution!
908 private UseZip64 useZip64_ = UseZip64.Dynamic;
911 /// Comment for the entire archive recorded in central header.
913 private byte[] zipComment = new byte[0];