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