// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Dtf.Compression.Zip { using System; using System.IO; using System.IO.Compression; using System.Collections.Generic; using System.Reflection; using System.Diagnostics.CodeAnalysis; /// /// Engine capable of packing and unpacking archives in the zip format. /// public partial class ZipEngine : CompressionEngine { private static Dictionary> compressionStreamCreators; private static Dictionary> decompressionStreamCreators; private static void InitCompressionStreamCreators() { if (ZipEngine.compressionStreamCreators == null) { ZipEngine.compressionStreamCreators = new Dictionary>(); ZipEngine.decompressionStreamCreators = new Dictionary>(); ZipEngine.RegisterCompressionStreamCreator( ZipCompressionMethod.Store, CompressionMode.Compress, delegate(Stream stream) { return stream; }); ZipEngine.RegisterCompressionStreamCreator( ZipCompressionMethod.Deflate, CompressionMode.Compress, delegate(Stream stream) { return new DeflateStream(stream, CompressionMode.Compress, true); }); ZipEngine.RegisterCompressionStreamCreator( ZipCompressionMethod.Store, CompressionMode.Decompress, delegate(Stream stream) { return stream; }); ZipEngine.RegisterCompressionStreamCreator( ZipCompressionMethod.Deflate, CompressionMode.Decompress, delegate(Stream stream) { return new DeflateStream(stream, CompressionMode.Decompress, true); }); } } /// /// Registers a delegate that can create a warpper stream for /// compressing or uncompressing the data of a source stream. /// /// Compression method being registered. /// Indicates registration for ether /// compress or decompress mode. /// Delegate being registered. /// /// For compression, the delegate accepts a stream that writes to the archive /// and returns a wrapper stream that compresses bytes as they are written. /// For decompression, the delegate accepts a stream that reads from the archive /// and returns a wrapper stream that decompresses bytes as they are read. /// This wrapper stream model follows the design used by /// System.IO.Compression.DeflateStream, and indeed that class is used /// to implement the Deflate compression method by default. /// To unregister a delegate, call this method again and pass /// null for the delegate parameter. /// /// /// When the ZipEngine class is initialized, the Deflate compression method /// is automatically registered like this: /// /// ZipEngine.RegisterCompressionStreamCreator( /// ZipCompressionMethod.Deflate, /// CompressionMode.Compress, /// delegate(Stream stream) { /// return new DeflateStream(stream, CompressionMode.Compress, true); /// }); /// ZipEngine.RegisterCompressionStreamCreator( /// ZipCompressionMethod.Deflate, /// CompressionMode.Decompress, /// delegate(Stream stream) { /// return new DeflateStream(stream, CompressionMode.Decompress, true); /// }); /// public static void RegisterCompressionStreamCreator( ZipCompressionMethod compressionMethod, CompressionMode compressionMode, Converter creator) { ZipEngine.InitCompressionStreamCreators(); if (compressionMode == CompressionMode.Compress) { ZipEngine.compressionStreamCreators[compressionMethod] = creator; } else { ZipEngine.decompressionStreamCreators[compressionMethod] = creator; } } // Progress data private string currentFileName; private int currentFileNumber; private int totalFiles; private long currentFileBytesProcessed; private long currentFileTotalBytes; private string mainArchiveName; private string currentArchiveName; private short currentArchiveNumber; private short totalArchives; private long currentArchiveBytesProcessed; private long currentArchiveTotalBytes; private long fileBytesProcessed; private long totalFileBytes; private string comment; /// /// Creates a new instance of the zip engine. /// public ZipEngine() : base() { ZipEngine.InitCompressionStreamCreators(); } /// /// Gets the comment from the last-examined archive, /// or sets the comment to be added to any created archives. /// public string ArchiveComment { get { return this.comment; } set { this.comment = value; } } /// /// Checks whether a Stream begins with a header that indicates /// it is a valid archive file. /// /// Stream for reading the archive file. /// True if the stream is a valid zip archive /// (with no offset); false otherwise. public override bool IsArchive(Stream stream) { if (stream == null) { throw new ArgumentNullException("stream"); } if (stream.Length - stream.Position < 4) { return false; } BinaryReader reader = new BinaryReader(stream); uint sig = reader.ReadUInt32(); switch (sig) { case ZipFileHeader.LFHSIG: case ZipEndOfCentralDirectory.EOCDSIG: case ZipEndOfCentralDirectory.EOCD64SIG: case ZipFileHeader.SPANSIG: case ZipFileHeader.SPANSIG2: return true; default: return false; } } /// /// Gets the offset of an archive that is positioned 0 or more bytes /// from the start of the Stream. /// /// A stream for reading the archive. /// The offset in bytes of the archive, /// or -1 if no archive is found in the Stream. /// The archive must begin on a 4-byte boundary. public override long FindArchiveOffset(Stream stream) { long offset = base.FindArchiveOffset(stream); if (offset > 0) { // Some self-extract packages include the exe stub in file offset calculations. // Check the first header directory offset to decide whether the entire // archive needs to be offset or not. ZipEndOfCentralDirectory eocd = this.GetEOCD(null, stream); if (eocd != null && eocd.totalEntries > 0) { stream.Seek(eocd.dirOffset, SeekOrigin.Begin); ZipFileHeader header = new ZipFileHeader(); if (header.Read(stream, true) && header.localHeaderOffset < stream.Length) { stream.Seek(header.localHeaderOffset, SeekOrigin.Begin); if (header.Read(stream, false)) { return 0; } } } } return offset; } /// /// Gets information about files in a zip archive or archive chain. /// /// A context interface to handle opening /// and closing of archive and file streams. /// A predicate that can determine /// which files to process, optional. /// Information about files in the archive stream. /// The archive provided /// by the stream context is not valid. /// /// The predicate takes an internal file /// path and returns true to include the file or false to exclude it. /// public override IList GetFileInfo( IUnpackStreamContext streamContext, Predicate fileFilter) { if (streamContext == null) { throw new ArgumentNullException("streamContext"); } lock (this) { IList headers = this.GetCentralDirectory(streamContext); if (headers == null) { throw new ZipException("Zip central directory not found."); } List files = new List(headers.Count); foreach (ZipFileHeader header in headers) { if (!header.IsDirectory && (fileFilter == null || fileFilter(header.fileName))) { files.Add(header.ToZipFileInfo()); } } return files.AsReadOnly(); } } /// /// Reads all the file headers from the central directory in the main archive. /// private IList GetCentralDirectory(IUnpackStreamContext streamContext) { Stream archiveStream = null; this.currentArchiveNumber = 0; try { List headers = new List(); archiveStream = this.OpenArchive(streamContext, 0); ZipEndOfCentralDirectory eocd = this.GetEOCD(streamContext, archiveStream); if (eocd == null) { return null; } else if (eocd.totalEntries == 0) { return headers; } headers.Capacity = (int) eocd.totalEntries; if (eocd.dirOffset > archiveStream.Length - ZipFileHeader.CFH_FIXEDSIZE) { streamContext.CloseArchiveReadStream( this.currentArchiveNumber, String.Empty, archiveStream); archiveStream = null; } else { archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin); uint sig = new BinaryReader(archiveStream).ReadUInt32(); if (sig != ZipFileHeader.CFHSIG) { streamContext.CloseArchiveReadStream( this.currentArchiveNumber, String.Empty, archiveStream); archiveStream = null; } } if (archiveStream == null) { this.currentArchiveNumber = (short) (eocd.dirStartDiskNumber + 1); archiveStream = streamContext.OpenArchiveReadStream( this.currentArchiveNumber, String.Empty, this); if (archiveStream == null) { return null; } } archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin); while (headers.Count < eocd.totalEntries) { ZipFileHeader header = new ZipFileHeader(); if (!header.Read(archiveStream, true)) { throw new ZipException( "Missing or invalid central directory file header"); } headers.Add(header); if (headers.Count < eocd.totalEntries && archiveStream.Position == archiveStream.Length) { streamContext.CloseArchiveReadStream( this.currentArchiveNumber, String.Empty, archiveStream); this.currentArchiveNumber++; archiveStream = streamContext.OpenArchiveReadStream( this.currentArchiveNumber, String.Empty, this); if (archiveStream == null) { this.currentArchiveNumber = 0; archiveStream = streamContext.OpenArchiveReadStream( this.currentArchiveNumber, String.Empty, this); } } } return headers; } finally { if (archiveStream != null) { streamContext.CloseArchiveReadStream( this.currentArchiveNumber, String.Empty, archiveStream); } } } /// /// Locates and reads the end of central directory record near the /// end of the archive. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "streamContext")] private ZipEndOfCentralDirectory GetEOCD( IUnpackStreamContext streamContext, Stream archiveStream) { BinaryReader reader = new BinaryReader(archiveStream); long offset = archiveStream.Length - ZipEndOfCentralDirectory.EOCD_RECORD_FIXEDSIZE; while (offset >= 0) { archiveStream.Seek(offset, SeekOrigin.Begin); uint sig = reader.ReadUInt32(); if (sig == ZipEndOfCentralDirectory.EOCDSIG) { break; } offset--; } if (offset < 0) { return null; } ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory(); archiveStream.Seek(offset, SeekOrigin.Begin); if (!eocd.Read(archiveStream)) { throw new ZipException("Invalid end of central directory record"); } if (eocd.dirOffset == (long) UInt32.MaxValue) { string saveComment = eocd.comment; archiveStream.Seek( offset - Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE, SeekOrigin.Begin); Zip64EndOfCentralDirectoryLocator eocdl = new Zip64EndOfCentralDirectoryLocator(); if (!eocdl.Read(archiveStream)) { throw new ZipException("Missing or invalid end of " + "central directory record locator"); } if (eocdl.dirStartDiskNumber == eocdl.totalDisks - 1) { // ZIP64 eocd is entirely in current stream. archiveStream.Seek(eocdl.dirOffset, SeekOrigin.Begin); if (!eocd.Read(archiveStream)) { throw new ZipException("Missing or invalid ZIP64 end of " + "central directory record"); } } else if (streamContext == null) { return null; } else { // TODO: handle EOCD64 spanning archives! throw new NotImplementedException("Zip implementation does not " + "handle end of central directory record that spans archives."); } eocd.comment = saveComment; } return eocd; } private void ResetProgressData() { this.currentFileName = null; this.currentFileNumber = 0; this.totalFiles = 0; this.currentFileBytesProcessed = 0; this.currentFileTotalBytes = 0; this.currentArchiveName = null; this.currentArchiveNumber = 0; this.totalArchives = 0; this.currentArchiveBytesProcessed = 0; this.currentArchiveTotalBytes = 0; this.fileBytesProcessed = 0; this.totalFileBytes = 0; } private void OnProgress(ArchiveProgressType progressType) { ArchiveProgressEventArgs e = new ArchiveProgressEventArgs( progressType, this.currentFileName, this.currentFileNumber >= 0 ? this.currentFileNumber : 0, this.totalFiles, this.currentFileBytesProcessed, this.currentFileTotalBytes, this.currentArchiveName, this.currentArchiveNumber, this.totalArchives, this.currentArchiveBytesProcessed, this.currentArchiveTotalBytes, this.fileBytesProcessed, this.totalFileBytes); this.OnProgress(e); } } }