// 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
}
}
}