From 3f583916719eeef598d10a5d4e14ef14f008243b Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 11 May 2021 07:36:37 -0700 Subject: Merge Dtf --- .../WixToolset.Dtf.Compression/ArchiveException.cs | 57 ++ .../WixToolset.Dtf.Compression/ArchiveFileInfo.cs | 430 ++++++++++++ .../ArchiveFileStreamContext.cs | 664 ++++++++++++++++++ src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs | 781 +++++++++++++++++++++ .../ArchiveProgressEventArgs.cs | 307 ++++++++ .../ArchiveProgressType.cs | 69 ++ .../BasicUnpackStreamContext.cs | 90 +++ src/dtf/WixToolset.Dtf.Compression/CargoStream.cs | 192 +++++ src/dtf/WixToolset.Dtf.Compression/Compression.cd | 175 +++++ .../CompressionEngine.cs | 371 ++++++++++ .../WixToolset.Dtf.Compression/CompressionLevel.cs | 31 + .../WixToolset.Dtf.Compression/DuplicateStream.cs | 212 ++++++ .../IPackStreamContext.cs | 117 +++ .../IUnpackStreamContext.cs | 71 ++ src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs | 206 ++++++ .../SafeNativeMethods.cs | 22 + .../WixToolset.Dtf.Compression.csproj | 21 + 17 files changed, 3816 insertions(+) create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/CargoStream.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/Compression.cd create mode 100644 src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs create mode 100644 src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj (limited to 'src/dtf/WixToolset.Dtf.Compression') diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs new file mode 100644 index 00000000..a53e862c --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs @@ -0,0 +1,57 @@ +// 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 +{ + using System; + using System.IO; + using System.Runtime.Serialization; + + /// + /// Base exception class for compression operations. Compression libraries should + /// derive subclass exceptions with more specific error information relevent to the + /// file format. + /// + [Serializable] + public class ArchiveException : IOException + { + /// + /// Creates a new ArchiveException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public ArchiveException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Creates a new ArchiveException with a specified error message. + /// + /// The message that describes the error. + public ArchiveException(string message) + : this(message, null) + { + } + + /// + /// Creates a new ArchiveException. + /// + public ArchiveException() + : this(null, null) + { + } + + /// + /// Initializes a new instance of the ArchiveException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected ArchiveException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs new file mode 100644 index 00000000..adcae3ec --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs @@ -0,0 +1,430 @@ +// 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 +{ + using System; + using System.IO; + using System.Runtime.Serialization; + using System.Diagnostics.CodeAnalysis; + + /// + /// Abstract object representing a compressed file within an archive; + /// provides operations for getting the file properties and unpacking + /// the file. + /// + [Serializable] + public abstract class ArchiveFileInfo : FileSystemInfo + { + private ArchiveInfo archiveInfo; + private string name; + private string path; + + private bool initialized; + private bool exists; + private int archiveNumber; + private FileAttributes attributes; + private DateTime lastWriteTime; + private long length; + + /// + /// Creates a new ArchiveFileInfo object representing a file within + /// an archive in a specified path. + /// + /// An object representing the archive + /// containing the file. + /// The path to the file within the archive. + /// Usually, this is a simple file name, but if the archive contains + /// a directory structure this may include the directory. + protected ArchiveFileInfo(ArchiveInfo archiveInfo, string filePath) + : base() + { + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + this.Archive = archiveInfo; + + this.name = System.IO.Path.GetFileName(filePath); + this.path = System.IO.Path.GetDirectoryName(filePath); + + this.attributes = FileAttributes.Normal; + this.lastWriteTime = DateTime.MinValue; + } + + /// + /// Creates a new ArchiveFileInfo object with all parameters specified; + /// used by subclasses when reading the metadata out of an archive. + /// + /// The internal path and name of the file in + /// the archive. + /// The archive number where the file + /// starts. + /// The stored attributes of the file. + /// The stored last write time of the + /// file. + /// The uncompressed size of the file. + protected ArchiveFileInfo( + string filePath, + int archiveNumber, + FileAttributes attributes, + DateTime lastWriteTime, + long length) + : this(null, filePath) + { + this.exists = true; + this.archiveNumber = archiveNumber; + this.attributes = attributes; + this.lastWriteTime = lastWriteTime; + this.length = length; + this.initialized = true; + } + + /// + /// Initializes a new instance of the ArchiveFileInfo class with + /// serialized data. + /// + /// The SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// The StreamingContext that contains contextual + /// information about the source or destination. + protected ArchiveFileInfo(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.archiveInfo = (ArchiveInfo) info.GetValue( + "archiveInfo", typeof(ArchiveInfo)); + this.name = info.GetString("name"); + this.path = info.GetString("path"); + this.initialized = info.GetBoolean("initialized"); + this.exists = info.GetBoolean("exists"); + this.archiveNumber = info.GetInt32("archiveNumber"); + this.attributes = (FileAttributes) info.GetValue( + "attributes", typeof(FileAttributes)); + this.lastWriteTime = info.GetDateTime("lastWriteTime"); + this.length = info.GetInt64("length"); + } + + /// + /// Gets the name of the file. + /// + /// The name of the file, not including any path. + public override string Name + { + get + { + return this.name; + } + } + + /// + /// Gets the internal path of the file in the archive. + /// + /// The internal path of the file in the archive, not including + /// the file name. + public string Path + { + get + { + return this.path; + } + } + + /// + /// Gets the full path to the file. + /// + /// The full path to the file, including the full path to the + /// archive, the internal path in the archive, and the file name. + /// + /// For example, the path "C:\archive.cab\file.txt" refers to + /// a file "file.txt" inside the archive "archive.cab". + /// + public override string FullName + { + get + { + string fullName = System.IO.Path.Combine(this.Path, this.Name); + + if (this.Archive != null) + { + fullName = System.IO.Path.Combine(this.ArchiveName, fullName); + } + + return fullName; + } + } + + /// + /// Gets or sets the archive that contains this file. + /// + /// + /// The ArchiveInfo instance that retrieved this file information -- this + /// may be null if the ArchiveFileInfo object was returned directly from + /// a stream. + /// + public ArchiveInfo Archive + { + get + { + return (ArchiveInfo) this.archiveInfo; + } + + internal set + { + this.archiveInfo = value; + + // protected instance members inherited from FileSystemInfo: + this.OriginalPath = (value != null ? value.FullName : null); + this.FullPath = this.OriginalPath; + } + } + + /// + /// Gets the full path of the archive that contains this file. + /// + /// The full path of the archive that contains this file. + public string ArchiveName + { + get + { + return this.Archive != null ? this.Archive.FullName : null; + } + } + + /// + /// Gets the number of the archive where this file starts. + /// + /// The number of the archive where this file starts. + /// A single archive or the first archive in a chain is + /// numbered 0. + public int ArchiveNumber + { + get + { + return this.archiveNumber; + } + } + + /// + /// Checks if the file exists within the archive. + /// + /// True if the file exists, false otherwise. + public override bool Exists + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.exists; + } + } + + /// + /// Gets the uncompressed size of the file. + /// + /// The uncompressed size of the file in bytes. + public long Length + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.length; + } + } + + /// + /// Gets the attributes of the file. + /// + /// The attributes of the file as stored in the archive. + public new FileAttributes Attributes + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.attributes; + } + } + + /// + /// Gets the last modification time of the file. + /// + /// The last modification time of the file as stored in the + /// archive. + public new DateTime LastWriteTime + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.lastWriteTime; + } + } + + /// + /// Sets the SerializationInfo with information about the archive. + /// + /// The SerializationInfo that holds the serialized + /// object data. + /// The StreamingContext that contains contextual + /// information about the source or destination. + public override void GetObjectData( + SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("archiveInfo", this.archiveInfo); + info.AddValue("name", this.name); + info.AddValue("path", this.path); + info.AddValue("initialized", this.initialized); + info.AddValue("exists", this.exists); + info.AddValue("archiveNumber", this.archiveNumber); + info.AddValue("attributes", this.attributes); + info.AddValue("lastWriteTime", this.lastWriteTime); + info.AddValue("length", this.length); + } + + /// + /// Gets the full path to the file. + /// + /// The same as + public override string ToString() + { + return this.FullName; + } + + /// + /// Deletes the file. NOT SUPPORTED. + /// + /// Files cannot be deleted + /// from an existing archive. + public override void Delete() + { + throw new NotSupportedException(); + } + + /// + /// Refreshes the attributes and other cached information about the file, + /// by re-reading the information from the archive. + /// + public new void Refresh() + { + base.Refresh(); + + if (this.Archive != null) + { + string filePath = System.IO.Path.Combine(this.Path, this.Name); + ArchiveFileInfo updatedFile = this.Archive.GetFile(filePath); + if (updatedFile == null) + { + throw new FileNotFoundException( + "File not found in archive.", filePath); + } + + this.Refresh(updatedFile); + } + } + + /// + /// Extracts the file. + /// + /// The destination path where the file + /// will be extracted. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName) + { + this.CopyTo(destFileName, false); + } + + /// + /// Extracts the file, optionally overwriting any existing file. + /// + /// The destination path where the file + /// will be extracted. + /// If true, + /// will be overwritten if it exists. + /// is false + /// and exists. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName, bool overwrite) + { + if (destFileName == null) + { + throw new ArgumentNullException("destFileName"); + } + + if (!overwrite && File.Exists(destFileName)) + { + throw new IOException(); + } + + if (this.Archive == null) + { + throw new InvalidOperationException(); + } + + this.Archive.UnpackFile( + System.IO.Path.Combine(this.Path, this.Name), destFileName); + } + + /// + /// Opens the archive file for reading without actually extracting the + /// file to disk. + /// + /// + /// A stream for reading directly from the packed file. Like any stream + /// this should be closed/disposed as soon as it is no longer needed. + /// + public Stream OpenRead() + { + return this.Archive.OpenRead(System.IO.Path.Combine(this.Path, this.Name)); + } + + /// + /// Opens the archive file reading text with UTF-8 encoding without + /// actually extracting the file to disk. + /// + /// + /// A reader for reading text directly from the packed file. Like any reader + /// this should be closed/disposed as soon as it is no longer needed. + /// + /// + /// To open an archived text file with different encoding, use the + /// method and pass the returned stream to one of + /// the constructor overloads. + /// + public StreamReader OpenText() + { + return this.Archive.OpenText(System.IO.Path.Combine(this.Path, this.Name)); + } + + /// + /// Refreshes the information in this object with new data retrieved + /// from an archive. + /// + /// Fresh instance for the same file just + /// read from the archive. + /// + /// Subclasses may override this method to refresh sublcass fields. + /// However they should always call the base implementation first. + /// + protected virtual void Refresh(ArchiveFileInfo newFileInfo) + { + this.exists = newFileInfo.exists; + this.length = newFileInfo.length; + this.attributes = newFileInfo.attributes; + this.lastWriteTime = newFileInfo.lastWriteTime; + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs new file mode 100644 index 00000000..8be3bff4 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs @@ -0,0 +1,664 @@ +// 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 +{ + using System; + using System.IO; + using System.Collections.Generic; + + /// + /// Provides a basic implementation of the archive pack and unpack stream context + /// interfaces, based on a list of archive files, a default directory, and an + /// optional mapping from internal to external file paths. + /// + /// + /// This class can also handle creating or extracting chained archive packages. + /// + public class ArchiveFileStreamContext + : IPackStreamContext, IUnpackStreamContext + { + private IList archiveFiles; + private string directory; + private IDictionary files; + private bool extractOnlyNewerFiles; + private bool enableOffsetOpen; + + #region Constructors + + /// + /// Creates a new ArchiveFileStreamContext with a archive file and + /// no default directory or file mapping. + /// + /// The path to a archive file that will be + /// created or extracted. + public ArchiveFileStreamContext(string archiveFile) + : this(archiveFile, null, null) + { + } + + /// + /// Creates a new ArchiveFileStreamContext with a archive file, default + /// directory and mapping from internal to external file paths. + /// + /// The path to a archive file that will be + /// created or extracted. + /// The default root directory where files will be + /// located, optional. + /// A mapping from internal file paths to external file + /// paths, optional. + /// + /// If the mapping is not null and a file is not included in the mapping, + /// the file will be skipped. + /// If the external path in the mapping is a simple file name or + /// relative file path, it will be concatenated onto the default directory, + /// if one was specified. + /// For more about how the default directory and files mapping are + /// used, see and + /// . + /// + public ArchiveFileStreamContext( + string archiveFile, + string directory, + IDictionary files) + : this(new string[] { archiveFile }, directory, files) + { + if (archiveFile == null) + { + throw new ArgumentNullException("archiveFile"); + } + } + + /// + /// Creates a new ArchiveFileStreamContext with a list of archive files, + /// a default directory and a mapping from internal to external file paths. + /// + /// A list of paths to archive files that will be + /// created or extracted. + /// The default root directory where files will be + /// located, optional. + /// A mapping from internal file paths to external file + /// paths, optional. + /// + /// When creating chained archives, the list + /// should include at least enough archives to handle the entire set of + /// input files, based on the maximum archive size that is passed to the + /// .. + /// If the mapping is not null and a file is not included in the mapping, + /// the file will be skipped. + /// If the external path in the mapping is a simple file name or + /// relative file path, it will be concatenated onto the default directory, + /// if one was specified. + /// For more about how the default directory and files mapping are used, + /// see and + /// . + /// + public ArchiveFileStreamContext( + IList archiveFiles, + string directory, + IDictionary files) + { + if (archiveFiles == null || archiveFiles.Count == 0) + { + throw new ArgumentNullException("archiveFiles"); + } + + this.archiveFiles = archiveFiles; + this.directory = directory; + this.files = files; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the list of archive files that are created or extracted. + /// + /// The list of archive files that are created or extracted. + public IList ArchiveFiles + { + get + { + return this.archiveFiles; + } + } + + /// + /// Gets or sets the default root directory where files are located. + /// + /// The default root directory where files are located. + /// + /// For details about how the default directory is used, + /// see and . + /// + public string Directory + { + get + { + return this.directory; + } + } + + /// + /// Gets or sets the mapping from internal file paths to external file paths. + /// + /// A mapping from internal file paths to external file paths. + /// + /// For details about how the files mapping is used, + /// see and . + /// + public IDictionary Files + { + get + { + return this.files; + } + } + + /// + /// Gets or sets a flag that can prevent extracted files from overwriting + /// newer files that already exist. + /// + /// True to prevent overwriting newer files that already exist + /// during extraction; false to always extract from the archive regardless + /// of existing files. + public bool ExtractOnlyNewerFiles + { + get + { + return this.extractOnlyNewerFiles; + } + + set + { + this.extractOnlyNewerFiles = value; + } + } + + /// + /// Gets or sets a flag that enables creating or extracting an archive + /// at an offset within an existing file. (This is typically used to open + /// archive-based self-extracting packages.) + /// + /// True to search an existing package file for an archive offset + /// or the end of the file;/ false to always create or open a plain + /// archive file. + public bool EnableOffsetOpen + { + get + { + return this.enableOffsetOpen; + } + + set + { + this.enableOffsetOpen = value; + } + } + + #endregion + + #region IPackStreamContext Members + + /// + /// Gets the name of the archive with a specified number. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the requested archive. May be an empty string + /// for non-chained archives, but may never be null. + /// This method returns the file name of the archive from the + /// list with the specified index, or an empty + /// string if the archive number is outside the bounds of the list. The + /// file name should not include any directory path. + public virtual string GetArchiveName(int archiveNumber) + { + if (archiveNumber < this.archiveFiles.Count) + { + return Path.GetFileName(this.archiveFiles[archiveNumber]); + } + + return String.Empty; + } + + /// + /// Opens a stream for writing an archive. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was returned + /// by . + /// True if the stream should be truncated when + /// opened (if it already exists); false if an existing stream is being + /// re-opened for writing additional data. + /// Instance of the compression engine + /// doing the operations. + /// A writable Stream where the compressed archive bytes will be + /// written, or null to cancel the archive creation. + /// + /// This method opens the file from the list + /// with the specified index. If the archive number is outside the bounds + /// of the list, this method returns null. + /// If the flag is set, this method + /// will seek to the start of any existing archive in the file, or to the + /// end of the file if the existing file is not an archive. + /// + public virtual Stream OpenArchiveWriteStream( + int archiveNumber, + string archiveName, + bool truncate, + CompressionEngine compressionEngine) + { + if (archiveNumber >= this.archiveFiles.Count) + { + return null; + } + + if (String.IsNullOrEmpty(archiveName)) + { + throw new ArgumentNullException("archiveName"); + } + + // All archives must be in the same directory, + // so always use the directory from the first archive. + string archiveFile = Path.Combine( + Path.GetDirectoryName(this.archiveFiles[0]), archiveName); + Stream stream = File.Open( + archiveFile, + (truncate ? FileMode.OpenOrCreate : FileMode.Open), + FileAccess.ReadWrite); + + if (this.enableOffsetOpen) + { + long offset = compressionEngine.FindArchiveOffset( + new DuplicateStream(stream)); + + // If this is not an archive file, append the archive to it. + if (offset < 0) + { + offset = stream.Length; + } + + if (offset > 0) + { + stream = new OffsetStream(stream, offset); + } + + stream.Seek(0, SeekOrigin.Begin); + } + + if (truncate) + { + // Truncate the stream, in case a larger old archive starts here. + stream.SetLength(0); + } + + return stream; + } + + /// + /// Closes a stream where an archive package was written. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was previously + /// returned by . + /// A stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseArchiveWriteStream( + int archiveNumber, + string archiveName, + Stream stream) + { + if (stream != null) + { + stream.Close(); + + FileStream fileStream = stream as FileStream; + if (fileStream != null) + { + string streamFile = fileStream.Name; + if (!String.IsNullOrEmpty(archiveName) && + archiveName != Path.GetFileName(streamFile)) + { + string archiveFile = Path.Combine( + Path.GetDirectoryName(this.archiveFiles[0]), archiveName); + if (File.Exists(archiveFile)) + { + File.Delete(archiveFile); + } + File.Move(streamFile, archiveFile); + } + } + } + } + + /// + /// Opens a stream to read a file that is to be included in an archive. + /// + /// The path of the file within the archive. + /// The returned attributes of the opened file, + /// to be stored in the archive. + /// The returned last-modified time of the + /// opened file, to be stored in the archive. + /// A readable Stream where the file bytes will be read from + /// before they are compressed, or null to skip inclusion of the file and + /// continue to the next file. + /// + /// This method opens a file using the following logic: + /// + /// If the and the mapping + /// are both null, the path is treated as relative to the current directory, + /// and that file is opened. + /// If the is not null but the + /// mapping is null, the path is treated as relative to that directory, and + /// that file is opened. + /// If the is null but the + /// mapping is not null, the path parameter is used as a key into the mapping, + /// and the resulting value is the file path that is opened, relative to the + /// current directory (or it may be an absolute path). If no mapping exists, + /// the file is skipped. + /// If both the and the + /// mapping are specified, the path parameter is used as a key into the + /// mapping, and the resulting value is the file path that is opened, relative + /// to the specified directory (or it may be an absolute path). If no mapping + /// exists, the file is skipped. + /// + /// + public virtual Stream OpenFileReadStream( + string path, out FileAttributes attributes, out DateTime lastWriteTime) + { + string filePath = this.TranslateFilePath(path); + + if (filePath == null) + { + attributes = FileAttributes.Normal; + lastWriteTime = DateTime.Now; + return null; + } + + attributes = File.GetAttributes(filePath); + lastWriteTime = File.GetLastWriteTime(filePath); + return File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + /// + /// Closes a stream that has been used to read a file. + /// + /// The path of the file within the archive; the same as + /// the path provided when the stream was opened. + /// A stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseFileReadStream(string path, Stream stream) + { + if (stream != null) + { + stream.Close(); + } + } + + /// + /// Gets extended parameter information specific to the compression format + /// being used. + /// + /// Name of the option being requested. + /// Parameters for the option; for per-file options, + /// the first parameter is typically the internal file path. + /// Option value, or null to use the default behavior. + /// + /// This implementation does not handle any options. Subclasses may override + /// this method to allow for non-default behavior. + /// + public virtual object GetOption(string optionName, object[] parameters) + { + return null; + } + + #endregion + + #region IUnpackStreamContext Members + + /// + /// Opens the archive stream for reading. + /// + /// The zero-based index of the archive to + /// open. + /// The name of the archive being opened. + /// Instance of the compression engine + /// doing the operations. + /// A stream from which archive bytes are read, or null to cancel + /// extraction of the archive. + /// + /// This method opens the file from the list with + /// the specified index. If the archive number is outside the bounds of the + /// list, this method returns null. + /// If the flag is set, this method will + /// seek to the start of any existing archive in the file, or to the end of + /// the file if the existing file is not an archive. + /// + public virtual Stream OpenArchiveReadStream( + int archiveNumber, string archiveName, CompressionEngine compressionEngine) + { + if (archiveNumber >= this.archiveFiles.Count) + { + return null; + } + + string archiveFile = this.archiveFiles[archiveNumber]; + Stream stream = File.Open( + archiveFile, FileMode.Open, FileAccess.Read, FileShare.Read); + + if (this.enableOffsetOpen) + { + long offset = compressionEngine.FindArchiveOffset( + new DuplicateStream(stream)); + if (offset > 0) + { + stream = new OffsetStream(stream, offset); + } + else + { + stream.Seek(0, SeekOrigin.Begin); + } + } + + return stream; + } + + /// + /// Closes a stream where an archive was read. + /// + /// The archive number of the stream + /// to close. + /// The name of the archive being closed. + /// The stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseArchiveReadStream( + int archiveNumber, string archiveName, Stream stream) + { + if (stream != null) + { + stream.Close(); + } + } + + /// + /// Opens a stream for writing extracted file bytes. + /// + /// The path of the file within the archive. + /// The uncompressed size of the file to be + /// extracted. + /// The last write time of the file to be + /// extracted. + /// A stream where extracted file bytes are to be written, or null + /// to skip extraction of the file and continue to the next file. + /// + /// This method opens a file using the following logic: + /// + /// If the and the mapping + /// are both null, the path is treated as relative to the current directory, + /// and that file is opened. + /// If the is not null but the + /// mapping is null, the path is treated as relative to that directory, and + /// that file is opened. + /// If the is null but the + /// mapping is not null, the path parameter is used as a key into the mapping, + /// and the resulting value is the file path that is opened, relative to the + /// current directory (or it may be an absolute path). If no mapping exists, + /// the file is skipped. + /// If both the and the + /// mapping are specified, the path parameter is used as a key into the + /// mapping, and the resulting value is the file path that is opened, + /// relative to the specified directory (or it may be an absolute path). + /// If no mapping exists, the file is skipped. + /// + /// If the flag is set, the file + /// is skipped if a file currently exists in the same path with an equal + /// or newer write time. + /// + public virtual Stream OpenFileWriteStream( + string path, + long fileSize, + DateTime lastWriteTime) + { + string filePath = this.TranslateFilePath(path); + + if (filePath == null) + { + return null; + } + + FileInfo file = new FileInfo(filePath); + if (file.Exists) + { + if (this.extractOnlyNewerFiles && lastWriteTime != DateTime.MinValue) + { + if (file.LastWriteTime >= lastWriteTime) + { + return null; + } + } + + // Clear attributes that will prevent overwriting the file. + // (The final attributes will be set after the file is unpacked.) + FileAttributes attributesToClear = + FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System; + if ((file.Attributes & attributesToClear) != 0) + { + file.Attributes &= ~attributesToClear; + } + } + + if (!file.Directory.Exists) + { + file.Directory.Create(); + } + + return File.Open( + filePath, FileMode.Create, FileAccess.Write, FileShare.None); + } + + /// + /// Closes a stream where an extracted file was written. + /// + /// The path of the file within the archive. + /// The stream that was previously returned by + /// and is now ready to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + /// + /// After closing the extracted file stream, this method applies the date + /// and attributes to that file. + /// + public virtual void CloseFileWriteStream( + string path, + Stream stream, + FileAttributes attributes, + DateTime lastWriteTime) + { + if (stream != null) + { + stream.Close(); + } + + string filePath = this.TranslateFilePath(path); + if (filePath != null) + { + FileInfo file = new FileInfo(filePath); + + if (lastWriteTime != DateTime.MinValue) + { + try + { + file.LastWriteTime = lastWriteTime; + } + catch (ArgumentException) + { + } + catch (IOException) + { + } + } + + try + { + file.Attributes = attributes; + } + catch (IOException) + { + } + } + } + + #endregion + + #region Private utility methods + + /// + /// Translates an internal file path to an external file path using the + /// and the mapping, according to + /// rules documented in and + /// . + /// + /// The path of the file with the archive. + /// The external path of the file, or null if there is no + /// valid translation. + private string TranslateFilePath(string path) + { + string filePath; + if (this.files != null) + { + filePath = this.files[path]; + } + else + { + this.ValidateArchivePath(path); + + filePath = path; + } + + if (filePath != null) + { + if (this.directory != null) + { + filePath = Path.Combine(this.directory, filePath); + } + } + + return filePath; + } + + private void ValidateArchivePath(string filePath) + { + string basePath = Path.GetFullPath(String.IsNullOrEmpty(this.directory) ? Environment.CurrentDirectory : this.directory); + string path = Path.GetFullPath(Path.Combine(basePath, filePath)); + if (!path.StartsWith(basePath, StringComparison.InvariantCultureIgnoreCase)) + { + throw new InvalidDataException("Archive cannot contain files with absolute or traversal paths."); + } + } + + #endregion + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs new file mode 100644 index 00000000..b5da4ea8 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs @@ -0,0 +1,781 @@ +// 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 +{ + using System; + using System.IO; + using System.Collections.Generic; + using System.Globalization; + using System.Text; + using System.Text.RegularExpressions; + using System.Runtime.Serialization; + using System.Diagnostics.CodeAnalysis; + + /// + /// Abstract object representing a compressed archive on disk; + /// provides access to file-based operations on the archive. + /// + [Serializable] + public abstract class ArchiveInfo : FileSystemInfo + { + /// + /// Creates a new ArchiveInfo object representing an archive in a + /// specified path. + /// + /// The path to the archive. When creating an archive, + /// this file does not necessarily exist yet. + protected ArchiveInfo(string path) : base() + { + if (path == null) + { + throw new ArgumentNullException("path"); + } + + // protected instance members inherited from FileSystemInfo: + this.OriginalPath = path; + this.FullPath = Path.GetFullPath(path); + } + + /// + /// Initializes a new instance of the ArchiveInfo class with serialized data. + /// + /// The SerializationInfo that holds the serialized object + /// data about the exception being thrown. + /// The StreamingContext that contains contextual + /// information about the source or destination. + protected ArchiveInfo(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Gets the directory that contains the archive. + /// + /// A DirectoryInfo object representing the parent directory of the + /// archive. + public DirectoryInfo Directory + { + get + { + return new DirectoryInfo(Path.GetDirectoryName(this.FullName)); + } + } + + /// + /// Gets the full path of the directory that contains the archive. + /// + /// The full path of the directory that contains the archive. + public string DirectoryName + { + get + { + return Path.GetDirectoryName(this.FullName); + } + } + + /// + /// Gets the size of the archive. + /// + /// The size of the archive in bytes. + public long Length + { + get + { + return new FileInfo(this.FullName).Length; + } + } + + /// + /// Gets the file name of the archive. + /// + /// The file name of the archive, not including any path. + public override string Name + { + get + { + return Path.GetFileName(this.FullName); + } + } + + /// + /// Checks if the archive exists. + /// + /// True if the archive exists; else false. + public override bool Exists + { + get + { + return File.Exists(this.FullName); + } + } + + /// + /// Gets the full path of the archive. + /// + /// The full path of the archive. + public override string ToString() + { + return this.FullName; + } + + /// + /// Deletes the archive. + /// + public override void Delete() + { + File.Delete(this.FullName); + } + + /// + /// Copies an existing archive to another location. + /// + /// The destination file path. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName) + { + File.Copy(this.FullName, destFileName); + } + + /// + /// Copies an existing archive to another location, optionally + /// overwriting the destination file. + /// + /// The destination file path. + /// If true, the destination file will be + /// overwritten if it exists. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName, bool overwrite) + { + File.Copy(this.FullName, destFileName, overwrite); + } + + /// + /// Moves an existing archive to another location. + /// + /// The destination file path. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void MoveTo(string destFileName) + { + File.Move(this.FullName, destFileName); + this.FullPath = Path.GetFullPath(destFileName); + } + + /// + /// Checks if the archive contains a valid archive header. + /// + /// True if the file is a valid archive; false otherwise. + public bool IsValid() + { + using (Stream stream = File.OpenRead(this.FullName)) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + return compressionEngine.FindArchiveOffset(stream) >= 0; + } + } + } + + /// + /// Gets information about the files contained in the archive. + /// + /// A list of objects, each + /// containing information about a file in the archive. + public IList GetFiles() + { + return this.InternalGetFiles((Predicate) null); + } + + /// + /// Gets information about the certain files contained in the archive file. + /// + /// The search string, such as + /// "*.txt". + /// A list of objects, each containing + /// information about a file in the archive. + public IList GetFiles(string searchPattern) + { + if (searchPattern == null) + { + throw new ArgumentNullException("searchPattern"); + } + + string regexPattern = String.Format( + CultureInfo.InvariantCulture, + "^{0}$", + Regex.Escape(searchPattern).Replace("\\*", ".*").Replace("\\?", ".")); + Regex regex = new Regex( + regexPattern, + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + return this.InternalGetFiles( + delegate(string match) + { + return regex.IsMatch(match); + }); + } + + /// + /// Extracts all files from an archive to a destination directory. + /// + /// Directory where the files are to be + /// extracted. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void Unpack(string destDirectory) + { + this.Unpack(destDirectory, null); + } + + /// + /// Extracts all files from an archive to a destination directory, + /// optionally extracting only newer files. + /// + /// Directory where the files are to be + /// extracted. + /// Handler for receiving progress + /// information; this may be null if progress is not desired. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void Unpack( + string destDirectory, + EventHandler progressHandler) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, destDirectory, null); + streamContext.EnableOffsetOpen = true; + compressionEngine.Unpack(streamContext, null); + } + } + + /// + /// Extracts a single file from the archive. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// The path where the file is to be + /// extracted on disk. + /// If already exists, + /// it will be overwritten. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFile(string fileName, string destFileName) + { + if (fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + if (destFileName == null) + { + throw new ArgumentNullException("destFileName"); + } + + this.UnpackFiles( + new string[] { fileName }, + null, + new string[] { destFileName }); + } + + /// + /// Extracts multiple files from the archive. + /// + /// The names of the files in the archive. + /// Each name includes the internal path of the file, if any. File name + /// matching is case-insensitive. + /// This parameter may be null, but if + /// specified it is the root directory for any relative paths in + /// . + /// The paths where the files are to be + /// extracted on disk. If this parameter is null, the files will be + /// extracted with the names from the archive. + /// + /// If any extracted files already exist on disk, they will be overwritten. + ///

The and + /// parameters cannot both be null.

+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFiles( + IList fileNames, + string destDirectory, + IList destFileNames) + { + this.UnpackFiles(fileNames, destDirectory, destFileNames, null); + } + + /// + /// Extracts multiple files from the archive, optionally extracting + /// only newer files. + /// + /// The names of the files in the archive. + /// Each name includes the internal path of the file, if any. File name + /// matching is case-insensitive. + /// This parameter may be null, but if + /// specified it is the root directory for any relative paths in + /// . + /// The paths where the files are to be + /// extracted on disk. If this parameter is null, the files will be + /// extracted with the names from the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// If any extracted files already exist on disk, they will be overwritten. + ///

The and + /// parameters cannot both be null.

+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFiles( + IList fileNames, + string destDirectory, + IList destFileNames, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + if (destFileNames == null) + { + if (destDirectory == null) + { + throw new ArgumentNullException("destFileNames"); + } + + destFileNames = fileNames; + } + + if (destFileNames.Count != fileNames.Count) + { + throw new ArgumentOutOfRangeException("destFileNames"); + } + + IDictionary files = + ArchiveInfo.CreateStringDictionary(fileNames, destFileNames); + this.UnpackFileSet(files, destDirectory, progressHandler); + } + + /// + /// Extracts multiple files from the archive. + /// + /// A mapping from internal file paths to + /// external file paths. Case-senstivity when matching internal paths + /// depends on the IDictionary implementation. + /// This parameter may be null, but if + /// specified it is the root directory for any relative external paths + /// in . + /// + /// If any extracted files already exist on disk, they will be overwritten. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFileSet( + IDictionary fileNames, + string destDirectory) + { + this.UnpackFileSet(fileNames, destDirectory, null); + } + + /// + /// Extracts multiple files from the archive. + /// + /// A mapping from internal file paths to + /// external file paths. Case-senstivity when matching internal + /// paths depends on the IDictionary implementation. + /// This parameter may be null, but if + /// specified it is the root directory for any relative external + /// paths in . + /// Handler for receiving progress + /// information; this may be null if progress is not desired. + /// + /// If any extracted files already exist on disk, they will be overwritten. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFileSet( + IDictionary fileNames, + string destDirectory, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, destDirectory, fileNames); + streamContext.EnableOffsetOpen = true; + compressionEngine.Unpack( + streamContext, + delegate(string match) + { + return fileNames.ContainsKey(match); + }); + } + } + + /// + /// Opens a file inside the archive for reading without actually + /// extracting the file to disk. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// + /// A stream for reading directly from the packed file. Like any stream + /// this should be closed/disposed as soon as it is no longer needed. + /// + public Stream OpenRead(string fileName) + { + Stream archiveStream = File.OpenRead(this.FullName); + CompressionEngine compressionEngine = this.CreateCompressionEngine(); + Stream fileStream = compressionEngine.Unpack(archiveStream, fileName); + + // Attach the archiveStream and compressionEngine to the + // fileStream so they get disposed when the fileStream is disposed. + return new CargoStream(fileStream, archiveStream, compressionEngine); + } + + /// + /// Opens a file inside the archive for reading text with UTF-8 encoding + /// without actually extracting the file to disk. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// + /// A reader for reading text directly from the packed file. Like any reader + /// this should be closed/disposed as soon as it is no longer needed. + /// + /// + /// To open an archived text file with different encoding, use the + /// method and pass the returned stream to one of + /// the constructor overloads. + /// + public StreamReader OpenText(string fileName) + { + return new StreamReader(this.OpenRead(fileName)); + } + + /// + /// Compresses all files in a directory into the archive. + /// Does not include subdirectories. + /// + /// The directory containing the + /// files to be included. + /// + /// Uses maximum compression level. + /// + public void Pack(string sourceDirectory) + { + this.Pack(sourceDirectory, false, CompressionLevel.Max, null); + } + + /// + /// Compresses all files in a directory into the archive, optionally + /// including subdirectories. + /// + /// This is the root directory + /// for to pack all files. + /// If true, recursively include + /// files in subdirectories. + /// The compression level used when creating + /// the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// The files are stored in the archive using their relative file paths in + /// the directory tree, if supported by the archive file format. + /// + public void Pack( + string sourceDirectory, + bool includeSubdirectories, + CompressionLevel compLevel, + EventHandler progressHandler) + { + IList files = this.GetRelativeFilePathsInDirectoryTree( + sourceDirectory, includeSubdirectories); + this.PackFiles(sourceDirectory, files, files, compLevel, progressHandler); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but + /// if specified it is the root directory + /// for any relative paths in . + /// The list of files to be included in + /// the archive. + /// The names of the files as they are stored + /// in the archive. Each name + /// includes the internal path of the file, if any. This parameter may + /// be null, in which case the files are stored in the archive with their + /// source file names and no path information. + /// + /// Uses maximum compression level. + ///

Duplicate items in the array will cause + /// an .

+ ///
+ public void PackFiles( + string sourceDirectory, + IList sourceFileNames, + IList fileNames) + { + this.PackFiles( + sourceDirectory, + sourceFileNames, + fileNames, + CompressionLevel.Max, + null); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// The list of files to be included in + /// the archive. + /// The names of the files as they are stored in + /// the archive. Each name includes the internal path of the file, if any. + /// This parameter may be null, in which case the files are stored in the + /// archive with their source file names and no path information. + /// The compression level used when creating the + /// archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// Duplicate items in the array will cause + /// an . + /// + public void PackFiles( + string sourceDirectory, + IList sourceFileNames, + IList fileNames, + CompressionLevel compLevel, + EventHandler progressHandler) + { + if (sourceFileNames == null) + { + throw new ArgumentNullException("sourceFileNames"); + } + + if (fileNames == null) + { + string[] fileNamesArray = new string[sourceFileNames.Count]; + for (int i = 0; i < sourceFileNames.Count; i++) + { + fileNamesArray[i] = Path.GetFileName(sourceFileNames[i]); + } + + fileNames = fileNamesArray; + } + else if (fileNames.Count != sourceFileNames.Count) + { + throw new ArgumentOutOfRangeException("fileNames"); + } + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + IDictionary contextFiles = + ArchiveInfo.CreateStringDictionary(fileNames, sourceFileNames); + ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( + this.FullName, sourceDirectory, contextFiles); + streamContext.EnableOffsetOpen = true; + compressionEngine.CompressionLevel = compLevel; + compressionEngine.Pack(streamContext, fileNames); + } + } + + /// + /// Compresses files into the archive, specifying the names used + /// to store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// A mapping from internal file paths to + /// external file paths. + /// + /// Uses maximum compression level. + /// + public void PackFileSet( + string sourceDirectory, + IDictionary fileNames) + { + this.PackFileSet(sourceDirectory, fileNames, CompressionLevel.Max, null); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// A mapping from internal file paths to + /// external file paths. + /// The compression level used when creating + /// the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + public void PackFileSet( + string sourceDirectory, + IDictionary fileNames, + CompressionLevel compLevel, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + string[] fileNamesArray = new string[fileNames.Count]; + fileNames.Keys.CopyTo(fileNamesArray, 0); + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( + this.FullName, sourceDirectory, fileNames); + streamContext.EnableOffsetOpen = true; + compressionEngine.CompressionLevel = compLevel; + compressionEngine.Pack(streamContext, fileNamesArray); + } + } + + /// + /// Given a directory, gets the relative paths of all files in the + /// directory, optionally including all subdirectories. + /// + /// The directory to search. + /// True to include subdirectories + /// in the search. + /// A list of file paths relative to the directory. + internal IList GetRelativeFilePathsInDirectoryTree( + string dir, bool includeSubdirectories) + { + IList fileList = new List(); + this.RecursiveGetRelativeFilePathsInDirectoryTree( + dir, String.Empty, includeSubdirectories, fileList); + return fileList; + } + + /// + /// Retrieves information about one file from this archive. + /// + /// Path of the file in the archive. + /// File information, or null if the file was not found + /// in the archive. + internal ArchiveFileInfo GetFile(string path) + { + IList files = this.InternalGetFiles( + delegate(string match) + { + return String.Compare( + match, path, true, CultureInfo.InvariantCulture) == 0; + }); + return (files != null && files.Count > 0 ? files[0] : null); + } + + /// + /// Creates a compression engine that does the low-level work for + /// this object. + /// + /// A new compression engine instance that matches the specific + /// subclass of archive. + /// + /// Each instance will be d + /// immediately after use. + /// + protected abstract CompressionEngine CreateCompressionEngine(); + + /// + /// Creates a case-insensitive dictionary mapping from one list of + /// strings to the other. + /// + /// List of keys. + /// List of values that are mapped 1-to-1 to + /// the keys. + /// A filled dictionary of the strings. + private static IDictionary CreateStringDictionary( + IList keys, IList values) + { + IDictionary stringDict = + new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < keys.Count; i++) + { + stringDict.Add(keys[i], values[i]); + } + + return stringDict; + } + + /// + /// Recursive-descent helper function for + /// GetRelativeFilePathsInDirectoryTree. + /// + /// The root directory of the search. + /// The relative directory to be + /// processed now. + /// True to descend into + /// subdirectories. + /// List of files found so far. + private void RecursiveGetRelativeFilePathsInDirectoryTree( + string dir, + string relativeDir, + bool includeSubdirectories, + IList fileList) + { + foreach (string file in System.IO.Directory.GetFiles(dir)) + { + string fileName = Path.GetFileName(file); + fileList.Add(Path.Combine(relativeDir, fileName)); + } + + if (includeSubdirectories) + { + foreach (string subDir in System.IO.Directory.GetDirectories(dir)) + { + string subDirName = Path.GetFileName(subDir); + this.RecursiveGetRelativeFilePathsInDirectoryTree( + Path.Combine(dir, subDirName), + Path.Combine(relativeDir, subDirName), + includeSubdirectories, + fileList); + } + } + } + + /// + /// Uses a CompressionEngine to get ArchiveFileInfo objects from this + /// archive, and then associates them with this ArchiveInfo instance. + /// + /// Optional predicate that can determine + /// which files to process. + /// A list of objects, each + /// containing information about a file in the archive. + private IList InternalGetFiles(Predicate fileFilter) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, null, null); + streamContext.EnableOffsetOpen = true; + IList files = + compressionEngine.GetFileInfo(streamContext, fileFilter); + for (int i = 0; i < files.Count; i++) + { + files[i].Archive = this; + } + + return files; + } + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs new file mode 100644 index 00000000..5d96d714 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs @@ -0,0 +1,307 @@ +// 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 +{ +using System; +using System.Collections.Generic; +using System.Text; + + /// + /// Contains the data reported in an archive progress event. + /// + public class ArchiveProgressEventArgs : EventArgs + { + private ArchiveProgressType progressType; + + private string currentFileName; + private int currentFileNumber; + private int totalFiles; + private long currentFileBytesProcessed; + private long currentFileTotalBytes; + + private string currentArchiveName; + private short currentArchiveNumber; + private short totalArchives; + private long currentArchiveBytesProcessed; + private long currentArchiveTotalBytes; + + private long fileBytesProcessed; + private long totalFileBytes; + + /// + /// Creates a new ArchiveProgressEventArgs object from specified event parameters. + /// + /// type of status message + /// name of the file being processed + /// number of the current file being processed + /// total number of files to be processed + /// number of bytes processed so far when compressing or extracting a file + /// total number of bytes in the current file + /// name of the current Archive + /// current Archive number, when processing a chained set of Archives + /// total number of Archives in a chained set + /// number of compressed bytes processed so far during an extraction + /// total number of compressed bytes to be processed during an extraction + /// number of uncompressed file bytes processed so far + /// total number of uncompressed file bytes to be processed + public ArchiveProgressEventArgs( + ArchiveProgressType progressType, + string currentFileName, + int currentFileNumber, + int totalFiles, + long currentFileBytesProcessed, + long currentFileTotalBytes, + string currentArchiveName, + int currentArchiveNumber, + int totalArchives, + long currentArchiveBytesProcessed, + long currentArchiveTotalBytes, + long fileBytesProcessed, + long totalFileBytes) + { + this.progressType = progressType; + this.currentFileName = currentFileName; + this.currentFileNumber = currentFileNumber; + this.totalFiles = totalFiles; + this.currentFileBytesProcessed = currentFileBytesProcessed; + this.currentFileTotalBytes = currentFileTotalBytes; + this.currentArchiveName = currentArchiveName; + this.currentArchiveNumber = (short) currentArchiveNumber; + this.totalArchives = (short) totalArchives; + this.currentArchiveBytesProcessed = currentArchiveBytesProcessed; + this.currentArchiveTotalBytes = currentArchiveTotalBytes; + this.fileBytesProcessed = fileBytesProcessed; + this.totalFileBytes = totalFileBytes; + } + + /// + /// Gets the type of status message. + /// + /// A value indicating what type of progress event occurred. + /// + /// The handler may choose to ignore some types of progress events. + /// For example, if the handler will only list each file as it is + /// compressed/extracted, it can ignore events that + /// are not of type . + /// + public ArchiveProgressType ProgressType + { + get + { + return this.progressType; + } + } + + /// + /// Gets the name of the file being processed. (The name of the file within the Archive; not the external + /// file path.) Also includes the internal path of the file, if any. Valid for + /// , , + /// and messages. + /// + /// The name of the file currently being processed, or null if processing + /// is currently at the stream or archive level. + public string CurrentFileName + { + get + { + return this.currentFileName; + } + } + + /// + /// Gets the number of the current file being processed. The first file is number 0, and the last file + /// is -1. Valid for , + /// , and messages. + /// + /// The number of the file currently being processed, or the most recent + /// file processed if processing is currently at the stream or archive level. + public int CurrentFileNumber + { + get + { + return this.currentFileNumber; + } + } + + /// + /// Gets the total number of files to be processed. Valid for all message types. + /// + /// The total number of files to be processed that are known so far. + public int TotalFiles + { + get + { + return this.totalFiles; + } + } + + /// + /// Gets the number of bytes processed so far when compressing or extracting a file. Valid for + /// , , + /// and messages. + /// + /// The number of uncompressed bytes processed so far for the current file, + /// or 0 if processing is currently at the stream or archive level. + public long CurrentFileBytesProcessed + { + get + { + return this.currentFileBytesProcessed; + } + } + + /// + /// Gets the total number of bytes in the current file. Valid for , + /// , and messages. + /// + /// The uncompressed size of the current file being processed, + /// or 0 if processing is currently at the stream or archive level. + public long CurrentFileTotalBytes + { + get + { + return this.currentFileTotalBytes; + } + } + + /// + /// Gets the name of the current archive. Not necessarily the name of the archive on disk. + /// Valid for all message types. + /// + /// The name of the current archive, or an empty string if no name was specified. + public string CurrentArchiveName + { + get + { + return this.currentArchiveName; + } + } + + /// + /// Gets the current archive number, when processing a chained set of archives. Valid for all message types. + /// + /// The number of the current archive. + /// The first archive is number 0, and the last archive is + /// -1. + public int CurrentArchiveNumber + { + get + { + return this.currentArchiveNumber; + } + } + + /// + /// Gets the total number of known archives in a chained set. Valid for all message types. + /// + /// The total number of known archives in a chained set. + /// + /// When using the compression option to auto-split into multiple archives based on data size, + /// this value will not be accurate until the end. + /// + public int TotalArchives + { + get + { + return this.totalArchives; + } + } + + /// + /// Gets the number of compressed bytes processed so far during extraction + /// of the current archive. Valid for all extraction messages. + /// + /// The number of compressed bytes processed so far during extraction + /// of the current archive. + public long CurrentArchiveBytesProcessed + { + get + { + return this.currentArchiveBytesProcessed; + } + } + + /// + /// Gets the total number of compressed bytes to be processed during extraction + /// of the current archive. Valid for all extraction messages. + /// + /// The total number of compressed bytes to be processed during extraction + /// of the current archive. + public long CurrentArchiveTotalBytes + { + get + { + return this.currentArchiveTotalBytes; + } + } + + /// + /// Gets the number of uncompressed bytes processed so far among all files. Valid for all message types. + /// + /// The number of uncompressed file bytes processed so far among all files. + /// + /// When compared to , this can be used as a measure of overall progress. + /// + public long FileBytesProcessed + { + get + { + return this.fileBytesProcessed; + } + } + + /// + /// Gets the total number of uncompressed file bytes to be processed. Valid for all message types. + /// + /// The total number of uncompressed bytes to be processed among all files. + public long TotalFileBytes + { + get + { + return this.totalFileBytes; + } + } + +#if DEBUG + + /// + /// Creates a string representation of the progress event. + /// + /// a listing of all event parameters and values + public override string ToString() + { + string formatString = + "{0}\n" + + "\t CurrentFileName = {1}\n" + + "\t CurrentFileNumber = {2}\n" + + "\t TotalFiles = {3}\n" + + "\t CurrentFileBytesProcessed = {4}\n" + + "\t CurrentFileTotalBytes = {5}\n" + + "\t CurrentArchiveName = {6}\n" + + "\t CurrentArchiveNumber = {7}\n" + + "\t TotalArchives = {8}\n" + + "\t CurrentArchiveBytesProcessed = {9}\n" + + "\t CurrentArchiveTotalBytes = {10}\n" + + "\t FileBytesProcessed = {11}\n" + + "\t TotalFileBytes = {12}\n"; + return String.Format( + System.Globalization.CultureInfo.InvariantCulture, + formatString, + this.ProgressType, + this.CurrentFileName, + this.CurrentFileNumber, + this.TotalFiles, + this.CurrentFileBytesProcessed, + this.CurrentFileTotalBytes, + this.CurrentArchiveName, + this.CurrentArchiveNumber, + this.TotalArchives, + this.CurrentArchiveBytesProcessed, + this.CurrentArchiveTotalBytes, + this.FileBytesProcessed, + this.TotalFileBytes); + } + +#endif + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs new file mode 100644 index 00000000..2307c28e --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs @@ -0,0 +1,69 @@ +// 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 +{ +using System; +using System.Collections.Generic; +using System.Text; + + /// + /// The type of progress event. + /// + /// + ///

PACKING EXAMPLE: The following sequence of events might be received when + /// extracting a simple archive file with 2 files.

+ /// + /// Message TypeDescription + /// StartArchive Begin extracting archive + /// StartFile Begin extracting first file + /// PartialFile Extracting first file + /// PartialFile Extracting first file + /// FinishFile Finished extracting first file + /// StartFile Begin extracting second file + /// PartialFile Extracting second file + /// FinishFile Finished extracting second file + /// FinishArchiveFinished extracting archive + /// + ///

+ ///

UNPACKING EXAMPLE: Packing 3 files into 2 archive chunks, where the second file is + /// continued to the second archive chunk.

+ /// + /// Message TypeDescription + /// StartFile Begin compressing first file + /// FinishFile Finished compressing first file + /// StartFile Begin compressing second file + /// PartialFile Compressing second file + /// PartialFile Compressing second file + /// FinishFile Finished compressing second file + /// StartArchive Begin writing first archive + /// PartialArchiveWriting first archive + /// FinishArchive Finished writing first archive + /// StartFile Begin compressing third file + /// PartialFile Compressing third file + /// FinishFile Finished compressing third file + /// StartArchive Begin writing second archive + /// PartialArchiveWriting second archive + /// FinishArchive Finished writing second archive + /// + ///
+ public enum ArchiveProgressType : int + { + /// Status message before beginning the packing or unpacking an individual file. + StartFile, + + /// Status message (possibly reported multiple times) during the process of packing or unpacking a file. + PartialFile, + + /// Status message after completion of the packing or unpacking an individual file. + FinishFile, + + /// Status message before beginning the packing or unpacking an archive. + StartArchive, + + /// Status message (possibly reported multiple times) during the process of packing or unpacking an archiv. + PartialArchive, + + /// Status message after completion of the packing or unpacking of an archive. + FinishArchive, + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs new file mode 100644 index 00000000..94d13b9c --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs @@ -0,0 +1,90 @@ +// 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 +{ + using System; + using System.IO; + using System.Diagnostics.CodeAnalysis; + + /// + /// Stream context used to extract a single file from an archive into a memory stream. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + public class BasicUnpackStreamContext : IUnpackStreamContext + { + private Stream archiveStream; + private Stream fileStream; + + /// + /// Creates a new BasicExtractStreamContext that reads from the specified archive stream. + /// + /// Archive stream to read from. + public BasicUnpackStreamContext(Stream archiveStream) + { + this.archiveStream = archiveStream; + } + + /// + /// Gets the stream for the extracted file, or null if no file was extracted. + /// + public Stream FileStream + { + get + { + return this.fileStream; + } + } + + /// + /// Opens the archive stream for reading. Returns a DuplicateStream instance, + /// so the stream may be virtually opened multiple times. + /// + /// The archive number to open (ignored; 0 is assumed). + /// The name of the archive being opened. + /// Instance of the compression engine doing the operations. + /// A stream from which archive bytes are read. + public Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine) + { + return new DuplicateStream(this.archiveStream); + } + + /// + /// Does *not* close the stream. The archive stream should be managed by + /// the code that invokes the archive extraction. + /// + /// The archive number of the stream to close. + /// The name of the archive being closed. + /// The stream being closed. + public void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream) + { + // Do nothing. + } + + /// + /// Opens a stream for writing extracted file bytes. The returned stream is a MemoryStream + /// instance, so the file is extracted straight into memory. + /// + /// Path of the file within the archive. + /// The uncompressed size of the file to be extracted. + /// The last write time of the file. + /// A stream where extracted file bytes are to be written. + public Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime) + { + this.fileStream = new MemoryStream(new byte[fileSize], 0, (int) fileSize, true, true); + return this.fileStream; + } + + /// + /// Does *not* close the file stream. The file stream is saved in memory so it can + /// be read later. + /// + /// Path of the file within the archive. + /// The file stream to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + public void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime) + { + // Do nothing. + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs b/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs new file mode 100644 index 00000000..78798a35 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs @@ -0,0 +1,192 @@ +// 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 +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Wraps a source stream and carries additional items that are disposed when the stream is closed. + /// + public class CargoStream : Stream + { + private Stream source; + private List cargo; + + /// + /// Creates a new a cargo stream. + /// + /// source of the stream + /// List of additional items that are disposed when the stream is closed. + /// The order of the list is the order in which the items are disposed. + public CargoStream(Stream source, params IDisposable[] cargo) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = source; + this.cargo = new List(cargo); + } + + /// + /// Gets the source stream of the cargo stream. + /// + public Stream Source + { + get + { + return this.source; + } + } + + /// + /// Gets the list of additional items that are disposed when the stream is closed. + /// The order of the list is the order in which the items are disposed. The contents can be modified any time. + /// + public IList Cargo + { + get + { + return this.cargo; + } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the length of the source stream. + /// + public override long Length + { + get + { + return this.source.Length; + } + } + + /// + /// Gets or sets the position of the source stream. + /// + public override long Position + { + get + { + return this.source.Position; + } + + set + { + this.source.Position = value; + } + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the length of the source stream. + /// + /// The desired length of the stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value); + } + + /// + /// Closes the source stream and also closes the additional objects that are carried. + /// + public override void Close() + { + this.source.Close(); + + foreach (IDisposable cargoObject in this.cargo) + { + cargoObject.Dispose(); + } + } + + /// + /// Reads from the source stream. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the stream. + /// The maximum number of bytes to be read from the stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + return this.source.Read(buffer, offset, count); + } + + /// + /// Writes to the source stream. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the stream. + /// The number of bytes to be written to the stream. + public override void Write(byte[] buffer, int offset, int count) + { + this.source.Write(buffer, offset, count); + } + + /// + /// Changes the position of the source stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference + /// point used to obtain the new position. + /// The new position within the stream. + public override long Seek(long offset, SeekOrigin origin) + { + return this.source.Seek(offset, origin); + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/Compression.cd b/src/dtf/WixToolset.Dtf.Compression/Compression.cd new file mode 100644 index 00000000..95012be0 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/Compression.cd @@ -0,0 +1,175 @@ + + + + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + ArchiveException.cs + + + + + + + + AAAgAAAAIRJAAIMEAEACgARwAAEEEAAAASAAAAEAIAA= + ArchiveFileInfo.cs + + + + + + + + AAEAABAAIAAAAgQEAAgBAARAHAEJACAAAABEAAkAMAI= + ArchiveInfo.cs + + + + + + + + AEQAABgAAACQAACACACAAgAQAAIgAAAAACAMgAAEAKA= + ArchiveFileStreamContext.cs + + + + + + + + + AAMCAQASACAAABBBAAASUAAAQBAAAMAAAAGQAAgBEAA= + ArchiveProgressEventArgs.cs + + + + + + + + AAAAAAgAAACEAAAAAAAAAAAAAAAgAAAAIAAMAAAAAAA= + BasicUnpackStreamContext.cs + + + + + + + + + AAAEAAAABCBAACRgAAAAAAQAAEAAAAAAQAEAAAiAAAI= + CompressionEngine.cs + + + + + + + AAAAAEAAAgAAQAIgGAAAIABgAAAAAAAAAAAAAAGIACA= + DuplicateStream.cs + + + + + + AAAAAAAAAgAAQAIgGAAAAABgAAAAAEAgAAAAAAGIwCA= + OffsetStream.cs + + + + + + AAAAAAAAAAAAAACAAACAAAAQAAAgAAAAACAIAAAAAAA= + IPackStreamContext.cs + + + + + + AAAAAAgAAACAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAA= + IUnpackStreamContext.cs + + + + + + QAAAAAAAAAAAAIAAgAAAAAAAAAQAAAAACIAAAAAAAAA= + ArchiveProgressType.cs + + + + + + AAAAAAAAABAAAAAAEAAAAAAAAAAIAAAAAAAAAAEAAAA= + CompressionLevel.cs + + + + \ No newline at end of file diff --git a/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs b/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs new file mode 100644 index 00000000..7758ea98 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs @@ -0,0 +1,371 @@ +// 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 +{ +using System; +using System.IO; +using System.Collections.Generic; +using System.Globalization; + + /// + /// Base class for an engine capable of packing and unpacking a particular + /// compressed file format. + /// + public abstract class CompressionEngine : IDisposable + { + private CompressionLevel compressionLevel; + private bool dontUseTempFiles; + + /// + /// Creates a new instance of the compression engine base class. + /// + protected CompressionEngine() + { + this.compressionLevel = CompressionLevel.Normal; + } + + /// + /// Disposes the compression engine. + /// + ~CompressionEngine() + { + this.Dispose(false); + } + + /// + /// Occurs when the compression engine reports progress in packing + /// or unpacking an archive. + /// + /// + public event EventHandler Progress; + + /// + /// Gets or sets a flag indicating whether temporary files are created + /// and used during compression. + /// + /// True if temporary files are used; false if compression is done + /// entirely in-memory. + /// The value of this property is true by default. Using temporary + /// files can greatly reduce the memory requirement of compression, + /// especially when compressing large archives. However, setting this property + /// to false may yield slightly better performance when creating small + /// archives. Or it may be necessary if the process does not have sufficient + /// privileges to create temporary files. + public bool UseTempFiles + { + get + { + return !this.dontUseTempFiles; + } + + set + { + this.dontUseTempFiles = !value; + } + } + + /// + /// Compression level to use when compressing files. + /// + /// A compression level ranging from minimum to maximum compression, + /// or no compression. + public CompressionLevel CompressionLevel + { + get + { + return this.compressionLevel; + } + + set + { + this.compressionLevel = value; + } + } + + /// + /// Disposes of resources allocated by the compression engine. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Creates an archive. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// The paths of the files in the archive + /// (not external file paths). + /// The archive could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the + /// file paths within the archive to the external file paths. + /// + public void Pack(IPackStreamContext streamContext, IEnumerable files) + { + if (files == null) + { + throw new ArgumentNullException("files"); + } + + this.Pack(streamContext, files, 0); + } + + /// + /// Creates an archive or chain of archives. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// The paths of the files in the archive (not + /// external file paths). + /// The maximum number of bytes for one + /// archive before the contents are chained to the next archive, or zero + /// for unlimited archive size. + /// The archive could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the file + /// paths within the archive to the external file paths. + /// + public abstract void Pack( + IPackStreamContext streamContext, + IEnumerable files, + long maxArchiveSize); + + /// + /// Checks whether a Stream begins with a header that indicates + /// it is a valid archive. + /// + /// Stream for reading the archive file. + /// True if the stream is a valid archive + /// (with no offset); false otherwise. + public abstract bool IsArchive(Stream stream); + + /// + /// 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 virtual long FindArchiveOffset(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + long sectionSize = 4; + long length = stream.Length; + for (long offset = 0; offset <= length - sectionSize; offset += sectionSize) + { + stream.Seek(offset, SeekOrigin.Begin); + if (this.IsArchive(stream)) + { + return offset; + } + } + + return -1; + } + + /// + /// Gets information about all files in an archive stream. + /// + /// A stream for reading the archive. + /// Information about all files in the archive stream. + /// The stream is not a valid + /// archive. + public IList GetFileInfo(Stream stream) + { + return this.GetFileInfo(new BasicUnpackStreamContext(stream), null); + } + + /// + /// Gets information about files in an 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 abstract IList GetFileInfo( + IUnpackStreamContext streamContext, + Predicate fileFilter); + + /// + /// Gets the list of files in an archive Stream. + /// + /// A stream for reading the archive. + /// A list of the paths of all files contained in the + /// archive. + /// The stream is not a valid + /// archive. + public IList GetFiles(Stream stream) + { + return this.GetFiles(new BasicUnpackStreamContext(stream), null); + } + + /// + /// Gets the list of files in an 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. + /// An array containing the names of all files contained in + /// the archive or archive chain. + /// 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 IList GetFiles( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + IList files = + this.GetFileInfo(streamContext, fileFilter); + IList fileNames = new List(files.Count); + for (int i = 0; i < files.Count; i++) + { + fileNames.Add(files[i].Name); + } + + return fileNames; + } + + /// + /// Reads a single file from an archive stream. + /// + /// A stream for reading the archive. + /// The path of the file within the archive + /// (not the external file path). + /// A stream for reading the extracted file, or null + /// if the file does not exist in the archive. + /// The stream is not a valid + /// archive. + /// The entire extracted file is cached in memory, so this + /// method requires enough free memory to hold the file. + public Stream Unpack(Stream stream, string path) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + if (path == null) + { + throw new ArgumentNullException("path"); + } + + BasicUnpackStreamContext streamContext = + new BasicUnpackStreamContext(stream); + this.Unpack( + streamContext, + delegate(string match) + { + return String.Compare( + match, path, true, CultureInfo.InvariantCulture) == 0; + }); + + Stream extractStream = streamContext.FileStream; + if (extractStream != null) + { + extractStream.Position = 0; + } + + return extractStream; + } + + /// + /// Extracts files from an 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 abstract void Unpack( + IUnpackStreamContext streamContext, + Predicate fileFilter); + + /// + /// Called by sublcasses to distribute a packing or unpacking progress + /// event to listeners. + /// + /// Event details. + protected void OnProgress(ArchiveProgressEventArgs e) + { + if (this.Progress != null) + { + this.Progress(this, e); + } + } + + /// + /// Disposes of resources allocated by the compression engine. + /// + /// If true, the method has been called + /// directly or indirectly by a user's code, so managed and unmanaged + /// resources will be disposed. If false, the method has been called by + /// the runtime from inside the finalizer, and only unmanaged resources + /// will be disposed. + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Compresion utility function for converting old-style + /// date and time values to a DateTime structure. + /// + public static void DosDateAndTimeToDateTime( + short dosDate, short dosTime, out DateTime dateTime) + { + if (dosDate == 0 && dosTime == 0) + { + dateTime = DateTime.MinValue; + } + else + { + long fileTime; + SafeNativeMethods.DosDateTimeToFileTime(dosDate, dosTime, out fileTime); + dateTime = DateTime.FromFileTimeUtc(fileTime); + dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Local); + } + } + + /// + /// Compresion utility function for converting a DateTime structure + /// to old-style date and time values. + /// + public static void DateTimeToDosDateAndTime( + DateTime dateTime, out short dosDate, out short dosTime) + { + dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc); + long filetime = dateTime.ToFileTimeUtc(); + SafeNativeMethods.FileTimeToDosDateTime(ref filetime, out dosDate, out dosTime); + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs b/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs new file mode 100644 index 00000000..84ec8fc4 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs @@ -0,0 +1,31 @@ +// 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 +{ +using System; +using System.Collections.Generic; +using System.Text; + + /// + /// Specifies the compression level ranging from minimum compresion to + /// maximum compression, or no compression at all. + /// + /// + /// Although only four values are enumerated, any integral value between + /// and can also be used. + /// + public enum CompressionLevel + { + /// Do not compress files, only store. + None = 0, + + /// Minimum compression; fastest. + Min = 1, + + /// A compromize between speed and compression efficiency. + Normal = 6, + + /// Maximum compression; slowest. + Max = 10 + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs b/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs new file mode 100644 index 00000000..50e62e73 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs @@ -0,0 +1,212 @@ +// 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 +{ + using System; + using System.IO; + + /// + /// Duplicates a source stream by maintaining a separate position. + /// + /// + /// WARNING: duplicate streams are not thread-safe with respect to each other or the original stream. + /// If multiple threads use duplicate copies of the same stream, they must synchronize for any operations. + /// + public class DuplicateStream : Stream + { + private Stream source; + private long position; + + /// + /// Creates a new duplicate of a stream. + /// + /// source of the duplicate + public DuplicateStream(Stream source) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = DuplicateStream.OriginalStream(source); + } + + /// + /// Gets the original stream that was used to create the duplicate. + /// + public Stream Source + { + get + { + return this.source; + } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the length of the source stream. + /// + public override long Length + { + get + { + return this.source.Length; + } + } + + /// + /// Gets or sets the position of the current stream, + /// ignoring the position of the source stream. + /// + public override long Position + { + get + { + return this.position; + } + + set + { + this.position = value; + } + } + + /// + /// Retrieves the original stream from a possible duplicate stream. + /// + /// Possible duplicate stream. + /// If the stream is a DuplicateStream, returns + /// the duplicate's source; otherwise returns the same stream. + public static Stream OriginalStream(Stream stream) + { + DuplicateStream dupStream = stream as DuplicateStream; + return dupStream != null ? dupStream.Source : stream; + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the length of the source stream. + /// + /// The desired length of the stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value); + } + + /// + /// Closes the underlying stream, effectively closing ALL duplicates. + /// + public override void Close() + { + this.source.Close(); + } + + /// + /// Reads from the source stream while maintaining a separate position + /// and not impacting the source stream's position. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + long saveSourcePosition = this.source.Position; + this.source.Position = this.position; + int read = this.source.Read(buffer, offset, count); + this.position = this.source.Position; + this.source.Position = saveSourcePosition; + return read; + } + + /// + /// Writes to the source stream while maintaining a separate position + /// and not impacting the source stream's position. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the current stream. + /// The number of bytes to be written to the + /// current stream. + public override void Write(byte[] buffer, int offset, int count) + { + long saveSourcePosition = this.source.Position; + this.source.Position = this.position; + this.source.Write(buffer, offset, count); + this.position = this.source.Position; + this.source.Position = saveSourcePosition; + } + + /// + /// Changes the position of this stream without impacting the + /// source stream's position. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference + /// point used to obtain the new position. + /// The new position within the current stream. + public override long Seek(long offset, SeekOrigin origin) + { + long originPosition = 0; + if (origin == SeekOrigin.Current) + { + originPosition = this.position; + } + else if (origin == SeekOrigin.End) + { + originPosition = this.Length; + } + + this.position = originPosition + offset; + return this.position; + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs new file mode 100644 index 00000000..19d77be5 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs @@ -0,0 +1,117 @@ +// 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 +{ + using System; + using System.IO; + using System.Diagnostics.CodeAnalysis; + + /// + /// This interface provides the methods necessary for the + /// to open and close streams for archives + /// and files. The implementor of this interface can use any kind of logic + /// to determine what kind of streams to open and where. + /// + public interface IPackStreamContext + { + /// + /// Gets the name of the archive with a specified number. + /// + /// The 0-based index of the archive + /// within the chain. + /// The name of the requested archive. May be an empty string + /// for non-chained archives, but may never be null. + /// The archive name is the name stored within the archive, used for + /// identification of the archive especially among archive chains. That + /// name is often, but not necessarily the same as the filename of the + /// archive package. + string GetArchiveName(int archiveNumber); + + /// + /// Opens a stream for writing an archive package. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was returned + /// by . + /// True if the stream should be truncated when + /// opened (if it already exists); false if an existing stream is being + /// re-opened for writing additional data. + /// Instance of the compression engine + /// doing the operations. + /// A writable Stream where the compressed archive bytes will be + /// written, or null to cancel the archive creation. + /// + /// If this method returns null, the archive engine will throw a + /// FileNotFoundException. + /// + Stream OpenArchiveWriteStream( + int archiveNumber, + string archiveName, + bool truncate, + CompressionEngine compressionEngine); + + /// + /// Closes a stream where an archive package was written. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was previously + /// returned by + /// . + /// A stream that was previously returned by + /// and is now ready to be closed. + /// + /// If there is another archive package in the chain, then after this stream + /// is closed a new stream will be opened. + /// + void CloseArchiveWriteStream(int archiveNumber, string archiveName, Stream stream); + + /// + /// Opens a stream to read a file that is to be included in an archive. + /// + /// The path of the file within the archive. This is often, + /// but not necessarily, the same as the relative path of the file outside + /// the archive. + /// Returned attributes of the opened file, to be + /// stored in the archive. + /// Returned last-modified time of the opened file, + /// to be stored in the archive. + /// A readable Stream where the file bytes will be read from before + /// they are compressed, or null to skip inclusion of the file and continue to + /// the next file. + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")] + Stream OpenFileReadStream( + string path, + out FileAttributes attributes, + out DateTime lastWriteTime); + + /// + /// Closes a stream that has been used to read a file. + /// + /// The path of the file within the archive; the same as + /// the path provided + /// when the stream was opened. + /// A stream that was previously returned by + /// and is now ready to be closed. + void CloseFileReadStream(string path, Stream stream); + + /// + /// Gets extended parameter information specific to the compression + /// format being used. + /// + /// Name of the option being requested. + /// Parameters for the option; for per-file options, + /// the first parameter is typically the internal file path. + /// Option value, or null to use the default behavior. + /// + /// This method provides a way to set uncommon options during packaging, or a + /// way to handle aspects of compression formats not supported by the base library. + /// For example, this may be used by the zip compression library to + /// specify different compression methods/levels on a per-file basis. + /// The available option names, parameters, and expected return values + /// should be documented by each compression library. + /// + object GetOption(string optionName, object[] parameters); + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs new file mode 100644 index 00000000..f0bc6aad --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs @@ -0,0 +1,71 @@ +// 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 +{ + using System; + using System.IO; + + /// + /// This interface provides the methods necessary for the to open + /// and close streams for archives and files. The implementor of this interface can use any + /// kind of logic to determine what kind of streams to open and where + /// + public interface IUnpackStreamContext + { + /// + /// Opens the archive stream for reading. + /// + /// The zero-based index of the archive to open. + /// The name of the archive being opened. + /// Instance of the compression engine doing the operations. + /// A stream from which archive bytes are read, or null to cancel extraction + /// of the archive. + /// + /// When the first archive in a chain is opened, the name is not yet known, so the + /// provided value will be an empty string. When opening further archives, the + /// provided value is the next-archive name stored in the previous archive. This + /// name is often, but not necessarily, the same as the filename of the archive + /// package to be opened. + /// If this method returns null, the archive engine will throw a + /// FileNotFoundException. + /// + Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine); + + /// + /// Closes a stream where an archive package was read. + /// + /// The archive number of the stream to close. + /// The name of the archive being closed. + /// The stream that was previously returned by + /// and is now ready to be closed. + void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream); + + /// + /// Opens a stream for writing extracted file bytes. + /// + /// The path of the file within the archive. This is often, but + /// not necessarily, the same as the relative path of the file outside the archive. + /// The uncompressed size of the file to be extracted. + /// The last write time of the file to be extracted. + /// A stream where extracted file bytes are to be written, or null to skip + /// extraction of the file and continue to the next file. + /// + /// The implementor may use the path, size and date information to dynamically + /// decide whether or not the file should be extracted. + /// + Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime); + + /// + /// Closes a stream where an extracted file was written. + /// + /// The path of the file within the archive. + /// The stream that was previously returned by + /// and is now ready to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + /// + /// The implementor may wish to apply the attributes and date to the newly-extracted file. + /// + void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime); + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs b/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs new file mode 100644 index 00000000..65562524 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs @@ -0,0 +1,206 @@ +// 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 +{ + using System; + using System.IO; + + /// + /// Wraps a source stream and offsets all read/write/seek calls by a given value. + /// + /// + /// This class is used to trick archive an packing or unpacking process + /// into reading or writing at an offset into a file, primarily for + /// self-extracting packages. + /// + public class OffsetStream : Stream + { + private Stream source; + private long sourceOffset; + + /// + /// Creates a new OffsetStream instance from a source stream + /// and using a specified offset. + /// + /// Underlying stream for which all calls will be offset. + /// Positive or negative number of bytes to offset. + public OffsetStream(Stream source, long offset) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = source; + this.sourceOffset = offset; + + this.source.Seek(this.sourceOffset, SeekOrigin.Current); + } + + /// + /// Gets the underlying stream that this OffsetStream calls into. + /// + public Stream Source + { + get { return this.source; } + } + + /// + /// Gets the number of bytes to offset all calls before + /// redirecting to the underlying stream. + /// + public long Offset + { + get { return this.sourceOffset; } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the effective length of the stream, which is equal to + /// the length of the source stream minus the offset. + /// + public override long Length + { + get { return this.source.Length - this.sourceOffset; } + } + + /// + /// Gets or sets the effective position of the stream, which + /// is equal to the position of the source stream minus the offset. + /// + public override long Position + { + get { return this.source.Position - this.sourceOffset; } + set { this.source.Position = value + this.sourceOffset; } + } + + /// + /// Reads a sequence of bytes from the source stream and advances + /// the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + return this.source.Read(buffer, offset, count); + } + + /// + /// Writes a sequence of bytes to the source stream and advances the + /// current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the current stream. + /// The number of bytes to be written to the + /// current stream. + public override void Write(byte[] buffer, int offset, int count) + { + this.source.Write(buffer, offset, count); + } + + /// + /// Reads a byte from the stream and advances the position within the + /// source stream by one byte, or returns -1 if at the end of the stream. + /// + /// The unsigned byte cast to an Int32, or -1 if at the + /// end of the stream. + public override int ReadByte() + { + return this.source.ReadByte(); + } + + /// + /// Writes a byte to the current position in the source stream and + /// advances the position within the stream by one byte. + /// + /// The byte to write to the stream. + public override void WriteByte(byte value) + { + this.source.WriteByte(value); + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the position within the current stream, which is + /// equal to the position within the source stream minus the offset. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating + /// the reference point used to obtain the new position. + /// The new position within the current stream. + public override long Seek(long offset, SeekOrigin origin) + { + return this.source.Seek(offset + (origin == SeekOrigin.Begin ? this.sourceOffset : 0), origin) - this.sourceOffset; + } + + /// + /// Sets the effective length of the stream, which is equal to + /// the length of the source stream minus the offset. + /// + /// The desired length of the + /// current stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value + this.sourceOffset); + } + + /// + /// Closes the underlying stream. + /// + public override void Close() + { + this.source.Close(); + } + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs b/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs new file mode 100644 index 00000000..1829ba81 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs @@ -0,0 +1,22 @@ +// 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 +{ + using System; + using System.Security; + using System.Runtime.InteropServices; + + [SuppressUnmanagedCodeSecurity] + internal static class SafeNativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DosDateTimeToFileTime( + short wFatDate, short wFatTime, out long fileTime); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FileTimeToDosDateTime( + ref long fileTime, out short wFatDate, out short wFatTime); + } +} diff --git a/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj b/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj new file mode 100644 index 00000000..e49a446b --- /dev/null +++ b/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj @@ -0,0 +1,21 @@ + + + + + + WixToolset.Dtf.Compression + WixToolset.Dtf.Compression + netstandard2.0;net20 + Abstract base libraries for archive packing and unpacking + true + + + + + + + + + + + -- cgit v1.2.3-55-g6feb