PageRenderTime 534ms CodeModel.GetById 243ms app.highlight 110ms RepoModel.GetById 172ms app.codeStats 0ms

/Utilities/Compression/Streams/ZipOutputStream.cs

#
C# | 829 lines | 507 code | 87 blank | 235 comment | 83 complexity | 37589ea4fd71f20ead507ba2e3e7d100 MD5 | raw file
  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
  5using System;
  6using System.Collections.Generic;
  7using System.IO;
  8using Delta.Utilities.Compression.Checksums;
  9using Delta.Utilities.Compression.Deflaters;
 10using Delta.Utilities.Helpers;
 11
 12namespace Delta.Utilities.Compression.Streams
 13{
 14
 15	#region Summary
 16	/// <summary>
 17	/// This is a DeflaterOutputStream that writes the files into a zip
 18	/// archive one after another.  It has a special method to start a new
 19	/// zip entry.  The zip entries contains information about the file name
 20	/// size, compressed size, CRC, etc.
 21	/// 
 22	/// It includes support for Stored and Deflated entries.
 23	/// This class is not thread safe.
 24	/// </summary>
 25	/// <example>This sample shows how to create a zip file
 26	/// <code>
 27	/// using System;
 28	/// using System.IO;
 29	/// 
 30	/// using Delta.Utilities.Compression;
 31	/// 
 32	/// class MainClass
 33	/// {
 34	/// 	public static void Main(string[] args)
 35	/// 	{
 36	/// 		string[] filenames = Directory.GetFiles(args[0]);
 37	/// 		
 38	/// 		ZipOutputStream s = new ZipOutputStream(File.Create(args[1]));
 39	/// 		
 40	/// 		s.SetLevel(5); // 0 - store only to 9 - means best compression
 41	/// 		
 42	/// 		foreach (string file in filenames)
 43	///     {
 44	/// 			FileStream fs = File.OpenRead(file);
 45	/// 			
 46	/// 			byte[] buffer = new byte[fs.Length];
 47	/// 			fs.Read(buffer, 0, buffer.Length);
 48	/// 			
 49	/// 			ZipEntry entry = new ZipEntry(file);
 50	/// 			
 51	/// 			s.PutNextEntry(entry);
 52	/// 			
 53	/// 			s.Write(buffer, 0, buffer.Length);
 54	/// 		} // foreach
 55	/// 		
 56	/// 		s.Finish();
 57	/// 		s.Close();
 58	/// 	} // Main(args)
 59	/// }	// class MainClass
 60	/// </code>
 61	/// </example>
 62	#endregion
 63
 64	public class ZipOutputStream : DeflaterOutputStream
 65	{
 66		#region IsFinished (Public)
 67		/// <summary>
 68		/// Gets boolean indicating central header has been added for this
 69		/// archive... No further entries can be added once this has been done.
 70		/// </summary>
 71		public bool IsFinished
 72		{
 73			get
 74			{
 75				return entries == null;
 76			} // get
 77		}
 78		#endregion
 79
 80		#region Private
 81
 82		#region entries (Private)
 83		/// <summary>
 84		/// Entries
 85		/// </summary>
 86		private List<ZipEntry> entries = new List<ZipEntry>();
 87		#endregion
 88
 89		#region crc (Private)
 90		/// <summary>
 91		/// Crc
 92		/// </summary>
 93		/// <returns>Crc 32</returns>
 94		private readonly Crc32 crc = new Crc32();
 95		#endregion
 96
 97		#region curEntry (Private)
 98		/// <summary>
 99		/// Cur entry
100		/// </summary>
101		/// <returns>Null</returns>
102		private ZipEntry curEntry;
103		#endregion
104
105		#region defaultCompressionLevel (Private)
106		/// <summary>
107		/// Default compression level
108		/// </summary>
109		/// <returns>Default compression</returns>
110		private int defaultCompressionLevel;
111		#endregion
112
113		#region curMethod (Private)
114		/// <summary>
115		/// Cur method
116		/// </summary>
117		/// <returns>Deflated</returns>
118		private CompressionMethod curMethod;
119		#endregion
120
121		#region size (Private)
122		/// <summary>
123		/// Size
124		/// </summary>
125		private long size;
126		#endregion
127
128		#region currentOffset (Private)
129		/// <summary>
130		/// Offset
131		/// </summary>
132		/// <returns>0</returns>
133		private long currentOffset;
134		#endregion
135
136		#region zipComment (Private)
137		/// <summary>
138		/// Zip comment
139		/// </summary>
140		/// <returns>Byte</returns>
141		private byte[] zipComment;
142		#endregion
143
144		#region patchEntryHeader (Private)
145		/// <summary>
146		/// Patch entry header
147		/// </summary>
148		/// <returns>False</returns>
149		private bool patchEntryHeader;
150		#endregion
151
152		#region headerPatchPos (Private)
153		/// <summary>
154		/// Header patch pos
155		/// </summary>
156		/// <returns>-</returns>
157		private long headerPatchPos;
158		#endregion
159
160		#endregion
161
162		#region Constructors
163		/// <summary>
164		/// Creates a new Zip output stream, writing a zip archive.
165		/// </summary>
166		/// <param name="baseOutputStream">
167		/// The output stream to which the archive contents are written.
168		/// </param>
169		internal ZipOutputStream(Stream baseOutputStream)
170			: this(baseOutputStream, true)
171		{
172		}
173
174		/// <summary>
175		/// Creates a new Zip output stream, writing a zip archive.
176		/// </summary>
177		/// <param name="baseOutputStream">
178		/// the output stream to which the zip archive is written.
179		/// </param>
180		/// <param name="nowrap">
181		/// Specify this to skip the header and footer for the zip file.
182		/// </param>
183		internal ZipOutputStream(Stream baseOutputStream, bool nowrap)
184			: base(baseOutputStream,
185				new Deflater(Deflater.DefaultCompression, nowrap))
186		{
187			defaultCompressionLevel = Deflater.DefaultCompression;
188			curMethod = CompressionMethod.Deflated;
189			patchEntryHeader = false;
190			headerPatchPos = MathHelper.InvalidIndex;
191			zipComment = new byte[0];
192		}
193		#endregion
194
195		#region SetComment (Public)
196		/// <summary>
197		/// Set the zip file comment.
198		/// </summary>
199		/// <param name="comment">
200		/// The comment string
201		/// </param>
202		/// <exception name ="ArgumentOutOfRangeException">
203		/// Encoding of comment is longer than 0xffff bytes.
204		/// </exception>
205		public void SetComment(string comment)
206		{
207			byte[] commentBytes = StringHelper.ToByteArray(comment);
208			if (commentBytes.Length > 0xffff)
209			{
210				throw new ArgumentOutOfRangeException("comment");
211			} // if
212			zipComment = commentBytes;
213		}
214		#endregion
215
216		#region SetCompressionLevel (Public)
217		/// <summary>
218		/// Sets default compression level (0=none - 9=best). The new level will be
219		/// activated immediately.
220		/// </summary>
221		/// <exception cref="ArgumentOutOfRangeException">
222		/// Level specified is not supported.
223		/// </exception>
224		/// <see cref="Deflater"/>
225		public void SetCompressionLevel(int level)
226		{
227			defaultCompressionLevel = level;
228			deflater.SetLevel(level);
229		}
230		#endregion
231
232		#region GetLevel (Public)
233		/// <summary>
234		/// Get the current deflate compression level
235		/// </summary>
236		/// <returns>The current compression level</returns>
237		public int GetLevel()
238		{
239			return deflater.GetLevel();
240		}
241		#endregion
242
243		#region Write (Public)
244		/// <summary>
245		/// Writes the given buffer to the current entry.
246		/// </summary>
247		/// <exception cref="ZipException">
248		/// Archive size is invalid
249		/// </exception>
250		/// <exception cref="System.InvalidOperationException">
251		/// No entry is active.
252		/// </exception>
253		public override void Write(byte[] buffer, int offset, int count)
254		{
255			if (curEntry == null)
256			{
257				throw new InvalidOperationException("No open entry.");
258			} // if
259
260			if (count <= 0)
261			{
262				return;
263			} // if
264
265			crc.Update(buffer, offset, count);
266			size += count;
267
268			if (size > 0xffffffff ||
269			    size < 0)
270			{
271				throw new ZipException("Maximum entry size exceeded");
272			} // if
273
274			switch (curMethod)
275			{
276				case CompressionMethod.Deflated:
277					base.Write(buffer, offset, count);
278					break;
279
280				case CompressionMethod.Stored:
281					if (Password != null)
282					{
283						byte[] buf = new byte[count];
284						Array.Copy(buffer, offset, buf, 0, count);
285						EncryptBlock(buf, 0, count);
286						baseOutputStream.Write(buf, offset, count);
287					} // if
288					else
289					{
290						baseOutputStream.Write(buffer, offset, count);
291					} // else
292					break;
293			} // switch
294		}
295		#endregion
296
297		#region Finish (Public)
298		/// <summary>
299		/// Finishes the stream.  This will write the central directory at the
300		/// end of the zip file and flush the stream.
301		/// </summary>
302		/// <remarks>
303		/// This is automatically called when the stream is closed.
304		/// </remarks>
305		/// <exception cref="System.IO.IOException">
306		/// An I/O error occurs.
307		/// </exception>
308		/// <exception cref="ZipException">
309		/// Comment exceeds the maximum length<br/>
310		/// Entry name exceeds the maximum length
311		/// </exception>
312		public override void Finish()
313		{
314			if (entries == null ||
315			    baseOutputStream == null ||
316			    baseOutputStream.CanWrite == false)
317			{
318				return;
319			} // if
320
321			if (curEntry != null)
322			{
323				CloseEntry();
324			} // if
325
326			int numEntries = 0;
327			int sizeEntries = 0;
328
329			foreach (ZipEntry entry in entries)
330			{
331				CompressionMethod method = entry.CompressionMethod;
332				WriteLeInt(ZipConstants.CentralDirectorySig);
333				WriteLeShort(ZipConstants.VersionMadeBy);
334				WriteLeShort(entry.Version);
335				WriteLeShort(entry.Flags);
336				WriteLeShort((short)method);
337				WriteLeInt((int)entry.DosTime);
338				WriteLeInt((int)entry.Crc);
339				WriteLeInt((int)entry.CompressedSize);
340				WriteLeInt((int)entry.Size);
341
342				byte[] name = StringHelper.ToByteArray(entry.Name);
343
344				if (name.Length > 0xffff)
345				{
346					throw new ZipException("Name too long.");
347				} // if
348
349				byte[] extra = entry.GetExtraData();
350				if (extra == null)
351				{
352					extra = new byte[0];
353				} // if
354
355				byte[] entryComment = (entry.Comment != null)
356				                      	? StringHelper.ToByteArray(entry.Comment)
357				                      	: new byte[0];
358				if (entryComment.Length > 0xffff)
359				{
360					throw new ZipException("Comment too long.");
361				} // if
362
363				WriteLeShort(name.Length);
364				WriteLeShort(extra.Length);
365				WriteLeShort(entryComment.Length);
366				// disk number
367				WriteLeShort(0);
368				// internal file attributes
369				WriteLeShort(0);
370
371				// external file attribute
372				if (entry.ExternalFileAttributes != MathHelper.InvalidIndex)
373				{
374					WriteLeInt(entry.ExternalFileAttributes);
375				} // if
376				else
377				{
378					// mark entry as directory (from nikolam.AT.perfectinfo.com)
379					if (entry.IsDirectory)
380					{
381						WriteLeInt(16);
382					} // if
383					else
384					{
385						WriteLeInt(0);
386					} // else
387				} // else
388
389				WriteLeInt(entry.Offset);
390
391				baseOutputStream.Write(name, 0, name.Length);
392				baseOutputStream.Write(extra, 0, extra.Length);
393				baseOutputStream.Write(entryComment, 0, entryComment.Length);
394				++numEntries;
395				sizeEntries += ZipConstants.CentralHeader + name.Length +
396				               extra.Length + entryComment.Length;
397			} // foreach
398
399			WriteLeInt(ZipConstants.EndSig);
400			// number of this disk
401			WriteLeShort(0);
402			// no of disk with start of central dir
403			WriteLeShort(0);
404			// entries in central dir for this disk
405			WriteLeShort(numEntries);
406			// total entries in central directory
407			WriteLeShort(numEntries);
408			// size of the central directory
409			WriteLeInt(sizeEntries);
410			// offset of start of central dir
411			WriteLeInt((int)currentOffset);
412			WriteLeShort(zipComment.Length);
413			baseOutputStream.Write(zipComment, 0, zipComment.Length);
414			baseOutputStream.Flush();
415			entries = null;
416		}
417		#endregion
418
419		#region Methods (Private)
420
421		#region WriteLeShort
422		/// <summary>
423		/// Write an unsigned short in little endian byte order.
424		/// </summary>
425		private void WriteLeShort(int value)
426		{
427			baseOutputStream.WriteByte((byte)(value & 0xff));
428			baseOutputStream.WriteByte((byte)((value >> 8) & 0xff));
429		}
430		#endregion
431
432		#region WriteLeInt
433		/// <summary>
434		/// Write an int in little endian byte order.
435		/// </summary>
436		private void WriteLeInt(int value)
437		{
438			WriteLeShort(value);
439			WriteLeShort(value >> 16);
440		}
441		#endregion
442
443		#region WriteLeLong
444		/// <summary>
445		/// Write an int in little endian byte order.
446		/// </summary>
447		private void WriteLeLong(long value)
448		{
449			WriteLeInt((int)value);
450			WriteLeInt((int)(value >> 32));
451		}
452		#endregion
453
454		#region PutNextEntry
455		/// <summary>
456		/// Starts a new Zip entry. It automatically closes the previous
457		/// entry if present.
458		/// All entry elements bar name are optional, but must be correct if present.
459		/// If the compression method is stored and the output is not patchable
460		/// the compression for that entry is automatically changed to deflate level 0
461		/// </summary>
462		/// <param name="entry">
463		/// the entry.
464		/// </param>
465		/// <exception cref="System.IO.IOException">
466		/// if an I/O error occured.
467		/// </exception>
468		/// <exception cref="System.InvalidOperationException">
469		/// if stream was finished
470		/// </exception>
471		/// <exception cref="ZipException">
472		/// Too many entries in the Zip file<br/>
473		/// Entry name is too long<br/>
474		/// Finish has already been called<br/>
475		/// </exception>
476		internal void PutNextEntry(ZipEntry entry)
477		{
478			if (entries == null)
479			{
480				throw new InvalidOperationException("ZipOutputStream was finished");
481			} // if
482
483			if (curEntry != null)
484			{
485				CloseEntry();
486			} // if
487
488			if (entries.Count >= 0xffff)
489			{
490				throw new ZipException("Too many entries for Zip file");
491			} // if
492
493			CompressionMethod method = entry.CompressionMethod;
494			int compressionLevel = defaultCompressionLevel;
495
496			entry.Flags = 0;
497			patchEntryHeader = false;
498			bool headerInfoAvailable = true;
499			PrepareEntryHeader(entry, ref method, ref compressionLevel,
500				ref headerInfoAvailable);
501
502			curMethod = method;
503
504			// Write the local file header
505			WriteLeInt(ZipConstants.LocalSignature);
506
507			WriteLeShort(entry.Version);
508			WriteLeShort(entry.Flags);
509			WriteLeShort((byte)method);
510			WriteLeInt((int)entry.DosTime);
511			if (headerInfoAvailable)
512			{
513				WriteLeInt((int)entry.Crc);
514				WriteLeInt(entry.IsCrypted
515				           	? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize
516				           	: (int)entry.CompressedSize);
517				WriteLeInt((int)entry.Size);
518			} // if
519			else
520			{
521				if (patchEntryHeader)
522				{
523					headerPatchPos = baseOutputStream.Position;
524				} // if
525
526				// Crc
527				WriteLeInt(0);
528				// Compressed size
529				WriteLeInt(0);
530				// Uncompressed size
531				WriteLeInt(0);
532			} // else
533
534			byte[] name = StringHelper.ToByteArray(entry.Name);
535			if (name.Length > 0xFFFF)
536			{
537				throw new ZipException("Entry name too long.");
538			} // if
539
540			byte[] extra = entry.GetExtraData();
541			if (extra == null)
542			{
543				extra = new byte[0];
544			} // if
545
546			if (extra.Length > 0xFFFF)
547			{
548				throw new ZipException("Extra data too long.");
549			} // if
550
551			WriteLeShort(name.Length);
552			WriteLeShort(extra.Length);
553			baseOutputStream.Write(name, 0, name.Length);
554			baseOutputStream.Write(extra, 0, extra.Length);
555
556			currentOffset += ZipConstants.LocalHeader + name.Length + extra.Length;
557
558			// Activate the entry.
559			curEntry = entry;
560			crc.Reset();
561			if (method == CompressionMethod.Deflated)
562			{
563				deflater.Reset();
564				deflater.SetLevel(compressionLevel);
565			} // if
566			size = 0;
567
568			if (entry.IsCrypted)
569			{
570				if (entry.Crc < 0)
571				{
572					// so testing Zip will says its ok
573					WriteEncryptionHeader(entry.DosTime << 16);
574				} // if
575				else
576				{
577					WriteEncryptionHeader(entry.Crc);
578				} // else
579			} // if
580		}
581		#endregion
582
583		#region PrepareEntryHeader
584		/// <summary>
585		/// Prepare entry header
586		/// </summary>
587		/// <param name="entry">Entry</param>
588		/// <param name="method">Method</param>
589		/// <param name="compressionLevel">Compression level</param>
590		/// <param name="headerInfoAvailable">Header info available</param>
591		private void PrepareEntryHeader(ZipEntry entry,
592			ref CompressionMethod method, ref int compressionLevel,
593			ref bool headerInfoAvailable)
594		{
595			if (method == CompressionMethod.Stored)
596			{
597				if (entry.CompressedSize >= 0)
598				{
599					if (entry.Size < 0)
600					{
601						entry.Size = entry.CompressedSize;
602					} // if
603					else if (entry.Size != entry.CompressedSize)
604					{
605						throw new ZipException(
606							"Method Stored, but compressed size != size");
607					} // else if
608				} // if
609				else
610				{
611					if (entry.Size >= 0)
612					{
613						entry.CompressedSize = entry.Size;
614					} // if
615				} // else
616
617				if (entry.Size < 0 ||
618				    entry.Crc < 0)
619				{
620					if (CanPatchEntries)
621					{
622						headerInfoAvailable = false;
623					} // if
624					else
625					{
626						// Cant patch entries so storing is not possible.
627						method = CompressionMethod.Deflated;
628						compressionLevel = 0;
629					} // else
630				} // if
631			} // if
632
633			if (method == CompressionMethod.Deflated)
634			{
635				if (entry.Size == 0)
636				{
637					// No need to compress - no data.
638					entry.CompressedSize = entry.Size;
639					entry.Crc = 0;
640					method = CompressionMethod.Stored;
641				} // if
642				else if (entry.CompressedSize < 0 ||
643				         entry.Size < 0 ||
644				         entry.Crc < 0)
645				{
646					headerInfoAvailable = false;
647				} // else if
648			} // if
649
650			if (headerInfoAvailable == false)
651			{
652				if (CanPatchEntries == false)
653				{
654					entry.Flags |= 8;
655				} // if
656				else
657				{
658					patchEntryHeader = true;
659				} // else
660			} // if
661
662			if (Password != null)
663			{
664				entry.IsCrypted = true;
665				if (entry.Crc < 0)
666				{
667					// Need to append data descriptor as crc is used for encryption and
668					// its not known.
669					entry.Flags |= 8;
670				} // if
671			} // if
672			entry.Offset = (int)currentOffset;
673			entry.CompressionMethod = method;
674		}
675		#endregion
676
677		#region CloseEntry
678		/// <summary>
679		/// Closes the current entry, updating header and footer information as
680		/// required
681		/// </summary>
682		/// <exception cref="System.IO.IOException">
683		/// An I/O error occurs.
684		/// </exception>
685		/// <exception cref="System.InvalidOperationException">
686		/// No entry is active.
687		/// </exception>
688		internal void CloseEntry()
689		{
690			if (curEntry == null)
691			{
692				throw new InvalidOperationException("No open entry");
693			} // if
694
695			// First finish the deflater, if appropriate
696			if (curMethod == CompressionMethod.Deflated)
697			{
698				base.Finish();
699			} // if
700
701			long csize = curMethod == CompressionMethod.Deflated
702			             	? deflater.TotalOut
703			             	: size;
704
705			if (curEntry.Size < 0)
706			{
707				curEntry.Size = size;
708			} // if
709			else if (curEntry.Size != size)
710			{
711				throw new ZipException("size was " + size + ", but I expected " +
712				                       curEntry.Size);
713			} // else if
714
715			if (curEntry.CompressedSize < 0)
716			{
717				curEntry.CompressedSize = csize;
718			} // if
719			else if (curEntry.CompressedSize != csize)
720			{
721				throw new ZipException("compressed size was " + csize +
722				                       ", but I expected " + curEntry.CompressedSize);
723			} // else if
724
725			if (curEntry.Crc < 0)
726			{
727				curEntry.Crc = crc.Value;
728			} // if
729			else if (curEntry.Crc != crc.Value)
730			{
731				throw new ZipException("crc was " + crc.Value + ", but I expected " +
732				                       curEntry.Crc);
733			} // else if
734
735			currentOffset += csize;
736
737			if (currentOffset > 0xffffffff)
738			{
739				throw new ZipException("Maximum Zip file size exceeded");
740			} // if
741
742			if (curEntry.IsCrypted)
743			{
744				curEntry.CompressedSize += ZipConstants.CryptoHeaderSize;
745			} // if
746
747			// Patch the header if possible
748			if (patchEntryHeader)
749			{
750				long curPos = baseOutputStream.Position;
751				baseOutputStream.Seek(headerPatchPos, SeekOrigin.Begin);
752				WriteLeInt((int)curEntry.Crc);
753				WriteLeInt((int)curEntry.CompressedSize);
754				WriteLeInt((int)curEntry.Size);
755				baseOutputStream.Seek(curPos, SeekOrigin.Begin);
756				patchEntryHeader = false;
757			} // if
758
759			// Add data descriptor if flagged as required
760			if ((curEntry.Flags & 8) != 0)
761			{
762				WriteLeInt(ZipConstants.ExternSig);
763				WriteLeInt((int)curEntry.Crc);
764				WriteLeInt((int)curEntry.CompressedSize);
765				WriteLeInt((int)curEntry.Size);
766				currentOffset += ZipConstants.ExternHeader;
767			} // if
768
769			entries.Add(curEntry);
770			curEntry = null;
771		}
772		#endregion
773
774		#region WriteEncryptionHeader
775		/// <summary>
776		/// Write encryption header
777		/// </summary>
778		/// <param name="crcValue">Crc value</param>
779		private void WriteEncryptionHeader(long crcValue)
780		{
781			currentOffset += ZipConstants.CryptoHeaderSize;
782
783			InitializePassword(Password);
784
785			byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize];
786			Random rnd = new Random();
787			rnd.NextBytes(cryptBuffer);
788			cryptBuffer[11] = (byte)(crcValue >> 24);
789
790			EncryptBlock(cryptBuffer, 0, cryptBuffer.Length);
791			baseOutputStream.Write(cryptBuffer, 0, cryptBuffer.Length);
792		}
793		#endregion
794
795		#region AddFile
796		/// <summary>
797		/// Add file to zip stream.
798		/// Will also make sure the header is updated and we store the
799		/// correct crc and file length.
800		/// </summary>
801		/// <param name="filePath">File to store</param>
802		internal void AddFile(string filePath)
803		{
804			// Read file data (might throw an exception if file doesn't exists)
805			FileStream fs = File.OpenRead(filePath);
806			byte[] buffer = new byte[fs.Length];
807			fs.Read(buffer, 0, buffer.Length);
808			ZipEntry entry = new ZipEntry(filePath);
809
810			// Set time of storage
811			entry.DateTime = DateTime.Now;
812
813			// Set file size
814			entry.Size = fs.Length;
815			fs.Close();
816
817			// And finally set crc value
818			entry.Crc = Crc32.ComputeCrcValue(buffer);
819
820			// Add entry 
821			PutNextEntry(entry);
822			Write(buffer, 0, buffer.Length);
823			CloseEntry();
824		}
825		#endregion
826
827		#endregion
828	}
829}