From 3f583916719eeef598d10a5d4e14ef14f008243b Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 11 May 2021 07:36:37 -0700 Subject: Merge Dtf --- .../ArchiveFileStreamContext.cs | 664 +++++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs (limited to 'src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs') 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 + } +} -- cgit v1.2.3-55-g6feb