// 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;
public partial class ZipEngine
{
///
/// Extracts files from a zip archive or archive chain.
///
/// A context interface to handle opening
/// and closing of archive and file streams.
/// An optional predicate that can determine
/// which files to process.
/// 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 void Unpack(
IUnpackStreamContext streamContext,
Predicate fileFilter)
{
if (streamContext == null)
{
throw new ArgumentNullException("streamContext");
}
lock (this)
{
IList allHeaders = this.GetCentralDirectory(streamContext);
if (allHeaders == null)
{
throw new ZipException("Zip central directory not found.");
}
IList headers = new List(allHeaders.Count);
foreach (ZipFileHeader header in allHeaders)
{
if (!header.IsDirectory &&
(fileFilter == null || fileFilter(header.fileName)))
{
headers.Add(header);
}
}
this.ResetProgressData();
// Count the total number of files and bytes to be compressed.
this.totalFiles = headers.Count;
foreach (ZipFileHeader header in headers)
{
long compressedSize;
long uncompressedSize;
long localHeaderOffset;
int archiveNumber;
uint crc;
header.GetZip64Fields(
out compressedSize,
out uncompressedSize,
out localHeaderOffset,
out archiveNumber,
out crc);
this.totalFileBytes += uncompressedSize;
if (archiveNumber >= this.totalArchives)
{
this.totalArchives = (short) (archiveNumber + 1);
}
}
this.currentArchiveNumber = -1;
this.currentFileNumber = -1;
Stream archiveStream = null;
try
{
foreach (ZipFileHeader header in headers)
{
this.currentFileNumber++;
this.UnpackOneFile(streamContext, header, ref archiveStream);
}
}
finally
{
if (archiveStream != null)
{
streamContext.CloseArchiveReadStream(
0, String.Empty, archiveStream);
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.FinishArchive);
}
}
}
}
///
/// Unpacks a single file from an archive or archive chain.
///
private void UnpackOneFile(
IUnpackStreamContext streamContext,
ZipFileHeader header,
ref Stream archiveStream)
{
ZipFileInfo fileInfo = null;
Stream fileStream = null;
try
{
Converter compressionStreamCreator;
if (!ZipEngine.decompressionStreamCreators.TryGetValue(
header.compressionMethod, out compressionStreamCreator))
{
// Silently skip files of an unsupported compression method.
return;
}
long compressedSize;
long uncompressedSize;
long localHeaderOffset;
int archiveNumber;
uint crc;
header.GetZip64Fields(
out compressedSize,
out uncompressedSize,
out localHeaderOffset,
out archiveNumber,
out crc);
if (this.currentArchiveNumber != archiveNumber + 1)
{
if (archiveStream != null)
{
streamContext.CloseArchiveReadStream(
this.currentArchiveNumber,
String.Empty,
archiveStream);
archiveStream = null;
this.OnProgress(ArchiveProgressType.FinishArchive);
this.currentArchiveName = null;
}
this.currentArchiveNumber = (short) (archiveNumber + 1);
this.currentArchiveBytesProcessed = 0;
this.currentArchiveTotalBytes = 0;
archiveStream = this.OpenArchive(
streamContext, this.currentArchiveNumber);
FileStream archiveFileStream = archiveStream as FileStream;
this.currentArchiveName = (archiveFileStream != null ?
Path.GetFileName(archiveFileStream.Name) : null);
this.currentArchiveTotalBytes = archiveStream.Length;
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.StartArchive);
this.currentArchiveNumber++;
}
archiveStream.Seek(localHeaderOffset, SeekOrigin.Begin);
ZipFileHeader localHeader = new ZipFileHeader();
if (!localHeader.Read(archiveStream, false) ||
!ZipEngine.AreFilePathsEqual(localHeader.fileName, header.fileName))
{
string msg = "Could not read file: " + header.fileName;
throw new ZipException(msg);
}
fileInfo = header.ToZipFileInfo();
fileStream = streamContext.OpenFileWriteStream(
fileInfo.FullName,
fileInfo.Length,
fileInfo.LastWriteTime);
if (fileStream != null)
{
this.currentFileName = header.fileName;
this.currentFileBytesProcessed = 0;
this.currentFileTotalBytes = fileInfo.Length;
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.StartFile);
this.currentArchiveNumber++;
this.UnpackFileBytes(
streamContext,
fileInfo.FullName,
fileInfo.CompressedLength,
fileInfo.Length,
header.crc32,
fileStream,
compressionStreamCreator,
ref archiveStream);
}
}
finally
{
if (fileStream != null)
{
streamContext.CloseFileWriteStream(
fileInfo.FullName,
fileStream,
fileInfo.Attributes,
fileInfo.LastWriteTime);
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.FinishFile);
this.currentArchiveNumber++;
}
}
}
///
/// Compares two internal file paths while ignoring case and slash differences.
///
/// The first path to compare.
/// The second path to compare.
/// True if the paths are equivalent.
private static bool AreFilePathsEqual(string path1, string path2)
{
path1 = path1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
path2 = path2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return String.Compare(path1, path2, StringComparison.OrdinalIgnoreCase) == 0;
}
private Stream OpenArchive(IUnpackStreamContext streamContext, int archiveNumber)
{
Stream archiveStream = streamContext.OpenArchiveReadStream(
archiveNumber, String.Empty, this);
if (archiveStream == null && archiveNumber != 0)
{
archiveStream = streamContext.OpenArchiveReadStream(
0, String.Empty, this);
}
if (archiveStream == null)
{
throw new FileNotFoundException("Archive stream not provided.");
}
return archiveStream;
}
///
/// Decompresses bytes for one file from an archive or archive chain,
/// checking the crc at the end.
///
private void UnpackFileBytes(
IUnpackStreamContext streamContext,
string fileName,
long compressedSize,
long uncompressedSize,
uint crc,
Stream fileStream,
Converter compressionStreamCreator,
ref Stream archiveStream)
{
CrcStream crcStream = new CrcStream(fileStream);
ConcatStream concatStream = new ConcatStream(
delegate(ConcatStream s)
{
this.currentArchiveBytesProcessed = s.Source.Position;
streamContext.CloseArchiveReadStream(
this.currentArchiveNumber,
String.Empty,
s.Source);
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.FinishArchive);
this.currentArchiveNumber += 2;
this.currentArchiveName = null;
this.currentArchiveBytesProcessed = 0;
this.currentArchiveTotalBytes = 0;
s.Source = this.OpenArchive(streamContext, this.currentArchiveNumber);
FileStream archiveFileStream = s.Source as FileStream;
this.currentArchiveName = (archiveFileStream != null ?
Path.GetFileName(archiveFileStream.Name) : null);
this.currentArchiveTotalBytes = s.Source.Length;
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.StartArchive);
this.currentArchiveNumber++;
});
concatStream.Source = archiveStream;
concatStream.SetLength(compressedSize);
Stream decompressionStream = compressionStreamCreator(concatStream);
try
{
byte[] buf = new byte[4096];
long bytesRemaining = uncompressedSize;
int counter = 0;
while (bytesRemaining > 0)
{
int count = (int) Math.Min(buf.Length, bytesRemaining);
count = decompressionStream.Read(buf, 0, count);
crcStream.Write(buf, 0, count);
bytesRemaining -= count;
this.fileBytesProcessed += count;
this.currentFileBytesProcessed += count;
this.currentArchiveBytesProcessed = concatStream.Source.Position;
if (++counter % 16 == 0) // Report every 64K
{
this.currentArchiveNumber--;
this.OnProgress(ArchiveProgressType.PartialFile);
this.currentArchiveNumber++;
}
}
}
finally
{
archiveStream = concatStream.Source;
}
crcStream.Flush();
if (crcStream.Crc != crc)
{
throw new ZipException("CRC check failed for file: " + fileName);
}
}
}
}