// GZipStream.cs // ------------------------------------------------------------------ // // Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. // All rights reserved. // // This code module is part of DotNetZip, a zipfile class library. // // ------------------------------------------------------------------ // // This code is licensed under the Microsoft Public License. // See the file License.txt for the license details. // More info on: http://dotnetzip.codeplex.com // // ------------------------------------------------------------------ // // last saved (in emacs): // Time-stamp: <2011-July-11 21:42:34> // // ------------------------------------------------------------------ // // This module defines the GZipStream class, which can be used as a replacement for // the System.IO.Compression.GZipStream class in the .NET BCL. NB: The design is not // completely OO clean: there is some intelligence in the ZlibBaseStream that reads the // GZip header. // // ------------------------------------------------------------------ using BestHTTP.PlatformSupport.Memory; using System; using System.IO; namespace BestHTTP.Decompression.Zlib { /// /// A class for compressing and decompressing GZIP streams. /// /// /// /// /// The GZipStream is a Decorator on a /// . It adds GZIP compression or decompression to any /// stream. /// /// /// /// Like the System.IO.Compression.GZipStream in the .NET Base Class Library, the /// Ionic.Zlib.GZipStream can compress while writing, or decompress while /// reading, but not vice versa. The compression method used is GZIP, which is /// documented in IETF RFC /// 1952, "GZIP file format specification version 4.3". /// /// /// A GZipStream can be used to decompress data (through Read()) or /// to compress data (through Write()), but not both. /// /// /// /// If you wish to use the GZipStream to compress data, you must wrap it /// around a write-able stream. As you call Write() on the GZipStream, the /// data will be compressed into the GZIP format. If you want to decompress data, /// you must wrap the GZipStream around a readable stream that contains an /// IETF RFC 1952-compliant stream. The data will be decompressed as you call /// Read() on the GZipStream. /// /// /// /// Though the GZIP format allows data from multiple files to be concatenated /// together, this stream handles only a single segment of GZIP format, typically /// representing a single file. /// /// /// /// /// public class GZipStream : System.IO.Stream { // GZip format // source: http://tools.ietf.org/html/rfc1952 // // header id: 2 bytes 1F 8B // compress method 1 byte 8= DEFLATE (none other supported) // flag 1 byte bitfield (See below) // mtime 4 bytes time_t (seconds since jan 1, 1970 UTC of the file. // xflg 1 byte 2 = max compress used , 4 = max speed (can be ignored) // OS 1 byte OS for originating archive. set to 0xFF in compression. // extra field length 2 bytes optional - only if FEXTRA is set. // extra field varies // filename varies optional - if FNAME is set. zero terminated. ISO-8859-1. // file comment varies optional - if FCOMMENT is set. zero terminated. ISO-8859-1. // crc16 1 byte optional - present only if FHCRC bit is set // compressed data varies // CRC32 4 bytes // isize 4 bytes data size modulo 2^32 // // FLG (FLaGs) // bit 0 FTEXT - indicates file is ASCII text (can be safely ignored) // bit 1 FHCRC - there is a CRC16 for the header immediately following the header // bit 2 FEXTRA - extra fields are present // bit 3 FNAME - the zero-terminated filename is present. encoding; ISO-8859-1. // bit 4 FCOMMENT - a zero-terminated file comment is present. encoding: ISO-8859-1 // bit 5 reserved // bit 6 reserved // bit 7 reserved // // On consumption: // Extra field is a bunch of nonsense and can be safely ignored. // Header CRC and OS, likewise. // // on generation: // all optional fields get 0, except for the OS, which gets 255. // /// /// The comment on the GZIP stream. /// /// /// /// /// The GZIP format allows for each file to optionally have an associated /// comment stored with the file. The comment is encoded with the ISO-8859-1 /// code page. To include a comment in a GZIP stream you create, set this /// property before calling Write() for the first time on the /// GZipStream. /// /// /// /// When using GZipStream to decompress, you can retrieve this property /// after the first call to Read(). If no comment has been set in the /// GZIP bytestream, the Comment property will return null /// (Nothing in VB). /// /// public String Comment { get { return _Comment; } set { if (_disposed) throw new ObjectDisposedException("GZipStream"); _Comment = value; } } /// /// The FileName for the GZIP stream. /// /// /// /// /// /// The GZIP format optionally allows each file to have an associated /// filename. When compressing data (through Write()), set this /// FileName before calling Write() the first time on the GZipStream. /// The actual filename is encoded into the GZIP bytestream with the /// ISO-8859-1 code page, according to RFC 1952. It is the application's /// responsibility to insure that the FileName can be encoded and decoded /// correctly with this code page. /// /// /// /// When decompressing (through Read()), you can retrieve this value /// any time after the first Read(). In the case where there was no filename /// encoded into the GZIP bytestream, the property will return null (Nothing /// in VB). /// /// public String FileName { get { return _FileName; } set { if (_disposed) throw new ObjectDisposedException("GZipStream"); _FileName = value; if (_FileName == null) return; if (_FileName.IndexOf("/") != -1) { _FileName = _FileName.Replace("/", "\\"); } if (_FileName.EndsWith("\\")) throw new Exception("Illegal filename"); if (_FileName.IndexOf("\\") != -1) { // trim any leading path _FileName = Path.GetFileName(_FileName); } } } /// /// The last modified time for the GZIP stream. /// /// /// /// GZIP allows the storage of a last modified time with each GZIP entity. /// When compressing data, you can set this before the first call to /// Write(). When decompressing, you can retrieve this value any time /// after the first call to Read(). /// public DateTime? LastModified; /// /// The CRC on the GZIP stream. /// /// /// This is used for internal error checking. You probably don't need to look at this property. /// public int Crc32 { get { return _Crc32; } } private int _headerByteCount; internal ZlibBaseStream _baseStream; bool _disposed; bool _firstReadDone; string _FileName; string _Comment; int _Crc32; /// /// Create a GZipStream using the specified CompressionMode. /// /// /// /// /// When mode is CompressionMode.Compress, the GZipStream will use the /// default compression level. /// /// /// /// As noted in the class documentation, the CompressionMode (Compress /// or Decompress) also establishes the "direction" of the stream. A /// GZipStream with CompressionMode.Compress works only through /// Write(). A GZipStream with /// CompressionMode.Decompress works only through Read(). /// /// /// /// /// /// This example shows how to use a GZipStream to compress data. /// /// using (System.IO.Stream input = System.IO.File.OpenRead(fileToCompress)) /// { /// using (var raw = System.IO.File.Create(outputFile)) /// { /// using (Stream compressor = new GZipStream(raw, CompressionMode.Compress)) /// { /// byte[] buffer = new byte[WORKING_BUFFER_SIZE]; /// int n; /// while ((n= input.Read(buffer, 0, buffer.Length)) != 0) /// { /// compressor.Write(buffer, 0, n); /// } /// } /// } /// } /// /// /// Dim outputFile As String = (fileToCompress & ".compressed") /// Using input As Stream = File.OpenRead(fileToCompress) /// Using raw As FileStream = File.Create(outputFile) /// Using compressor As Stream = New GZipStream(raw, CompressionMode.Compress) /// Dim buffer As Byte() = New Byte(4096) {} /// Dim n As Integer = -1 /// Do While (n <> 0) /// If (n > 0) Then /// compressor.Write(buffer, 0, n) /// End If /// n = input.Read(buffer, 0, buffer.Length) /// Loop /// End Using /// End Using /// End Using /// /// /// /// /// This example shows how to use a GZipStream to uncompress a file. /// /// private void GunZipFile(string filename) /// { /// if (!filename.EndsWith(".gz)) /// throw new ArgumentException("filename"); /// var DecompressedFile = filename.Substring(0,filename.Length-3); /// byte[] working = new byte[WORKING_BUFFER_SIZE]; /// int n= 1; /// using (System.IO.Stream input = System.IO.File.OpenRead(filename)) /// { /// using (Stream decompressor= new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, true)) /// { /// using (var output = System.IO.File.Create(DecompressedFile)) /// { /// while (n !=0) /// { /// n= decompressor.Read(working, 0, working.Length); /// if (n > 0) /// { /// output.Write(working, 0, n); /// } /// } /// } /// } /// } /// } /// /// /// /// Private Sub GunZipFile(ByVal filename as String) /// If Not (filename.EndsWith(".gz)) Then /// Throw New ArgumentException("filename") /// End If /// Dim DecompressedFile as String = filename.Substring(0,filename.Length-3) /// Dim working(WORKING_BUFFER_SIZE) as Byte /// Dim n As Integer = 1 /// Using input As Stream = File.OpenRead(filename) /// Using decompressor As Stream = new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, True) /// Using output As Stream = File.Create(UncompressedFile) /// Do /// n= decompressor.Read(working, 0, working.Length) /// If n > 0 Then /// output.Write(working, 0, n) /// End IF /// Loop While (n > 0) /// End Using /// End Using /// End Using /// End Sub /// /// /// /// The stream which will be read or written. /// Indicates whether the GZipStream will compress or decompress. public GZipStream(Stream stream, CompressionMode mode) : this(stream, mode, CompressionLevel.Default, false) { } /// /// Create a GZipStream using the specified CompressionMode and /// the specified CompressionLevel. /// /// /// /// /// The CompressionMode (Compress or Decompress) also establishes the /// "direction" of the stream. A GZipStream with /// CompressionMode.Compress works only through Write(). A /// GZipStream with CompressionMode.Decompress works only /// through Read(). /// /// /// /// /// /// /// This example shows how to use a GZipStream to compress a file into a .gz file. /// /// /// using (System.IO.Stream input = System.IO.File.OpenRead(fileToCompress)) /// { /// using (var raw = System.IO.File.Create(fileToCompress + ".gz")) /// { /// using (Stream compressor = new GZipStream(raw, /// CompressionMode.Compress, /// CompressionLevel.BestCompression)) /// { /// byte[] buffer = new byte[WORKING_BUFFER_SIZE]; /// int n; /// while ((n= input.Read(buffer, 0, buffer.Length)) != 0) /// { /// compressor.Write(buffer, 0, n); /// } /// } /// } /// } /// /// /// /// Using input As Stream = File.OpenRead(fileToCompress) /// Using raw As FileStream = File.Create(fileToCompress & ".gz") /// Using compressor As Stream = New GZipStream(raw, CompressionMode.Compress, CompressionLevel.BestCompression) /// Dim buffer As Byte() = New Byte(4096) {} /// Dim n As Integer = -1 /// Do While (n <> 0) /// If (n > 0) Then /// compressor.Write(buffer, 0, n) /// End If /// n = input.Read(buffer, 0, buffer.Length) /// Loop /// End Using /// End Using /// End Using /// /// /// The stream to be read or written while deflating or inflating. /// Indicates whether the GZipStream will compress or decompress. /// A tuning knob to trade speed for effectiveness. public GZipStream(Stream stream, CompressionMode mode, CompressionLevel level) : this(stream, mode, level, false) { } /// /// Create a GZipStream using the specified CompressionMode, and /// explicitly specify whether the stream should be left open after Deflation /// or Inflation. /// /// /// /// /// This constructor allows the application to request that the captive stream /// remain open after the deflation or inflation occurs. By default, after /// Close() is called on the stream, the captive stream is also /// closed. In some cases this is not desired, for example if the stream is a /// memory stream that will be re-read after compressed data has been written /// to it. Specify true for the parameter to leave /// the stream open. /// /// /// /// The (Compress or Decompress) also /// establishes the "direction" of the stream. A GZipStream with /// CompressionMode.Compress works only through Write(). A GZipStream /// with CompressionMode.Decompress works only through Read(). /// /// /// /// The GZipStream will use the default compression level. If you want /// to specify the compression level, see . /// /// /// /// See the other overloads of this constructor for example code. /// /// /// /// /// /// The stream which will be read or written. This is called the "captive" /// stream in other places in this documentation. /// /// /// Indicates whether the GZipStream will compress or decompress. /// /// /// /// true if the application would like the base stream to remain open after /// inflation/deflation. /// public GZipStream(Stream stream, CompressionMode mode, bool leaveOpen) : this(stream, mode, CompressionLevel.Default, leaveOpen) { } /// /// Create a GZipStream using the specified CompressionMode and the /// specified CompressionLevel, and explicitly specify whether the /// stream should be left open after Deflation or Inflation. /// /// /// /// /// /// This constructor allows the application to request that the captive stream /// remain open after the deflation or inflation occurs. By default, after /// Close() is called on the stream, the captive stream is also /// closed. In some cases this is not desired, for example if the stream is a /// memory stream that will be re-read after compressed data has been written /// to it. Specify true for the parameter to /// leave the stream open. /// /// /// /// As noted in the class documentation, the CompressionMode (Compress /// or Decompress) also establishes the "direction" of the stream. A /// GZipStream with CompressionMode.Compress works only through /// Write(). A GZipStream with CompressionMode.Decompress works only /// through Read(). /// /// /// /// /// /// This example shows how to use a GZipStream to compress data. /// /// using (System.IO.Stream input = System.IO.File.OpenRead(fileToCompress)) /// { /// using (var raw = System.IO.File.Create(outputFile)) /// { /// using (Stream compressor = new GZipStream(raw, CompressionMode.Compress, CompressionLevel.BestCompression, true)) /// { /// byte[] buffer = new byte[WORKING_BUFFER_SIZE]; /// int n; /// while ((n= input.Read(buffer, 0, buffer.Length)) != 0) /// { /// compressor.Write(buffer, 0, n); /// } /// } /// } /// } /// /// /// Dim outputFile As String = (fileToCompress & ".compressed") /// Using input As Stream = File.OpenRead(fileToCompress) /// Using raw As FileStream = File.Create(outputFile) /// Using compressor As Stream = New GZipStream(raw, CompressionMode.Compress, CompressionLevel.BestCompression, True) /// Dim buffer As Byte() = New Byte(4096) {} /// Dim n As Integer = -1 /// Do While (n <> 0) /// If (n > 0) Then /// compressor.Write(buffer, 0, n) /// End If /// n = input.Read(buffer, 0, buffer.Length) /// Loop /// End Using /// End Using /// End Using /// /// /// The stream which will be read or written. /// Indicates whether the GZipStream will compress or decompress. /// true if the application would like the stream to remain open after inflation/deflation. /// A tuning knob to trade speed for effectiveness. public GZipStream(Stream stream, CompressionMode mode, CompressionLevel level, bool leaveOpen) { _baseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.GZIP, leaveOpen); } #region Zlib properties /// /// This property sets the flush behavior on the stream. /// virtual public FlushType FlushMode { get { return (this._baseStream._flushMode); } set { if (_disposed) throw new ObjectDisposedException("GZipStream"); this._baseStream._flushMode = value; } } /// /// The size of the working buffer for the compression codec. /// /// /// /// /// The working buffer is used for all stream operations. The default size is /// 1024 bytes. The minimum size is 128 bytes. You may get better performance /// with a larger buffer. Then again, you might not. You would have to test /// it. /// /// /// /// Set this before the first call to Read() or Write() on the /// stream. If you try to set it afterwards, it will throw. /// /// public int BufferSize { get { return this._baseStream._bufferSize; } set { if (_disposed) throw new ObjectDisposedException("GZipStream"); if (this._baseStream._workingBuffer != null) throw new ZlibException("The working buffer is already set."); if (value < ZlibConstants.WorkingBufferSizeMin) throw new ZlibException(String.Format("Don't be silly. {0} bytes?? Use a bigger buffer, at least {1}.", value, ZlibConstants.WorkingBufferSizeMin)); this._baseStream._bufferSize = value; } } /// Returns the total number of bytes input so far. virtual public long TotalIn { get { return this._baseStream._z.TotalBytesIn; } } /// Returns the total number of bytes output so far. virtual public long TotalOut { get { return this._baseStream._z.TotalBytesOut; } } #endregion #region Stream methods /// /// Dispose the stream. /// /// /// /// This may or may not result in a Close() call on the captive /// stream. See the constructors that have a leaveOpen parameter /// for more information. /// /// /// This method may be invoked in two distinct scenarios. If disposing /// == true, the method has been called directly or indirectly by a /// user's code, for example via the public Dispose() method. In this /// case, both managed and unmanaged resources can be referenced and /// disposed. If disposing == false, the method has been called by the /// runtime from inside the object finalizer and this method should not /// reference other objects; in that case only unmanaged resources must /// be referenced or disposed. /// /// /// /// indicates whether the Dispose method was invoked by user code. /// protected override void Dispose(bool disposing) { try { if (!_disposed) { if (disposing && (this._baseStream != null)) { this._baseStream.Close(); this._Crc32 = _baseStream.Crc32; } _disposed = true; } } finally { base.Dispose(disposing); } } /// /// Indicates whether the stream can be read. /// /// /// The return value depends on whether the captive stream supports reading. /// public override bool CanRead { get { if (_disposed) throw new ObjectDisposedException("GZipStream"); return _baseStream._stream.CanRead; } } /// /// Indicates whether the stream supports Seek operations. /// /// /// Always returns false. /// public override bool CanSeek { get { return false; } } /// /// Indicates whether the stream can be written. /// /// /// The return value depends on whether the captive stream supports writing. /// public override bool CanWrite { get { if (_disposed) throw new ObjectDisposedException("GZipStream"); return _baseStream._stream.CanWrite; } } /// /// Flush the stream. /// public override void Flush() { if (_disposed) throw new ObjectDisposedException("GZipStream"); _baseStream.Flush(); } /// /// Reading this property always throws a . /// public override long Length { get { throw new NotImplementedException(); } } /// /// The position of the stream pointer. /// /// /// /// Setting this property always throws a . Reading will return the total bytes /// written out, if used in writing, or the total bytes read in, if used in /// reading. The count may refer to compressed bytes or uncompressed bytes, /// depending on how you've used the stream. /// public override long Position { get { if (this._baseStream._streamMode == BestHTTP.Decompression.Zlib.ZlibBaseStream.StreamMode.Writer) return this._baseStream._z.TotalBytesOut + _headerByteCount; if (this._baseStream._streamMode == BestHTTP.Decompression.Zlib.ZlibBaseStream.StreamMode.Reader) return this._baseStream._z.TotalBytesIn + this._baseStream._gzipHeaderByteCount; return 0; } set { throw new NotImplementedException(); } } /// /// Read and decompress data from the source stream. /// /// /// /// With a GZipStream, decompression is done through reading. /// /// /// /// /// byte[] working = new byte[WORKING_BUFFER_SIZE]; /// using (System.IO.Stream input = System.IO.File.OpenRead(_CompressedFile)) /// { /// using (Stream decompressor= new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, true)) /// { /// using (var output = System.IO.File.Create(_DecompressedFile)) /// { /// int n; /// while ((n= decompressor.Read(working, 0, working.Length)) !=0) /// { /// output.Write(working, 0, n); /// } /// } /// } /// } /// /// /// The buffer into which the decompressed data should be placed. /// the offset within that data array to put the first byte read. /// the number of bytes to read. /// the number of bytes actually read public override int Read(byte[] buffer, int offset, int count) { if (_disposed) throw new ObjectDisposedException("GZipStream"); int n = _baseStream.Read(buffer, offset, count); // Console.WriteLine("GZipStream::Read(buffer, off({0}), c({1}) = {2}", offset, count, n); // Console.WriteLine( Util.FormatByteArray(buffer, offset, n) ); if (!_firstReadDone) { _firstReadDone = true; FileName = _baseStream._GzipFileName; Comment = _baseStream._GzipComment; } return n; } /// /// Calling this method always throws a . /// /// irrelevant; it will always throw! /// irrelevant; it will always throw! /// irrelevant! public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } /// /// Calling this method always throws a . /// /// irrelevant; this method will always throw! public override void SetLength(long value) { //throw new NotImplementedException(); _baseStream.SetLength(value); } /// /// Write data to the stream. /// /// /// /// /// If you wish to use the GZipStream to compress data while writing, /// you can create a GZipStream with CompressionMode.Compress, and a /// writable output stream. Then call Write() on that GZipStream, /// providing uncompressed data as input. The data sent to the output stream /// will be the compressed form of the data written. /// /// /// /// A GZipStream can be used for Read() or Write(), but not /// both. Writing implies compression. Reading implies decompression. /// /// /// /// The buffer holding data to write to the stream. /// the offset within that data array to find the first byte to write. /// the number of bytes to write. public override void Write(byte[] buffer, int offset, int count) { if (_disposed) throw new ObjectDisposedException("GZipStream"); if (_baseStream._streamMode == BestHTTP.Decompression.Zlib.ZlibBaseStream.StreamMode.Undefined) { //Console.WriteLine("GZipStream: First write"); if (_baseStream._wantCompress) { // first write in compression, therefore, emit the GZIP header _headerByteCount = EmitHeader(); } else { throw new InvalidOperationException(); } } _baseStream.Write(buffer, offset, count); } #endregion internal static readonly System.DateTime _unixEpoch = new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); internal static readonly System.Text.Encoding iso8859dash1 = System.Text.Encoding.GetEncoding("iso-8859-1"); private int EmitHeader() { byte[] commentBytes = (Comment == null) ? null : iso8859dash1.GetBytes(Comment); byte[] filenameBytes = (FileName == null) ? null : iso8859dash1.GetBytes(FileName); int cbLength = (Comment == null) ? 0 : commentBytes.Length + 1; int fnLength = (FileName == null) ? 0 : filenameBytes.Length + 1; int bufferLength = 10 + cbLength + fnLength; byte[] header = BufferPool.Get(bufferLength, true); int i = 0; // ID header[i++] = 0x1F; header[i++] = 0x8B; // compression method header[i++] = 8; byte flag = 0; if (Comment != null) flag ^= 0x10; if (FileName != null) flag ^= 0x8; // flag header[i++] = flag; // mtime if (!LastModified.HasValue) LastModified = DateTime.Now; System.TimeSpan delta = LastModified.Value - _unixEpoch; Int32 timet = (Int32)delta.TotalSeconds; Array.Copy(BitConverter.GetBytes(timet), 0, header, i, 4); i += 4; // xflg header[i++] = 0; // this field is totally useless // OS header[i++] = 0xFF; // 0xFF == unspecified // extra field length - only if FEXTRA is set, which it is not. //header[i++]= 0; //header[i++]= 0; // filename if (fnLength != 0) { Array.Copy(filenameBytes, 0, header, i, fnLength - 1); i += fnLength - 1; header[i++] = 0; // terminate } // comment if (cbLength != 0) { Array.Copy(commentBytes, 0, header, i, cbLength - 1); i += cbLength - 1; header[i++] = 0; // terminate } _baseStream._stream.Write(header, 0, i); int headerLength = header.Length; BufferPool.Release(header); return headerLength; // bytes written } } }