diff options
| author | Rob Mensching <rob@firegiant.com> | 2021-05-11 07:36:37 -0700 |
|---|---|---|
| committer | Rob Mensching <rob@firegiant.com> | 2021-05-11 07:36:37 -0700 |
| commit | 3f583916719eeef598d10a5d4e14ef14f008243b (patch) | |
| tree | 3d528e0ddb5c0550954217c97059d2f19cd6152a /src/dtf/WixToolset.Dtf.Compression | |
| parent | 2e5ab696b8b4666d551b2a0532b95fb7fe6dbe03 (diff) | |
| download | wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.gz wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.bz2 wix-3f583916719eeef598d10a5d4e14ef14f008243b.zip | |
Merge Dtf
Diffstat (limited to 'src/dtf/WixToolset.Dtf.Compression')
17 files changed, 3816 insertions, 0 deletions
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Runtime.Serialization; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Base exception class for compression operations. Compression libraries should | ||
| 11 | /// derive subclass exceptions with more specific error information relevent to the | ||
| 12 | /// file format. | ||
| 13 | /// </summary> | ||
| 14 | [Serializable] | ||
| 15 | public class ArchiveException : IOException | ||
| 16 | { | ||
| 17 | /// <summary> | ||
| 18 | /// Creates a new ArchiveException with a specified error message and a reference to the | ||
| 19 | /// inner exception that is the cause of this exception. | ||
| 20 | /// </summary> | ||
| 21 | /// <param name="message">The message that describes the error.</param> | ||
| 22 | /// <param name="innerException">The exception that is the cause of the current exception. If the | ||
| 23 | /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception | ||
| 24 | /// is raised in a catch block that handles the inner exception.</param> | ||
| 25 | public ArchiveException(string message, Exception innerException) | ||
| 26 | : base(message, innerException) | ||
| 27 | { | ||
| 28 | } | ||
| 29 | |||
| 30 | /// <summary> | ||
| 31 | /// Creates a new ArchiveException with a specified error message. | ||
| 32 | /// </summary> | ||
| 33 | /// <param name="message">The message that describes the error.</param> | ||
| 34 | public ArchiveException(string message) | ||
| 35 | : this(message, null) | ||
| 36 | { | ||
| 37 | } | ||
| 38 | |||
| 39 | /// <summary> | ||
| 40 | /// Creates a new ArchiveException. | ||
| 41 | /// </summary> | ||
| 42 | public ArchiveException() | ||
| 43 | : this(null, null) | ||
| 44 | { | ||
| 45 | } | ||
| 46 | |||
| 47 | /// <summary> | ||
| 48 | /// Initializes a new instance of the ArchiveException class with serialized data. | ||
| 49 | /// </summary> | ||
| 50 | /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param> | ||
| 51 | /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param> | ||
| 52 | protected ArchiveException(SerializationInfo info, StreamingContext context) | ||
| 53 | : base(info, context) | ||
| 54 | { | ||
| 55 | } | ||
| 56 | } | ||
| 57 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Runtime.Serialization; | ||
| 8 | using System.Diagnostics.CodeAnalysis; | ||
| 9 | |||
| 10 | /// <summary> | ||
| 11 | /// Abstract object representing a compressed file within an archive; | ||
| 12 | /// provides operations for getting the file properties and unpacking | ||
| 13 | /// the file. | ||
| 14 | /// </summary> | ||
| 15 | [Serializable] | ||
| 16 | public abstract class ArchiveFileInfo : FileSystemInfo | ||
| 17 | { | ||
| 18 | private ArchiveInfo archiveInfo; | ||
| 19 | private string name; | ||
| 20 | private string path; | ||
| 21 | |||
| 22 | private bool initialized; | ||
| 23 | private bool exists; | ||
| 24 | private int archiveNumber; | ||
| 25 | private FileAttributes attributes; | ||
| 26 | private DateTime lastWriteTime; | ||
| 27 | private long length; | ||
| 28 | |||
| 29 | /// <summary> | ||
| 30 | /// Creates a new ArchiveFileInfo object representing a file within | ||
| 31 | /// an archive in a specified path. | ||
| 32 | /// </summary> | ||
| 33 | /// <param name="archiveInfo">An object representing the archive | ||
| 34 | /// containing the file.</param> | ||
| 35 | /// <param name="filePath">The path to the file within the archive. | ||
| 36 | /// Usually, this is a simple file name, but if the archive contains | ||
| 37 | /// a directory structure this may include the directory.</param> | ||
| 38 | protected ArchiveFileInfo(ArchiveInfo archiveInfo, string filePath) | ||
| 39 | : base() | ||
| 40 | { | ||
| 41 | if (filePath == null) | ||
| 42 | { | ||
| 43 | throw new ArgumentNullException("filePath"); | ||
| 44 | } | ||
| 45 | |||
| 46 | this.Archive = archiveInfo; | ||
| 47 | |||
| 48 | this.name = System.IO.Path.GetFileName(filePath); | ||
| 49 | this.path = System.IO.Path.GetDirectoryName(filePath); | ||
| 50 | |||
| 51 | this.attributes = FileAttributes.Normal; | ||
| 52 | this.lastWriteTime = DateTime.MinValue; | ||
| 53 | } | ||
| 54 | |||
| 55 | /// <summary> | ||
| 56 | /// Creates a new ArchiveFileInfo object with all parameters specified; | ||
| 57 | /// used by subclasses when reading the metadata out of an archive. | ||
| 58 | /// </summary> | ||
| 59 | /// <param name="filePath">The internal path and name of the file in | ||
| 60 | /// the archive.</param> | ||
| 61 | /// <param name="archiveNumber">The archive number where the file | ||
| 62 | /// starts.</param> | ||
| 63 | /// <param name="attributes">The stored attributes of the file.</param> | ||
| 64 | /// <param name="lastWriteTime">The stored last write time of the | ||
| 65 | /// file.</param> | ||
| 66 | /// <param name="length">The uncompressed size of the file.</param> | ||
| 67 | protected ArchiveFileInfo( | ||
| 68 | string filePath, | ||
| 69 | int archiveNumber, | ||
| 70 | FileAttributes attributes, | ||
| 71 | DateTime lastWriteTime, | ||
| 72 | long length) | ||
| 73 | : this(null, filePath) | ||
| 74 | { | ||
| 75 | this.exists = true; | ||
| 76 | this.archiveNumber = archiveNumber; | ||
| 77 | this.attributes = attributes; | ||
| 78 | this.lastWriteTime = lastWriteTime; | ||
| 79 | this.length = length; | ||
| 80 | this.initialized = true; | ||
| 81 | } | ||
| 82 | |||
| 83 | /// <summary> | ||
| 84 | /// Initializes a new instance of the ArchiveFileInfo class with | ||
| 85 | /// serialized data. | ||
| 86 | /// </summary> | ||
| 87 | /// <param name="info">The SerializationInfo that holds the serialized | ||
| 88 | /// object data about the exception being thrown.</param> | ||
| 89 | /// <param name="context">The StreamingContext that contains contextual | ||
| 90 | /// information about the source or destination.</param> | ||
| 91 | protected ArchiveFileInfo(SerializationInfo info, StreamingContext context) | ||
| 92 | : base(info, context) | ||
| 93 | { | ||
| 94 | this.archiveInfo = (ArchiveInfo) info.GetValue( | ||
| 95 | "archiveInfo", typeof(ArchiveInfo)); | ||
| 96 | this.name = info.GetString("name"); | ||
| 97 | this.path = info.GetString("path"); | ||
| 98 | this.initialized = info.GetBoolean("initialized"); | ||
| 99 | this.exists = info.GetBoolean("exists"); | ||
| 100 | this.archiveNumber = info.GetInt32("archiveNumber"); | ||
| 101 | this.attributes = (FileAttributes) info.GetValue( | ||
| 102 | "attributes", typeof(FileAttributes)); | ||
| 103 | this.lastWriteTime = info.GetDateTime("lastWriteTime"); | ||
| 104 | this.length = info.GetInt64("length"); | ||
| 105 | } | ||
| 106 | |||
| 107 | /// <summary> | ||
| 108 | /// Gets the name of the file. | ||
| 109 | /// </summary> | ||
| 110 | /// <value>The name of the file, not including any path.</value> | ||
| 111 | public override string Name | ||
| 112 | { | ||
| 113 | get | ||
| 114 | { | ||
| 115 | return this.name; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | /// <summary> | ||
| 120 | /// Gets the internal path of the file in the archive. | ||
| 121 | /// </summary> | ||
| 122 | /// <value>The internal path of the file in the archive, not including | ||
| 123 | /// the file name.</value> | ||
| 124 | public string Path | ||
| 125 | { | ||
| 126 | get | ||
| 127 | { | ||
| 128 | return this.path; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | /// <summary> | ||
| 133 | /// Gets the full path to the file. | ||
| 134 | /// </summary> | ||
| 135 | /// <value>The full path to the file, including the full path to the | ||
| 136 | /// archive, the internal path in the archive, and the file name.</value> | ||
| 137 | /// <remarks> | ||
| 138 | /// For example, the path <c>"C:\archive.cab\file.txt"</c> refers to | ||
| 139 | /// a file "file.txt" inside the archive "archive.cab". | ||
| 140 | /// </remarks> | ||
| 141 | public override string FullName | ||
| 142 | { | ||
| 143 | get | ||
| 144 | { | ||
| 145 | string fullName = System.IO.Path.Combine(this.Path, this.Name); | ||
| 146 | |||
| 147 | if (this.Archive != null) | ||
| 148 | { | ||
| 149 | fullName = System.IO.Path.Combine(this.ArchiveName, fullName); | ||
| 150 | } | ||
| 151 | |||
| 152 | return fullName; | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | /// <summary> | ||
| 157 | /// Gets or sets the archive that contains this file. | ||
| 158 | /// </summary> | ||
| 159 | /// <value> | ||
| 160 | /// The ArchiveInfo instance that retrieved this file information -- this | ||
| 161 | /// may be null if the ArchiveFileInfo object was returned directly from | ||
| 162 | /// a stream. | ||
| 163 | /// </value> | ||
| 164 | public ArchiveInfo Archive | ||
| 165 | { | ||
| 166 | get | ||
| 167 | { | ||
| 168 | return (ArchiveInfo) this.archiveInfo; | ||
| 169 | } | ||
| 170 | |||
| 171 | internal set | ||
| 172 | { | ||
| 173 | this.archiveInfo = value; | ||
| 174 | |||
| 175 | // protected instance members inherited from FileSystemInfo: | ||
| 176 | this.OriginalPath = (value != null ? value.FullName : null); | ||
| 177 | this.FullPath = this.OriginalPath; | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | /// <summary> | ||
| 182 | /// Gets the full path of the archive that contains this file. | ||
| 183 | /// </summary> | ||
| 184 | /// <value>The full path of the archive that contains this file.</value> | ||
| 185 | public string ArchiveName | ||
| 186 | { | ||
| 187 | get | ||
| 188 | { | ||
| 189 | return this.Archive != null ? this.Archive.FullName : null; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | /// <summary> | ||
| 194 | /// Gets the number of the archive where this file starts. | ||
| 195 | /// </summary> | ||
| 196 | /// <value>The number of the archive where this file starts.</value> | ||
| 197 | /// <remarks>A single archive or the first archive in a chain is | ||
| 198 | /// numbered 0.</remarks> | ||
| 199 | public int ArchiveNumber | ||
| 200 | { | ||
| 201 | get | ||
| 202 | { | ||
| 203 | return this.archiveNumber; | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | /// <summary> | ||
| 208 | /// Checks if the file exists within the archive. | ||
| 209 | /// </summary> | ||
| 210 | /// <value>True if the file exists, false otherwise.</value> | ||
| 211 | public override bool Exists | ||
| 212 | { | ||
| 213 | get | ||
| 214 | { | ||
| 215 | if (!this.initialized) | ||
| 216 | { | ||
| 217 | this.Refresh(); | ||
| 218 | } | ||
| 219 | |||
| 220 | return this.exists; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | /// <summary> | ||
| 225 | /// Gets the uncompressed size of the file. | ||
| 226 | /// </summary> | ||
| 227 | /// <value>The uncompressed size of the file in bytes.</value> | ||
| 228 | public long Length | ||
| 229 | { | ||
| 230 | get | ||
| 231 | { | ||
| 232 | if (!this.initialized) | ||
| 233 | { | ||
| 234 | this.Refresh(); | ||
| 235 | } | ||
| 236 | |||
| 237 | return this.length; | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | /// <summary> | ||
| 242 | /// Gets the attributes of the file. | ||
| 243 | /// </summary> | ||
| 244 | /// <value>The attributes of the file as stored in the archive.</value> | ||
| 245 | public new FileAttributes Attributes | ||
| 246 | { | ||
| 247 | get | ||
| 248 | { | ||
| 249 | if (!this.initialized) | ||
| 250 | { | ||
| 251 | this.Refresh(); | ||
| 252 | } | ||
| 253 | |||
| 254 | return this.attributes; | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | /// <summary> | ||
| 259 | /// Gets the last modification time of the file. | ||
| 260 | /// </summary> | ||
| 261 | /// <value>The last modification time of the file as stored in the | ||
| 262 | /// archive.</value> | ||
| 263 | public new DateTime LastWriteTime | ||
| 264 | { | ||
| 265 | get | ||
| 266 | { | ||
| 267 | if (!this.initialized) | ||
| 268 | { | ||
| 269 | this.Refresh(); | ||
| 270 | } | ||
| 271 | |||
| 272 | return this.lastWriteTime; | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | /// <summary> | ||
| 277 | /// Sets the SerializationInfo with information about the archive. | ||
| 278 | /// </summary> | ||
| 279 | /// <param name="info">The SerializationInfo that holds the serialized | ||
| 280 | /// object data.</param> | ||
| 281 | /// <param name="context">The StreamingContext that contains contextual | ||
| 282 | /// information about the source or destination.</param> | ||
| 283 | public override void GetObjectData( | ||
| 284 | SerializationInfo info, StreamingContext context) | ||
| 285 | { | ||
| 286 | base.GetObjectData(info, context); | ||
| 287 | info.AddValue("archiveInfo", this.archiveInfo); | ||
| 288 | info.AddValue("name", this.name); | ||
| 289 | info.AddValue("path", this.path); | ||
| 290 | info.AddValue("initialized", this.initialized); | ||
| 291 | info.AddValue("exists", this.exists); | ||
| 292 | info.AddValue("archiveNumber", this.archiveNumber); | ||
| 293 | info.AddValue("attributes", this.attributes); | ||
| 294 | info.AddValue("lastWriteTime", this.lastWriteTime); | ||
| 295 | info.AddValue("length", this.length); | ||
| 296 | } | ||
| 297 | |||
| 298 | /// <summary> | ||
| 299 | /// Gets the full path to the file. | ||
| 300 | /// </summary> | ||
| 301 | /// <returns>The same as <see cref="FullName"/></returns> | ||
| 302 | public override string ToString() | ||
| 303 | { | ||
| 304 | return this.FullName; | ||
| 305 | } | ||
| 306 | |||
| 307 | /// <summary> | ||
| 308 | /// Deletes the file. NOT SUPPORTED. | ||
| 309 | /// </summary> | ||
| 310 | /// <exception cref="NotSupportedException">Files cannot be deleted | ||
| 311 | /// from an existing archive.</exception> | ||
| 312 | public override void Delete() | ||
| 313 | { | ||
| 314 | throw new NotSupportedException(); | ||
| 315 | } | ||
| 316 | |||
| 317 | /// <summary> | ||
| 318 | /// Refreshes the attributes and other cached information about the file, | ||
| 319 | /// by re-reading the information from the archive. | ||
| 320 | /// </summary> | ||
| 321 | public new void Refresh() | ||
| 322 | { | ||
| 323 | base.Refresh(); | ||
| 324 | |||
| 325 | if (this.Archive != null) | ||
| 326 | { | ||
| 327 | string filePath = System.IO.Path.Combine(this.Path, this.Name); | ||
| 328 | ArchiveFileInfo updatedFile = this.Archive.GetFile(filePath); | ||
| 329 | if (updatedFile == null) | ||
| 330 | { | ||
| 331 | throw new FileNotFoundException( | ||
| 332 | "File not found in archive.", filePath); | ||
| 333 | } | ||
| 334 | |||
| 335 | this.Refresh(updatedFile); | ||
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | /// <summary> | ||
| 340 | /// Extracts the file. | ||
| 341 | /// </summary> | ||
| 342 | /// <param name="destFileName">The destination path where the file | ||
| 343 | /// will be extracted.</param> | ||
| 344 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 345 | public void CopyTo(string destFileName) | ||
| 346 | { | ||
| 347 | this.CopyTo(destFileName, false); | ||
| 348 | } | ||
| 349 | |||
| 350 | /// <summary> | ||
| 351 | /// Extracts the file, optionally overwriting any existing file. | ||
| 352 | /// </summary> | ||
| 353 | /// <param name="destFileName">The destination path where the file | ||
| 354 | /// will be extracted.</param> | ||
| 355 | /// <param name="overwrite">If true, <paramref name="destFileName"/> | ||
| 356 | /// will be overwritten if it exists.</param> | ||
| 357 | /// <exception cref="IOException"><paramref name="overwrite"/> is false | ||
| 358 | /// and <paramref name="destFileName"/> exists.</exception> | ||
| 359 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 360 | public void CopyTo(string destFileName, bool overwrite) | ||
| 361 | { | ||
| 362 | if (destFileName == null) | ||
| 363 | { | ||
| 364 | throw new ArgumentNullException("destFileName"); | ||
| 365 | } | ||
| 366 | |||
| 367 | if (!overwrite && File.Exists(destFileName)) | ||
| 368 | { | ||
| 369 | throw new IOException(); | ||
| 370 | } | ||
| 371 | |||
| 372 | if (this.Archive == null) | ||
| 373 | { | ||
| 374 | throw new InvalidOperationException(); | ||
| 375 | } | ||
| 376 | |||
| 377 | this.Archive.UnpackFile( | ||
| 378 | System.IO.Path.Combine(this.Path, this.Name), destFileName); | ||
| 379 | } | ||
| 380 | |||
| 381 | /// <summary> | ||
| 382 | /// Opens the archive file for reading without actually extracting the | ||
| 383 | /// file to disk. | ||
| 384 | /// </summary> | ||
| 385 | /// <returns> | ||
| 386 | /// A stream for reading directly from the packed file. Like any stream | ||
| 387 | /// this should be closed/disposed as soon as it is no longer needed. | ||
| 388 | /// </returns> | ||
| 389 | public Stream OpenRead() | ||
| 390 | { | ||
| 391 | return this.Archive.OpenRead(System.IO.Path.Combine(this.Path, this.Name)); | ||
| 392 | } | ||
| 393 | |||
| 394 | /// <summary> | ||
| 395 | /// Opens the archive file reading text with UTF-8 encoding without | ||
| 396 | /// actually extracting the file to disk. | ||
| 397 | /// </summary> | ||
| 398 | /// <returns> | ||
| 399 | /// A reader for reading text directly from the packed file. Like any reader | ||
| 400 | /// this should be closed/disposed as soon as it is no longer needed. | ||
| 401 | /// </returns> | ||
| 402 | /// <remarks> | ||
| 403 | /// To open an archived text file with different encoding, use the | ||
| 404 | /// <see cref="OpenRead" /> method and pass the returned stream to one of | ||
| 405 | /// the <see cref="StreamReader" /> constructor overloads. | ||
| 406 | /// </remarks> | ||
| 407 | public StreamReader OpenText() | ||
| 408 | { | ||
| 409 | return this.Archive.OpenText(System.IO.Path.Combine(this.Path, this.Name)); | ||
| 410 | } | ||
| 411 | |||
| 412 | /// <summary> | ||
| 413 | /// Refreshes the information in this object with new data retrieved | ||
| 414 | /// from an archive. | ||
| 415 | /// </summary> | ||
| 416 | /// <param name="newFileInfo">Fresh instance for the same file just | ||
| 417 | /// read from the archive.</param> | ||
| 418 | /// <remarks> | ||
| 419 | /// Subclasses may override this method to refresh sublcass fields. | ||
| 420 | /// However they should always call the base implementation first. | ||
| 421 | /// </remarks> | ||
| 422 | protected virtual void Refresh(ArchiveFileInfo newFileInfo) | ||
| 423 | { | ||
| 424 | this.exists = newFileInfo.exists; | ||
| 425 | this.length = newFileInfo.length; | ||
| 426 | this.attributes = newFileInfo.attributes; | ||
| 427 | this.lastWriteTime = newFileInfo.lastWriteTime; | ||
| 428 | } | ||
| 429 | } | ||
| 430 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Collections.Generic; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Provides a basic implementation of the archive pack and unpack stream context | ||
| 11 | /// interfaces, based on a list of archive files, a default directory, and an | ||
| 12 | /// optional mapping from internal to external file paths. | ||
| 13 | /// </summary> | ||
| 14 | /// <remarks> | ||
| 15 | /// This class can also handle creating or extracting chained archive packages. | ||
| 16 | /// </remarks> | ||
| 17 | public class ArchiveFileStreamContext | ||
| 18 | : IPackStreamContext, IUnpackStreamContext | ||
| 19 | { | ||
| 20 | private IList<string> archiveFiles; | ||
| 21 | private string directory; | ||
| 22 | private IDictionary<string, string> files; | ||
| 23 | private bool extractOnlyNewerFiles; | ||
| 24 | private bool enableOffsetOpen; | ||
| 25 | |||
| 26 | #region Constructors | ||
| 27 | |||
| 28 | /// <summary> | ||
| 29 | /// Creates a new ArchiveFileStreamContext with a archive file and | ||
| 30 | /// no default directory or file mapping. | ||
| 31 | /// </summary> | ||
| 32 | /// <param name="archiveFile">The path to a archive file that will be | ||
| 33 | /// created or extracted.</param> | ||
| 34 | public ArchiveFileStreamContext(string archiveFile) | ||
| 35 | : this(archiveFile, null, null) | ||
| 36 | { | ||
| 37 | } | ||
| 38 | |||
| 39 | /// <summary> | ||
| 40 | /// Creates a new ArchiveFileStreamContext with a archive file, default | ||
| 41 | /// directory and mapping from internal to external file paths. | ||
| 42 | /// </summary> | ||
| 43 | /// <param name="archiveFile">The path to a archive file that will be | ||
| 44 | /// created or extracted.</param> | ||
| 45 | /// <param name="directory">The default root directory where files will be | ||
| 46 | /// located, optional.</param> | ||
| 47 | /// <param name="files">A mapping from internal file paths to external file | ||
| 48 | /// paths, optional.</param> | ||
| 49 | /// <remarks> | ||
| 50 | /// If the mapping is not null and a file is not included in the mapping, | ||
| 51 | /// the file will be skipped. | ||
| 52 | /// <para>If the external path in the mapping is a simple file name or | ||
| 53 | /// relative file path, it will be concatenated onto the default directory, | ||
| 54 | /// if one was specified.</para> | ||
| 55 | /// <para>For more about how the default directory and files mapping are | ||
| 56 | /// used, see <see cref="OpenFileReadStream"/> and | ||
| 57 | /// <see cref="OpenFileWriteStream"/>.</para> | ||
| 58 | /// </remarks> | ||
| 59 | public ArchiveFileStreamContext( | ||
| 60 | string archiveFile, | ||
| 61 | string directory, | ||
| 62 | IDictionary<string, string> files) | ||
| 63 | : this(new string[] { archiveFile }, directory, files) | ||
| 64 | { | ||
| 65 | if (archiveFile == null) | ||
| 66 | { | ||
| 67 | throw new ArgumentNullException("archiveFile"); | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | /// <summary> | ||
| 72 | /// Creates a new ArchiveFileStreamContext with a list of archive files, | ||
| 73 | /// a default directory and a mapping from internal to external file paths. | ||
| 74 | /// </summary> | ||
| 75 | /// <param name="archiveFiles">A list of paths to archive files that will be | ||
| 76 | /// created or extracted.</param> | ||
| 77 | /// <param name="directory">The default root directory where files will be | ||
| 78 | /// located, optional.</param> | ||
| 79 | /// <param name="files">A mapping from internal file paths to external file | ||
| 80 | /// paths, optional.</param> | ||
| 81 | /// <remarks> | ||
| 82 | /// When creating chained archives, the <paramref name="archiveFiles"/> list | ||
| 83 | /// should include at least enough archives to handle the entire set of | ||
| 84 | /// input files, based on the maximum archive size that is passed to the | ||
| 85 | /// <see cref="CompressionEngine"/>.<see | ||
| 86 | /// cref="CompressionEngine.Pack(IPackStreamContext,IEnumerable<string>,long)"/>. | ||
| 87 | /// <para>If the mapping is not null and a file is not included in the mapping, | ||
| 88 | /// the file will be skipped.</para> | ||
| 89 | /// <para>If the external path in the mapping is a simple file name or | ||
| 90 | /// relative file path, it will be concatenated onto the default directory, | ||
| 91 | /// if one was specified.</para> | ||
| 92 | /// <para>For more about how the default directory and files mapping are used, | ||
| 93 | /// see <see cref="OpenFileReadStream"/> and | ||
| 94 | /// <see cref="OpenFileWriteStream"/>.</para> | ||
| 95 | /// </remarks> | ||
| 96 | public ArchiveFileStreamContext( | ||
| 97 | IList<string> archiveFiles, | ||
| 98 | string directory, | ||
| 99 | IDictionary<string, string> files) | ||
| 100 | { | ||
| 101 | if (archiveFiles == null || archiveFiles.Count == 0) | ||
| 102 | { | ||
| 103 | throw new ArgumentNullException("archiveFiles"); | ||
| 104 | } | ||
| 105 | |||
| 106 | this.archiveFiles = archiveFiles; | ||
| 107 | this.directory = directory; | ||
| 108 | this.files = files; | ||
| 109 | } | ||
| 110 | |||
| 111 | #endregion | ||
| 112 | |||
| 113 | #region Properties | ||
| 114 | |||
| 115 | /// <summary> | ||
| 116 | /// Gets or sets the list of archive files that are created or extracted. | ||
| 117 | /// </summary> | ||
| 118 | /// <value>The list of archive files that are created or extracted.</value> | ||
| 119 | public IList<string> ArchiveFiles | ||
| 120 | { | ||
| 121 | get | ||
| 122 | { | ||
| 123 | return this.archiveFiles; | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | /// <summary> | ||
| 128 | /// Gets or sets the default root directory where files are located. | ||
| 129 | /// </summary> | ||
| 130 | /// <value>The default root directory where files are located.</value> | ||
| 131 | /// <remarks> | ||
| 132 | /// For details about how the default directory is used, | ||
| 133 | /// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>. | ||
| 134 | /// </remarks> | ||
| 135 | public string Directory | ||
| 136 | { | ||
| 137 | get | ||
| 138 | { | ||
| 139 | return this.directory; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | /// <summary> | ||
| 144 | /// Gets or sets the mapping from internal file paths to external file paths. | ||
| 145 | /// </summary> | ||
| 146 | /// <value>A mapping from internal file paths to external file paths.</value> | ||
| 147 | /// <remarks> | ||
| 148 | /// For details about how the files mapping is used, | ||
| 149 | /// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>. | ||
| 150 | /// </remarks> | ||
| 151 | public IDictionary<string, string> Files | ||
| 152 | { | ||
| 153 | get | ||
| 154 | { | ||
| 155 | return this.files; | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | /// <summary> | ||
| 160 | /// Gets or sets a flag that can prevent extracted files from overwriting | ||
| 161 | /// newer files that already exist. | ||
| 162 | /// </summary> | ||
| 163 | /// <value>True to prevent overwriting newer files that already exist | ||
| 164 | /// during extraction; false to always extract from the archive regardless | ||
| 165 | /// of existing files.</value> | ||
| 166 | public bool ExtractOnlyNewerFiles | ||
| 167 | { | ||
| 168 | get | ||
| 169 | { | ||
| 170 | return this.extractOnlyNewerFiles; | ||
| 171 | } | ||
| 172 | |||
| 173 | set | ||
| 174 | { | ||
| 175 | this.extractOnlyNewerFiles = value; | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | /// <summary> | ||
| 180 | /// Gets or sets a flag that enables creating or extracting an archive | ||
| 181 | /// at an offset within an existing file. (This is typically used to open | ||
| 182 | /// archive-based self-extracting packages.) | ||
| 183 | /// </summary> | ||
| 184 | /// <value>True to search an existing package file for an archive offset | ||
| 185 | /// or the end of the file;/ false to always create or open a plain | ||
| 186 | /// archive file.</value> | ||
| 187 | public bool EnableOffsetOpen | ||
| 188 | { | ||
| 189 | get | ||
| 190 | { | ||
| 191 | return this.enableOffsetOpen; | ||
| 192 | } | ||
| 193 | |||
| 194 | set | ||
| 195 | { | ||
| 196 | this.enableOffsetOpen = value; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | #endregion | ||
| 201 | |||
| 202 | #region IPackStreamContext Members | ||
| 203 | |||
| 204 | /// <summary> | ||
| 205 | /// Gets the name of the archive with a specified number. | ||
| 206 | /// </summary> | ||
| 207 | /// <param name="archiveNumber">The 0-based index of the archive within | ||
| 208 | /// the chain.</param> | ||
| 209 | /// <returns>The name of the requested archive. May be an empty string | ||
| 210 | /// for non-chained archives, but may never be null.</returns> | ||
| 211 | /// <remarks>This method returns the file name of the archive from the | ||
| 212 | /// <see cref="archiveFiles"/> list with the specified index, or an empty | ||
| 213 | /// string if the archive number is outside the bounds of the list. The | ||
| 214 | /// file name should not include any directory path.</remarks> | ||
| 215 | public virtual string GetArchiveName(int archiveNumber) | ||
| 216 | { | ||
| 217 | if (archiveNumber < this.archiveFiles.Count) | ||
| 218 | { | ||
| 219 | return Path.GetFileName(this.archiveFiles[archiveNumber]); | ||
| 220 | } | ||
| 221 | |||
| 222 | return String.Empty; | ||
| 223 | } | ||
| 224 | |||
| 225 | /// <summary> | ||
| 226 | /// Opens a stream for writing an archive. | ||
| 227 | /// </summary> | ||
| 228 | /// <param name="archiveNumber">The 0-based index of the archive within | ||
| 229 | /// the chain.</param> | ||
| 230 | /// <param name="archiveName">The name of the archive that was returned | ||
| 231 | /// by <see cref="GetArchiveName"/>.</param> | ||
| 232 | /// <param name="truncate">True if the stream should be truncated when | ||
| 233 | /// opened (if it already exists); false if an existing stream is being | ||
| 234 | /// re-opened for writing additional data.</param> | ||
| 235 | /// <param name="compressionEngine">Instance of the compression engine | ||
| 236 | /// doing the operations.</param> | ||
| 237 | /// <returns>A writable Stream where the compressed archive bytes will be | ||
| 238 | /// written, or null to cancel the archive creation.</returns> | ||
| 239 | /// <remarks> | ||
| 240 | /// This method opens the file from the <see cref="ArchiveFiles"/> list | ||
| 241 | /// with the specified index. If the archive number is outside the bounds | ||
| 242 | /// of the list, this method returns null. | ||
| 243 | /// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method | ||
| 244 | /// will seek to the start of any existing archive in the file, or to the | ||
| 245 | /// end of the file if the existing file is not an archive.</para> | ||
| 246 | /// </remarks> | ||
| 247 | public virtual Stream OpenArchiveWriteStream( | ||
| 248 | int archiveNumber, | ||
| 249 | string archiveName, | ||
| 250 | bool truncate, | ||
| 251 | CompressionEngine compressionEngine) | ||
| 252 | { | ||
| 253 | if (archiveNumber >= this.archiveFiles.Count) | ||
| 254 | { | ||
| 255 | return null; | ||
| 256 | } | ||
| 257 | |||
| 258 | if (String.IsNullOrEmpty(archiveName)) | ||
| 259 | { | ||
| 260 | throw new ArgumentNullException("archiveName"); | ||
| 261 | } | ||
| 262 | |||
| 263 | // All archives must be in the same directory, | ||
| 264 | // so always use the directory from the first archive. | ||
| 265 | string archiveFile = Path.Combine( | ||
| 266 | Path.GetDirectoryName(this.archiveFiles[0]), archiveName); | ||
| 267 | Stream stream = File.Open( | ||
| 268 | archiveFile, | ||
| 269 | (truncate ? FileMode.OpenOrCreate : FileMode.Open), | ||
| 270 | FileAccess.ReadWrite); | ||
| 271 | |||
| 272 | if (this.enableOffsetOpen) | ||
| 273 | { | ||
| 274 | long offset = compressionEngine.FindArchiveOffset( | ||
| 275 | new DuplicateStream(stream)); | ||
| 276 | |||
| 277 | // If this is not an archive file, append the archive to it. | ||
| 278 | if (offset < 0) | ||
| 279 | { | ||
| 280 | offset = stream.Length; | ||
| 281 | } | ||
| 282 | |||
| 283 | if (offset > 0) | ||
| 284 | { | ||
| 285 | stream = new OffsetStream(stream, offset); | ||
| 286 | } | ||
| 287 | |||
| 288 | stream.Seek(0, SeekOrigin.Begin); | ||
| 289 | } | ||
| 290 | |||
| 291 | if (truncate) | ||
| 292 | { | ||
| 293 | // Truncate the stream, in case a larger old archive starts here. | ||
| 294 | stream.SetLength(0); | ||
| 295 | } | ||
| 296 | |||
| 297 | return stream; | ||
| 298 | } | ||
| 299 | |||
| 300 | /// <summary> | ||
| 301 | /// Closes a stream where an archive package was written. | ||
| 302 | /// </summary> | ||
| 303 | /// <param name="archiveNumber">The 0-based index of the archive within | ||
| 304 | /// the chain.</param> | ||
| 305 | /// <param name="archiveName">The name of the archive that was previously | ||
| 306 | /// returned by <see cref="GetArchiveName"/>.</param> | ||
| 307 | /// <param name="stream">A stream that was previously returned by | ||
| 308 | /// <see cref="OpenArchiveWriteStream"/> and is now ready to be closed.</param> | ||
| 309 | public virtual void CloseArchiveWriteStream( | ||
| 310 | int archiveNumber, | ||
| 311 | string archiveName, | ||
| 312 | Stream stream) | ||
| 313 | { | ||
| 314 | if (stream != null) | ||
| 315 | { | ||
| 316 | stream.Close(); | ||
| 317 | |||
| 318 | FileStream fileStream = stream as FileStream; | ||
| 319 | if (fileStream != null) | ||
| 320 | { | ||
| 321 | string streamFile = fileStream.Name; | ||
| 322 | if (!String.IsNullOrEmpty(archiveName) && | ||
| 323 | archiveName != Path.GetFileName(streamFile)) | ||
| 324 | { | ||
| 325 | string archiveFile = Path.Combine( | ||
| 326 | Path.GetDirectoryName(this.archiveFiles[0]), archiveName); | ||
| 327 | if (File.Exists(archiveFile)) | ||
| 328 | { | ||
| 329 | File.Delete(archiveFile); | ||
| 330 | } | ||
| 331 | File.Move(streamFile, archiveFile); | ||
| 332 | } | ||
| 333 | } | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | /// <summary> | ||
| 338 | /// Opens a stream to read a file that is to be included in an archive. | ||
| 339 | /// </summary> | ||
| 340 | /// <param name="path">The path of the file within the archive.</param> | ||
| 341 | /// <param name="attributes">The returned attributes of the opened file, | ||
| 342 | /// to be stored in the archive.</param> | ||
| 343 | /// <param name="lastWriteTime">The returned last-modified time of the | ||
| 344 | /// opened file, to be stored in the archive.</param> | ||
| 345 | /// <returns>A readable Stream where the file bytes will be read from | ||
| 346 | /// before they are compressed, or null to skip inclusion of the file and | ||
| 347 | /// continue to the next file.</returns> | ||
| 348 | /// <remarks> | ||
| 349 | /// This method opens a file using the following logic: | ||
| 350 | /// <list> | ||
| 351 | /// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping | ||
| 352 | /// are both null, the path is treated as relative to the current directory, | ||
| 353 | /// and that file is opened.</item> | ||
| 354 | /// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/> | ||
| 355 | /// mapping is null, the path is treated as relative to that directory, and | ||
| 356 | /// that file is opened.</item> | ||
| 357 | /// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/> | ||
| 358 | /// mapping is not null, the path parameter is used as a key into the mapping, | ||
| 359 | /// and the resulting value is the file path that is opened, relative to the | ||
| 360 | /// current directory (or it may be an absolute path). If no mapping exists, | ||
| 361 | /// the file is skipped.</item> | ||
| 362 | /// <item>If both the <see cref="Directory"/> and the <see cref="Files"/> | ||
| 363 | /// mapping are specified, the path parameter is used as a key into the | ||
| 364 | /// mapping, and the resulting value is the file path that is opened, relative | ||
| 365 | /// to the specified directory (or it may be an absolute path). If no mapping | ||
| 366 | /// exists, the file is skipped.</item> | ||
| 367 | /// </list> | ||
| 368 | /// </remarks> | ||
| 369 | public virtual Stream OpenFileReadStream( | ||
| 370 | string path, out FileAttributes attributes, out DateTime lastWriteTime) | ||
| 371 | { | ||
| 372 | string filePath = this.TranslateFilePath(path); | ||
| 373 | |||
| 374 | if (filePath == null) | ||
| 375 | { | ||
| 376 | attributes = FileAttributes.Normal; | ||
| 377 | lastWriteTime = DateTime.Now; | ||
| 378 | return null; | ||
| 379 | } | ||
| 380 | |||
| 381 | attributes = File.GetAttributes(filePath); | ||
| 382 | lastWriteTime = File.GetLastWriteTime(filePath); | ||
| 383 | return File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); | ||
| 384 | } | ||
| 385 | |||
| 386 | /// <summary> | ||
| 387 | /// Closes a stream that has been used to read a file. | ||
| 388 | /// </summary> | ||
| 389 | /// <param name="path">The path of the file within the archive; the same as | ||
| 390 | /// the path provided when the stream was opened.</param> | ||
| 391 | /// <param name="stream">A stream that was previously returned by | ||
| 392 | /// <see cref="OpenFileReadStream"/> and is now ready to be closed.</param> | ||
| 393 | public virtual void CloseFileReadStream(string path, Stream stream) | ||
| 394 | { | ||
| 395 | if (stream != null) | ||
| 396 | { | ||
| 397 | stream.Close(); | ||
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 401 | /// <summary> | ||
| 402 | /// Gets extended parameter information specific to the compression format | ||
| 403 | /// being used. | ||
| 404 | /// </summary> | ||
| 405 | /// <param name="optionName">Name of the option being requested.</param> | ||
| 406 | /// <param name="parameters">Parameters for the option; for per-file options, | ||
| 407 | /// the first parameter is typically the internal file path.</param> | ||
| 408 | /// <returns>Option value, or null to use the default behavior.</returns> | ||
| 409 | /// <remarks> | ||
| 410 | /// This implementation does not handle any options. Subclasses may override | ||
| 411 | /// this method to allow for non-default behavior. | ||
| 412 | /// </remarks> | ||
| 413 | public virtual object GetOption(string optionName, object[] parameters) | ||
| 414 | { | ||
| 415 | return null; | ||
| 416 | } | ||
| 417 | |||
| 418 | #endregion | ||
| 419 | |||
| 420 | #region IUnpackStreamContext Members | ||
| 421 | |||
| 422 | /// <summary> | ||
| 423 | /// Opens the archive stream for reading. | ||
| 424 | /// </summary> | ||
| 425 | /// <param name="archiveNumber">The zero-based index of the archive to | ||
| 426 | /// open.</param> | ||
| 427 | /// <param name="archiveName">The name of the archive being opened.</param> | ||
| 428 | /// <param name="compressionEngine">Instance of the compression engine | ||
| 429 | /// doing the operations.</param> | ||
| 430 | /// <returns>A stream from which archive bytes are read, or null to cancel | ||
| 431 | /// extraction of the archive.</returns> | ||
| 432 | /// <remarks> | ||
| 433 | /// This method opens the file from the <see cref="ArchiveFiles"/> list with | ||
| 434 | /// the specified index. If the archive number is outside the bounds of the | ||
| 435 | /// list, this method returns null. | ||
| 436 | /// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method will | ||
| 437 | /// seek to the start of any existing archive in the file, or to the end of | ||
| 438 | /// the file if the existing file is not an archive.</para> | ||
| 439 | /// </remarks> | ||
| 440 | public virtual Stream OpenArchiveReadStream( | ||
| 441 | int archiveNumber, string archiveName, CompressionEngine compressionEngine) | ||
| 442 | { | ||
| 443 | if (archiveNumber >= this.archiveFiles.Count) | ||
| 444 | { | ||
| 445 | return null; | ||
| 446 | } | ||
| 447 | |||
| 448 | string archiveFile = this.archiveFiles[archiveNumber]; | ||
| 449 | Stream stream = File.Open( | ||
| 450 | archiveFile, FileMode.Open, FileAccess.Read, FileShare.Read); | ||
| 451 | |||
| 452 | if (this.enableOffsetOpen) | ||
| 453 | { | ||
| 454 | long offset = compressionEngine.FindArchiveOffset( | ||
| 455 | new DuplicateStream(stream)); | ||
| 456 | if (offset > 0) | ||
| 457 | { | ||
| 458 | stream = new OffsetStream(stream, offset); | ||
| 459 | } | ||
| 460 | else | ||
| 461 | { | ||
| 462 | stream.Seek(0, SeekOrigin.Begin); | ||
| 463 | } | ||
| 464 | } | ||
| 465 | |||
| 466 | return stream; | ||
| 467 | } | ||
| 468 | |||
| 469 | /// <summary> | ||
| 470 | /// Closes a stream where an archive was read. | ||
| 471 | /// </summary> | ||
| 472 | /// <param name="archiveNumber">The archive number of the stream | ||
| 473 | /// to close.</param> | ||
| 474 | /// <param name="archiveName">The name of the archive being closed.</param> | ||
| 475 | /// <param name="stream">The stream that was previously returned by | ||
| 476 | /// <see cref="OpenArchiveReadStream"/> and is now ready to be closed.</param> | ||
| 477 | public virtual void CloseArchiveReadStream( | ||
| 478 | int archiveNumber, string archiveName, Stream stream) | ||
| 479 | { | ||
| 480 | if (stream != null) | ||
| 481 | { | ||
| 482 | stream.Close(); | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | /// <summary> | ||
| 487 | /// Opens a stream for writing extracted file bytes. | ||
| 488 | /// </summary> | ||
| 489 | /// <param name="path">The path of the file within the archive.</param> | ||
| 490 | /// <param name="fileSize">The uncompressed size of the file to be | ||
| 491 | /// extracted.</param> | ||
| 492 | /// <param name="lastWriteTime">The last write time of the file to be | ||
| 493 | /// extracted.</param> | ||
| 494 | /// <returns>A stream where extracted file bytes are to be written, or null | ||
| 495 | /// to skip extraction of the file and continue to the next file.</returns> | ||
| 496 | /// <remarks> | ||
| 497 | /// This method opens a file using the following logic: | ||
| 498 | /// <list> | ||
| 499 | /// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping | ||
| 500 | /// are both null, the path is treated as relative to the current directory, | ||
| 501 | /// and that file is opened.</item> | ||
| 502 | /// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/> | ||
| 503 | /// mapping is null, the path is treated as relative to that directory, and | ||
| 504 | /// that file is opened.</item> | ||
| 505 | /// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/> | ||
| 506 | /// mapping is not null, the path parameter is used as a key into the mapping, | ||
| 507 | /// and the resulting value is the file path that is opened, relative to the | ||
| 508 | /// current directory (or it may be an absolute path). If no mapping exists, | ||
| 509 | /// the file is skipped.</item> | ||
| 510 | /// <item>If both the <see cref="Directory"/> and the <see cref="Files"/> | ||
| 511 | /// mapping are specified, the path parameter is used as a key into the | ||
| 512 | /// mapping, and the resulting value is the file path that is opened, | ||
| 513 | /// relative to the specified directory (or it may be an absolute path). | ||
| 514 | /// If no mapping exists, the file is skipped.</item> | ||
| 515 | /// </list> | ||
| 516 | /// <para>If the <see cref="ExtractOnlyNewerFiles"/> flag is set, the file | ||
| 517 | /// is skipped if a file currently exists in the same path with an equal | ||
| 518 | /// or newer write time.</para> | ||
| 519 | /// </remarks> | ||
| 520 | public virtual Stream OpenFileWriteStream( | ||
| 521 | string path, | ||
| 522 | long fileSize, | ||
| 523 | DateTime lastWriteTime) | ||
| 524 | { | ||
| 525 | string filePath = this.TranslateFilePath(path); | ||
| 526 | |||
| 527 | if (filePath == null) | ||
| 528 | { | ||
| 529 | return null; | ||
| 530 | } | ||
| 531 | |||
| 532 | FileInfo file = new FileInfo(filePath); | ||
| 533 | if (file.Exists) | ||
| 534 | { | ||
| 535 | if (this.extractOnlyNewerFiles && lastWriteTime != DateTime.MinValue) | ||
| 536 | { | ||
| 537 | if (file.LastWriteTime >= lastWriteTime) | ||
| 538 | { | ||
| 539 | return null; | ||
| 540 | } | ||
| 541 | } | ||
| 542 | |||
| 543 | // Clear attributes that will prevent overwriting the file. | ||
| 544 | // (The final attributes will be set after the file is unpacked.) | ||
| 545 | FileAttributes attributesToClear = | ||
| 546 | FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System; | ||
| 547 | if ((file.Attributes & attributesToClear) != 0) | ||
| 548 | { | ||
| 549 | file.Attributes &= ~attributesToClear; | ||
| 550 | } | ||
| 551 | } | ||
| 552 | |||
| 553 | if (!file.Directory.Exists) | ||
| 554 | { | ||
| 555 | file.Directory.Create(); | ||
| 556 | } | ||
| 557 | |||
| 558 | return File.Open( | ||
| 559 | filePath, FileMode.Create, FileAccess.Write, FileShare.None); | ||
| 560 | } | ||
| 561 | |||
| 562 | /// <summary> | ||
| 563 | /// Closes a stream where an extracted file was written. | ||
| 564 | /// </summary> | ||
| 565 | /// <param name="path">The path of the file within the archive.</param> | ||
| 566 | /// <param name="stream">The stream that was previously returned by | ||
| 567 | /// <see cref="OpenFileWriteStream"/> and is now ready to be closed.</param> | ||
| 568 | /// <param name="attributes">The attributes of the extracted file.</param> | ||
| 569 | /// <param name="lastWriteTime">The last write time of the file.</param> | ||
| 570 | /// <remarks> | ||
| 571 | /// After closing the extracted file stream, this method applies the date | ||
| 572 | /// and attributes to that file. | ||
| 573 | /// </remarks> | ||
| 574 | public virtual void CloseFileWriteStream( | ||
| 575 | string path, | ||
| 576 | Stream stream, | ||
| 577 | FileAttributes attributes, | ||
| 578 | DateTime lastWriteTime) | ||
| 579 | { | ||
| 580 | if (stream != null) | ||
| 581 | { | ||
| 582 | stream.Close(); | ||
| 583 | } | ||
| 584 | |||
| 585 | string filePath = this.TranslateFilePath(path); | ||
| 586 | if (filePath != null) | ||
| 587 | { | ||
| 588 | FileInfo file = new FileInfo(filePath); | ||
| 589 | |||
| 590 | if (lastWriteTime != DateTime.MinValue) | ||
| 591 | { | ||
| 592 | try | ||
| 593 | { | ||
| 594 | file.LastWriteTime = lastWriteTime; | ||
| 595 | } | ||
| 596 | catch (ArgumentException) | ||
| 597 | { | ||
| 598 | } | ||
| 599 | catch (IOException) | ||
| 600 | { | ||
| 601 | } | ||
| 602 | } | ||
| 603 | |||
| 604 | try | ||
| 605 | { | ||
| 606 | file.Attributes = attributes; | ||
| 607 | } | ||
| 608 | catch (IOException) | ||
| 609 | { | ||
| 610 | } | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | #endregion | ||
| 615 | |||
| 616 | #region Private utility methods | ||
| 617 | |||
| 618 | /// <summary> | ||
| 619 | /// Translates an internal file path to an external file path using the | ||
| 620 | /// <see cref="Directory"/> and the <see cref="Files"/> mapping, according to | ||
| 621 | /// rules documented in <see cref="OpenFileReadStream"/> and | ||
| 622 | /// <see cref="OpenFileWriteStream"/>. | ||
| 623 | /// </summary> | ||
| 624 | /// <param name="path">The path of the file with the archive.</param> | ||
| 625 | /// <returns>The external path of the file, or null if there is no | ||
| 626 | /// valid translation.</returns> | ||
| 627 | private string TranslateFilePath(string path) | ||
| 628 | { | ||
| 629 | string filePath; | ||
| 630 | if (this.files != null) | ||
| 631 | { | ||
| 632 | filePath = this.files[path]; | ||
| 633 | } | ||
| 634 | else | ||
| 635 | { | ||
| 636 | this.ValidateArchivePath(path); | ||
| 637 | |||
| 638 | filePath = path; | ||
| 639 | } | ||
| 640 | |||
| 641 | if (filePath != null) | ||
| 642 | { | ||
| 643 | if (this.directory != null) | ||
| 644 | { | ||
| 645 | filePath = Path.Combine(this.directory, filePath); | ||
| 646 | } | ||
| 647 | } | ||
| 648 | |||
| 649 | return filePath; | ||
| 650 | } | ||
| 651 | |||
| 652 | private void ValidateArchivePath(string filePath) | ||
| 653 | { | ||
| 654 | string basePath = Path.GetFullPath(String.IsNullOrEmpty(this.directory) ? Environment.CurrentDirectory : this.directory); | ||
| 655 | string path = Path.GetFullPath(Path.Combine(basePath, filePath)); | ||
| 656 | if (!path.StartsWith(basePath, StringComparison.InvariantCultureIgnoreCase)) | ||
| 657 | { | ||
| 658 | throw new InvalidDataException("Archive cannot contain files with absolute or traversal paths."); | ||
| 659 | } | ||
| 660 | } | ||
| 661 | |||
| 662 | #endregion | ||
| 663 | } | ||
| 664 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Collections.Generic; | ||
| 8 | using System.Globalization; | ||
| 9 | using System.Text; | ||
| 10 | using System.Text.RegularExpressions; | ||
| 11 | using System.Runtime.Serialization; | ||
| 12 | using System.Diagnostics.CodeAnalysis; | ||
| 13 | |||
| 14 | /// <summary> | ||
| 15 | /// Abstract object representing a compressed archive on disk; | ||
| 16 | /// provides access to file-based operations on the archive. | ||
| 17 | /// </summary> | ||
| 18 | [Serializable] | ||
| 19 | public abstract class ArchiveInfo : FileSystemInfo | ||
| 20 | { | ||
| 21 | /// <summary> | ||
| 22 | /// Creates a new ArchiveInfo object representing an archive in a | ||
| 23 | /// specified path. | ||
| 24 | /// </summary> | ||
| 25 | /// <param name="path">The path to the archive. When creating an archive, | ||
| 26 | /// this file does not necessarily exist yet.</param> | ||
| 27 | protected ArchiveInfo(string path) : base() | ||
| 28 | { | ||
| 29 | if (path == null) | ||
| 30 | { | ||
| 31 | throw new ArgumentNullException("path"); | ||
| 32 | } | ||
| 33 | |||
| 34 | // protected instance members inherited from FileSystemInfo: | ||
| 35 | this.OriginalPath = path; | ||
| 36 | this.FullPath = Path.GetFullPath(path); | ||
| 37 | } | ||
| 38 | |||
| 39 | /// <summary> | ||
| 40 | /// Initializes a new instance of the ArchiveInfo class with serialized data. | ||
| 41 | /// </summary> | ||
| 42 | /// <param name="info">The SerializationInfo that holds the serialized object | ||
| 43 | /// data about the exception being thrown.</param> | ||
| 44 | /// <param name="context">The StreamingContext that contains contextual | ||
| 45 | /// information about the source or destination.</param> | ||
| 46 | protected ArchiveInfo(SerializationInfo info, StreamingContext context) | ||
| 47 | : base(info, context) | ||
| 48 | { | ||
| 49 | } | ||
| 50 | |||
| 51 | /// <summary> | ||
| 52 | /// Gets the directory that contains the archive. | ||
| 53 | /// </summary> | ||
| 54 | /// <value>A DirectoryInfo object representing the parent directory of the | ||
| 55 | /// archive.</value> | ||
| 56 | public DirectoryInfo Directory | ||
| 57 | { | ||
| 58 | get | ||
| 59 | { | ||
| 60 | return new DirectoryInfo(Path.GetDirectoryName(this.FullName)); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | /// <summary> | ||
| 65 | /// Gets the full path of the directory that contains the archive. | ||
| 66 | /// </summary> | ||
| 67 | /// <value>The full path of the directory that contains the archive.</value> | ||
| 68 | public string DirectoryName | ||
| 69 | { | ||
| 70 | get | ||
| 71 | { | ||
| 72 | return Path.GetDirectoryName(this.FullName); | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | /// <summary> | ||
| 77 | /// Gets the size of the archive. | ||
| 78 | /// </summary> | ||
| 79 | /// <value>The size of the archive in bytes.</value> | ||
| 80 | public long Length | ||
| 81 | { | ||
| 82 | get | ||
| 83 | { | ||
| 84 | return new FileInfo(this.FullName).Length; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | /// <summary> | ||
| 89 | /// Gets the file name of the archive. | ||
| 90 | /// </summary> | ||
| 91 | /// <value>The file name of the archive, not including any path.</value> | ||
| 92 | public override string Name | ||
| 93 | { | ||
| 94 | get | ||
| 95 | { | ||
| 96 | return Path.GetFileName(this.FullName); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | /// <summary> | ||
| 101 | /// Checks if the archive exists. | ||
| 102 | /// </summary> | ||
| 103 | /// <value>True if the archive exists; else false.</value> | ||
| 104 | public override bool Exists | ||
| 105 | { | ||
| 106 | get | ||
| 107 | { | ||
| 108 | return File.Exists(this.FullName); | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | /// <summary> | ||
| 113 | /// Gets the full path of the archive. | ||
| 114 | /// </summary> | ||
| 115 | /// <returns>The full path of the archive.</returns> | ||
| 116 | public override string ToString() | ||
| 117 | { | ||
| 118 | return this.FullName; | ||
| 119 | } | ||
| 120 | |||
| 121 | /// <summary> | ||
| 122 | /// Deletes the archive. | ||
| 123 | /// </summary> | ||
| 124 | public override void Delete() | ||
| 125 | { | ||
| 126 | File.Delete(this.FullName); | ||
| 127 | } | ||
| 128 | |||
| 129 | /// <summary> | ||
| 130 | /// Copies an existing archive to another location. | ||
| 131 | /// </summary> | ||
| 132 | /// <param name="destFileName">The destination file path.</param> | ||
| 133 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 134 | public void CopyTo(string destFileName) | ||
| 135 | { | ||
| 136 | File.Copy(this.FullName, destFileName); | ||
| 137 | } | ||
| 138 | |||
| 139 | /// <summary> | ||
| 140 | /// Copies an existing archive to another location, optionally | ||
| 141 | /// overwriting the destination file. | ||
| 142 | /// </summary> | ||
| 143 | /// <param name="destFileName">The destination file path.</param> | ||
| 144 | /// <param name="overwrite">If true, the destination file will be | ||
| 145 | /// overwritten if it exists.</param> | ||
| 146 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 147 | public void CopyTo(string destFileName, bool overwrite) | ||
| 148 | { | ||
| 149 | File.Copy(this.FullName, destFileName, overwrite); | ||
| 150 | } | ||
| 151 | |||
| 152 | /// <summary> | ||
| 153 | /// Moves an existing archive to another location. | ||
| 154 | /// </summary> | ||
| 155 | /// <param name="destFileName">The destination file path.</param> | ||
| 156 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 157 | public void MoveTo(string destFileName) | ||
| 158 | { | ||
| 159 | File.Move(this.FullName, destFileName); | ||
| 160 | this.FullPath = Path.GetFullPath(destFileName); | ||
| 161 | } | ||
| 162 | |||
| 163 | /// <summary> | ||
| 164 | /// Checks if the archive contains a valid archive header. | ||
| 165 | /// </summary> | ||
| 166 | /// <returns>True if the file is a valid archive; false otherwise.</returns> | ||
| 167 | public bool IsValid() | ||
| 168 | { | ||
| 169 | using (Stream stream = File.OpenRead(this.FullName)) | ||
| 170 | { | ||
| 171 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 172 | { | ||
| 173 | return compressionEngine.FindArchiveOffset(stream) >= 0; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | } | ||
| 177 | |||
| 178 | /// <summary> | ||
| 179 | /// Gets information about the files contained in the archive. | ||
| 180 | /// </summary> | ||
| 181 | /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each | ||
| 182 | /// containing information about a file in the archive.</returns> | ||
| 183 | public IList<ArchiveFileInfo> GetFiles() | ||
| 184 | { | ||
| 185 | return this.InternalGetFiles((Predicate<string>) null); | ||
| 186 | } | ||
| 187 | |||
| 188 | /// <summary> | ||
| 189 | /// Gets information about the certain files contained in the archive file. | ||
| 190 | /// </summary> | ||
| 191 | /// <param name="searchPattern">The search string, such as | ||
| 192 | /// "*.txt".</param> | ||
| 193 | /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each containing | ||
| 194 | /// information about a file in the archive.</returns> | ||
| 195 | public IList<ArchiveFileInfo> GetFiles(string searchPattern) | ||
| 196 | { | ||
| 197 | if (searchPattern == null) | ||
| 198 | { | ||
| 199 | throw new ArgumentNullException("searchPattern"); | ||
| 200 | } | ||
| 201 | |||
| 202 | string regexPattern = String.Format( | ||
| 203 | CultureInfo.InvariantCulture, | ||
| 204 | "^{0}$", | ||
| 205 | Regex.Escape(searchPattern).Replace("\\*", ".*").Replace("\\?", ".")); | ||
| 206 | Regex regex = new Regex( | ||
| 207 | regexPattern, | ||
| 208 | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); | ||
| 209 | |||
| 210 | return this.InternalGetFiles( | ||
| 211 | delegate(string match) | ||
| 212 | { | ||
| 213 | return regex.IsMatch(match); | ||
| 214 | }); | ||
| 215 | } | ||
| 216 | |||
| 217 | /// <summary> | ||
| 218 | /// Extracts all files from an archive to a destination directory. | ||
| 219 | /// </summary> | ||
| 220 | /// <param name="destDirectory">Directory where the files are to be | ||
| 221 | /// extracted.</param> | ||
| 222 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 223 | public void Unpack(string destDirectory) | ||
| 224 | { | ||
| 225 | this.Unpack(destDirectory, null); | ||
| 226 | } | ||
| 227 | |||
| 228 | /// <summary> | ||
| 229 | /// Extracts all files from an archive to a destination directory, | ||
| 230 | /// optionally extracting only newer files. | ||
| 231 | /// </summary> | ||
| 232 | /// <param name="destDirectory">Directory where the files are to be | ||
| 233 | /// extracted.</param> | ||
| 234 | /// <param name="progressHandler">Handler for receiving progress | ||
| 235 | /// information; this may be null if progress is not desired.</param> | ||
| 236 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 237 | public void Unpack( | ||
| 238 | string destDirectory, | ||
| 239 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 240 | { | ||
| 241 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 242 | { | ||
| 243 | compressionEngine.Progress += progressHandler; | ||
| 244 | ArchiveFileStreamContext streamContext = | ||
| 245 | new ArchiveFileStreamContext(this.FullName, destDirectory, null); | ||
| 246 | streamContext.EnableOffsetOpen = true; | ||
| 247 | compressionEngine.Unpack(streamContext, null); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | /// <summary> | ||
| 252 | /// Extracts a single file from the archive. | ||
| 253 | /// </summary> | ||
| 254 | /// <param name="fileName">The name of the file in the archive. Also | ||
| 255 | /// includes the internal path of the file, if any. File name matching | ||
| 256 | /// is case-insensitive.</param> | ||
| 257 | /// <param name="destFileName">The path where the file is to be | ||
| 258 | /// extracted on disk.</param> | ||
| 259 | /// <remarks>If <paramref name="destFileName"/> already exists, | ||
| 260 | /// it will be overwritten.</remarks> | ||
| 261 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 262 | public void UnpackFile(string fileName, string destFileName) | ||
| 263 | { | ||
| 264 | if (fileName == null) | ||
| 265 | { | ||
| 266 | throw new ArgumentNullException("fileName"); | ||
| 267 | } | ||
| 268 | |||
| 269 | if (destFileName == null) | ||
| 270 | { | ||
| 271 | throw new ArgumentNullException("destFileName"); | ||
| 272 | } | ||
| 273 | |||
| 274 | this.UnpackFiles( | ||
| 275 | new string[] { fileName }, | ||
| 276 | null, | ||
| 277 | new string[] { destFileName }); | ||
| 278 | } | ||
| 279 | |||
| 280 | /// <summary> | ||
| 281 | /// Extracts multiple files from the archive. | ||
| 282 | /// </summary> | ||
| 283 | /// <param name="fileNames">The names of the files in the archive. | ||
| 284 | /// Each name includes the internal path of the file, if any. File name | ||
| 285 | /// matching is case-insensitive.</param> | ||
| 286 | /// <param name="destDirectory">This parameter may be null, but if | ||
| 287 | /// specified it is the root directory for any relative paths in | ||
| 288 | /// <paramref name="destFileNames"/>.</param> | ||
| 289 | /// <param name="destFileNames">The paths where the files are to be | ||
| 290 | /// extracted on disk. If this parameter is null, the files will be | ||
| 291 | /// extracted with the names from the archive.</param> | ||
| 292 | /// <remarks> | ||
| 293 | /// If any extracted files already exist on disk, they will be overwritten. | ||
| 294 | /// <p>The <paramref name="destDirectory"/> and | ||
| 295 | /// <paramref name="destFileNames"/> parameters cannot both be null.</p> | ||
| 296 | /// </remarks> | ||
| 297 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 298 | public void UnpackFiles( | ||
| 299 | IList<string> fileNames, | ||
| 300 | string destDirectory, | ||
| 301 | IList<string> destFileNames) | ||
| 302 | { | ||
| 303 | this.UnpackFiles(fileNames, destDirectory, destFileNames, null); | ||
| 304 | } | ||
| 305 | |||
| 306 | /// <summary> | ||
| 307 | /// Extracts multiple files from the archive, optionally extracting | ||
| 308 | /// only newer files. | ||
| 309 | /// </summary> | ||
| 310 | /// <param name="fileNames">The names of the files in the archive. | ||
| 311 | /// Each name includes the internal path of the file, if any. File name | ||
| 312 | /// matching is case-insensitive.</param> | ||
| 313 | /// <param name="destDirectory">This parameter may be null, but if | ||
| 314 | /// specified it is the root directory for any relative paths in | ||
| 315 | /// <paramref name="destFileNames"/>.</param> | ||
| 316 | /// <param name="destFileNames">The paths where the files are to be | ||
| 317 | /// extracted on disk. If this parameter is null, the files will be | ||
| 318 | /// extracted with the names from the archive.</param> | ||
| 319 | /// <param name="progressHandler">Handler for receiving progress information; | ||
| 320 | /// this may be null if progress is not desired.</param> | ||
| 321 | /// <remarks> | ||
| 322 | /// If any extracted files already exist on disk, they will be overwritten. | ||
| 323 | /// <p>The <paramref name="destDirectory"/> and | ||
| 324 | /// <paramref name="destFileNames"/> parameters cannot both be null.</p> | ||
| 325 | /// </remarks> | ||
| 326 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 327 | public void UnpackFiles( | ||
| 328 | IList<string> fileNames, | ||
| 329 | string destDirectory, | ||
| 330 | IList<string> destFileNames, | ||
| 331 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 332 | { | ||
| 333 | if (fileNames == null) | ||
| 334 | { | ||
| 335 | throw new ArgumentNullException("fileNames"); | ||
| 336 | } | ||
| 337 | |||
| 338 | if (destFileNames == null) | ||
| 339 | { | ||
| 340 | if (destDirectory == null) | ||
| 341 | { | ||
| 342 | throw new ArgumentNullException("destFileNames"); | ||
| 343 | } | ||
| 344 | |||
| 345 | destFileNames = fileNames; | ||
| 346 | } | ||
| 347 | |||
| 348 | if (destFileNames.Count != fileNames.Count) | ||
| 349 | { | ||
| 350 | throw new ArgumentOutOfRangeException("destFileNames"); | ||
| 351 | } | ||
| 352 | |||
| 353 | IDictionary<string, string> files = | ||
| 354 | ArchiveInfo.CreateStringDictionary(fileNames, destFileNames); | ||
| 355 | this.UnpackFileSet(files, destDirectory, progressHandler); | ||
| 356 | } | ||
| 357 | |||
| 358 | /// <summary> | ||
| 359 | /// Extracts multiple files from the archive. | ||
| 360 | /// </summary> | ||
| 361 | /// <param name="fileNames">A mapping from internal file paths to | ||
| 362 | /// external file paths. Case-senstivity when matching internal paths | ||
| 363 | /// depends on the IDictionary implementation.</param> | ||
| 364 | /// <param name="destDirectory">This parameter may be null, but if | ||
| 365 | /// specified it is the root directory for any relative external paths | ||
| 366 | /// in <paramref name="fileNames"/>.</param> | ||
| 367 | /// <remarks> | ||
| 368 | /// If any extracted files already exist on disk, they will be overwritten. | ||
| 369 | /// </remarks> | ||
| 370 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 371 | public void UnpackFileSet( | ||
| 372 | IDictionary<string, string> fileNames, | ||
| 373 | string destDirectory) | ||
| 374 | { | ||
| 375 | this.UnpackFileSet(fileNames, destDirectory, null); | ||
| 376 | } | ||
| 377 | |||
| 378 | /// <summary> | ||
| 379 | /// Extracts multiple files from the archive. | ||
| 380 | /// </summary> | ||
| 381 | /// <param name="fileNames">A mapping from internal file paths to | ||
| 382 | /// external file paths. Case-senstivity when matching internal | ||
| 383 | /// paths depends on the IDictionary implementation.</param> | ||
| 384 | /// <param name="destDirectory">This parameter may be null, but if | ||
| 385 | /// specified it is the root directory for any relative external | ||
| 386 | /// paths in <paramref name="fileNames"/>.</param> | ||
| 387 | /// <param name="progressHandler">Handler for receiving progress | ||
| 388 | /// information; this may be null if progress is not desired.</param> | ||
| 389 | /// <remarks> | ||
| 390 | /// If any extracted files already exist on disk, they will be overwritten. | ||
| 391 | /// </remarks> | ||
| 392 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] | ||
| 393 | public void UnpackFileSet( | ||
| 394 | IDictionary<string, string> fileNames, | ||
| 395 | string destDirectory, | ||
| 396 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 397 | { | ||
| 398 | if (fileNames == null) | ||
| 399 | { | ||
| 400 | throw new ArgumentNullException("fileNames"); | ||
| 401 | } | ||
| 402 | |||
| 403 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 404 | { | ||
| 405 | compressionEngine.Progress += progressHandler; | ||
| 406 | ArchiveFileStreamContext streamContext = | ||
| 407 | new ArchiveFileStreamContext(this.FullName, destDirectory, fileNames); | ||
| 408 | streamContext.EnableOffsetOpen = true; | ||
| 409 | compressionEngine.Unpack( | ||
| 410 | streamContext, | ||
| 411 | delegate(string match) | ||
| 412 | { | ||
| 413 | return fileNames.ContainsKey(match); | ||
| 414 | }); | ||
| 415 | } | ||
| 416 | } | ||
| 417 | |||
| 418 | /// <summary> | ||
| 419 | /// Opens a file inside the archive for reading without actually | ||
| 420 | /// extracting the file to disk. | ||
| 421 | /// </summary> | ||
| 422 | /// <param name="fileName">The name of the file in the archive. Also | ||
| 423 | /// includes the internal path of the file, if any. File name matching | ||
| 424 | /// is case-insensitive.</param> | ||
| 425 | /// <returns> | ||
| 426 | /// A stream for reading directly from the packed file. Like any stream | ||
| 427 | /// this should be closed/disposed as soon as it is no longer needed. | ||
| 428 | /// </returns> | ||
| 429 | public Stream OpenRead(string fileName) | ||
| 430 | { | ||
| 431 | Stream archiveStream = File.OpenRead(this.FullName); | ||
| 432 | CompressionEngine compressionEngine = this.CreateCompressionEngine(); | ||
| 433 | Stream fileStream = compressionEngine.Unpack(archiveStream, fileName); | ||
| 434 | |||
| 435 | // Attach the archiveStream and compressionEngine to the | ||
| 436 | // fileStream so they get disposed when the fileStream is disposed. | ||
| 437 | return new CargoStream(fileStream, archiveStream, compressionEngine); | ||
| 438 | } | ||
| 439 | |||
| 440 | /// <summary> | ||
| 441 | /// Opens a file inside the archive for reading text with UTF-8 encoding | ||
| 442 | /// without actually extracting the file to disk. | ||
| 443 | /// </summary> | ||
| 444 | /// <param name="fileName">The name of the file in the archive. Also | ||
| 445 | /// includes the internal path of the file, if any. File name matching | ||
| 446 | /// is case-insensitive.</param> | ||
| 447 | /// <returns> | ||
| 448 | /// A reader for reading text directly from the packed file. Like any reader | ||
| 449 | /// this should be closed/disposed as soon as it is no longer needed. | ||
| 450 | /// </returns> | ||
| 451 | /// <remarks> | ||
| 452 | /// To open an archived text file with different encoding, use the | ||
| 453 | /// <see cref="OpenRead" /> method and pass the returned stream to one of | ||
| 454 | /// the <see cref="StreamReader" /> constructor overloads. | ||
| 455 | /// </remarks> | ||
| 456 | public StreamReader OpenText(string fileName) | ||
| 457 | { | ||
| 458 | return new StreamReader(this.OpenRead(fileName)); | ||
| 459 | } | ||
| 460 | |||
| 461 | /// <summary> | ||
| 462 | /// Compresses all files in a directory into the archive. | ||
| 463 | /// Does not include subdirectories. | ||
| 464 | /// </summary> | ||
| 465 | /// <param name="sourceDirectory">The directory containing the | ||
| 466 | /// files to be included.</param> | ||
| 467 | /// <remarks> | ||
| 468 | /// Uses maximum compression level. | ||
| 469 | /// </remarks> | ||
| 470 | public void Pack(string sourceDirectory) | ||
| 471 | { | ||
| 472 | this.Pack(sourceDirectory, false, CompressionLevel.Max, null); | ||
| 473 | } | ||
| 474 | |||
| 475 | /// <summary> | ||
| 476 | /// Compresses all files in a directory into the archive, optionally | ||
| 477 | /// including subdirectories. | ||
| 478 | /// </summary> | ||
| 479 | /// <param name="sourceDirectory">This is the root directory | ||
| 480 | /// for to pack all files.</param> | ||
| 481 | /// <param name="includeSubdirectories">If true, recursively include | ||
| 482 | /// files in subdirectories.</param> | ||
| 483 | /// <param name="compLevel">The compression level used when creating | ||
| 484 | /// the archive.</param> | ||
| 485 | /// <param name="progressHandler">Handler for receiving progress information; | ||
| 486 | /// this may be null if progress is not desired.</param> | ||
| 487 | /// <remarks> | ||
| 488 | /// The files are stored in the archive using their relative file paths in | ||
| 489 | /// the directory tree, if supported by the archive file format. | ||
| 490 | /// </remarks> | ||
| 491 | public void Pack( | ||
| 492 | string sourceDirectory, | ||
| 493 | bool includeSubdirectories, | ||
| 494 | CompressionLevel compLevel, | ||
| 495 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 496 | { | ||
| 497 | IList<string> files = this.GetRelativeFilePathsInDirectoryTree( | ||
| 498 | sourceDirectory, includeSubdirectories); | ||
| 499 | this.PackFiles(sourceDirectory, files, files, compLevel, progressHandler); | ||
| 500 | } | ||
| 501 | |||
| 502 | /// <summary> | ||
| 503 | /// Compresses files into the archive, specifying the names used to | ||
| 504 | /// store the files in the archive. | ||
| 505 | /// </summary> | ||
| 506 | /// <param name="sourceDirectory">This parameter may be null, but | ||
| 507 | /// if specified it is the root directory | ||
| 508 | /// for any relative paths in <paramref name="sourceFileNames"/>.</param> | ||
| 509 | /// <param name="sourceFileNames">The list of files to be included in | ||
| 510 | /// the archive.</param> | ||
| 511 | /// <param name="fileNames">The names of the files as they are stored | ||
| 512 | /// in the archive. Each name | ||
| 513 | /// includes the internal path of the file, if any. This parameter may | ||
| 514 | /// be null, in which case the files are stored in the archive with their | ||
| 515 | /// source file names and no path information.</param> | ||
| 516 | /// <remarks> | ||
| 517 | /// Uses maximum compression level. | ||
| 518 | /// <p>Duplicate items in the <paramref name="fileNames"/> array will cause | ||
| 519 | /// an <see cref="ArchiveException"/>.</p> | ||
| 520 | /// </remarks> | ||
| 521 | public void PackFiles( | ||
| 522 | string sourceDirectory, | ||
| 523 | IList<string> sourceFileNames, | ||
| 524 | IList<string> fileNames) | ||
| 525 | { | ||
| 526 | this.PackFiles( | ||
| 527 | sourceDirectory, | ||
| 528 | sourceFileNames, | ||
| 529 | fileNames, | ||
| 530 | CompressionLevel.Max, | ||
| 531 | null); | ||
| 532 | } | ||
| 533 | |||
| 534 | /// <summary> | ||
| 535 | /// Compresses files into the archive, specifying the names used to | ||
| 536 | /// store the files in the archive. | ||
| 537 | /// </summary> | ||
| 538 | /// <param name="sourceDirectory">This parameter may be null, but if | ||
| 539 | /// specified it is the root directory | ||
| 540 | /// for any relative paths in <paramref name="sourceFileNames"/>.</param> | ||
| 541 | /// <param name="sourceFileNames">The list of files to be included in | ||
| 542 | /// the archive.</param> | ||
| 543 | /// <param name="fileNames">The names of the files as they are stored in | ||
| 544 | /// the archive. Each name includes the internal path of the file, if any. | ||
| 545 | /// This parameter may be null, in which case the files are stored in the | ||
| 546 | /// archive with their source file names and no path information.</param> | ||
| 547 | /// <param name="compLevel">The compression level used when creating the | ||
| 548 | /// archive.</param> | ||
| 549 | /// <param name="progressHandler">Handler for receiving progress information; | ||
| 550 | /// this may be null if progress is not desired.</param> | ||
| 551 | /// <remarks> | ||
| 552 | /// Duplicate items in the <paramref name="fileNames"/> array will cause | ||
| 553 | /// an <see cref="ArchiveException"/>. | ||
| 554 | /// </remarks> | ||
| 555 | public void PackFiles( | ||
| 556 | string sourceDirectory, | ||
| 557 | IList<string> sourceFileNames, | ||
| 558 | IList<string> fileNames, | ||
| 559 | CompressionLevel compLevel, | ||
| 560 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 561 | { | ||
| 562 | if (sourceFileNames == null) | ||
| 563 | { | ||
| 564 | throw new ArgumentNullException("sourceFileNames"); | ||
| 565 | } | ||
| 566 | |||
| 567 | if (fileNames == null) | ||
| 568 | { | ||
| 569 | string[] fileNamesArray = new string[sourceFileNames.Count]; | ||
| 570 | for (int i = 0; i < sourceFileNames.Count; i++) | ||
| 571 | { | ||
| 572 | fileNamesArray[i] = Path.GetFileName(sourceFileNames[i]); | ||
| 573 | } | ||
| 574 | |||
| 575 | fileNames = fileNamesArray; | ||
| 576 | } | ||
| 577 | else if (fileNames.Count != sourceFileNames.Count) | ||
| 578 | { | ||
| 579 | throw new ArgumentOutOfRangeException("fileNames"); | ||
| 580 | } | ||
| 581 | |||
| 582 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 583 | { | ||
| 584 | compressionEngine.Progress += progressHandler; | ||
| 585 | IDictionary<string, string> contextFiles = | ||
| 586 | ArchiveInfo.CreateStringDictionary(fileNames, sourceFileNames); | ||
| 587 | ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( | ||
| 588 | this.FullName, sourceDirectory, contextFiles); | ||
| 589 | streamContext.EnableOffsetOpen = true; | ||
| 590 | compressionEngine.CompressionLevel = compLevel; | ||
| 591 | compressionEngine.Pack(streamContext, fileNames); | ||
| 592 | } | ||
| 593 | } | ||
| 594 | |||
| 595 | /// <summary> | ||
| 596 | /// Compresses files into the archive, specifying the names used | ||
| 597 | /// to store the files in the archive. | ||
| 598 | /// </summary> | ||
| 599 | /// <param name="sourceDirectory">This parameter may be null, but if | ||
| 600 | /// specified it is the root directory | ||
| 601 | /// for any relative paths in <paramref name="fileNames"/>.</param> | ||
| 602 | /// <param name="fileNames">A mapping from internal file paths to | ||
| 603 | /// external file paths.</param> | ||
| 604 | /// <remarks> | ||
| 605 | /// Uses maximum compression level. | ||
| 606 | /// </remarks> | ||
| 607 | public void PackFileSet( | ||
| 608 | string sourceDirectory, | ||
| 609 | IDictionary<string, string> fileNames) | ||
| 610 | { | ||
| 611 | this.PackFileSet(sourceDirectory, fileNames, CompressionLevel.Max, null); | ||
| 612 | } | ||
| 613 | |||
| 614 | /// <summary> | ||
| 615 | /// Compresses files into the archive, specifying the names used to | ||
| 616 | /// store the files in the archive. | ||
| 617 | /// </summary> | ||
| 618 | /// <param name="sourceDirectory">This parameter may be null, but if | ||
| 619 | /// specified it is the root directory | ||
| 620 | /// for any relative paths in <paramref name="fileNames"/>.</param> | ||
| 621 | /// <param name="fileNames">A mapping from internal file paths to | ||
| 622 | /// external file paths.</param> | ||
| 623 | /// <param name="compLevel">The compression level used when creating | ||
| 624 | /// the archive.</param> | ||
| 625 | /// <param name="progressHandler">Handler for receiving progress information; | ||
| 626 | /// this may be null if progress is not desired.</param> | ||
| 627 | public void PackFileSet( | ||
| 628 | string sourceDirectory, | ||
| 629 | IDictionary<string, string> fileNames, | ||
| 630 | CompressionLevel compLevel, | ||
| 631 | EventHandler<ArchiveProgressEventArgs> progressHandler) | ||
| 632 | { | ||
| 633 | if (fileNames == null) | ||
| 634 | { | ||
| 635 | throw new ArgumentNullException("fileNames"); | ||
| 636 | } | ||
| 637 | |||
| 638 | string[] fileNamesArray = new string[fileNames.Count]; | ||
| 639 | fileNames.Keys.CopyTo(fileNamesArray, 0); | ||
| 640 | |||
| 641 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 642 | { | ||
| 643 | compressionEngine.Progress += progressHandler; | ||
| 644 | ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( | ||
| 645 | this.FullName, sourceDirectory, fileNames); | ||
| 646 | streamContext.EnableOffsetOpen = true; | ||
| 647 | compressionEngine.CompressionLevel = compLevel; | ||
| 648 | compressionEngine.Pack(streamContext, fileNamesArray); | ||
| 649 | } | ||
| 650 | } | ||
| 651 | |||
| 652 | /// <summary> | ||
| 653 | /// Given a directory, gets the relative paths of all files in the | ||
| 654 | /// directory, optionally including all subdirectories. | ||
| 655 | /// </summary> | ||
| 656 | /// <param name="dir">The directory to search.</param> | ||
| 657 | /// <param name="includeSubdirectories">True to include subdirectories | ||
| 658 | /// in the search.</param> | ||
| 659 | /// <returns>A list of file paths relative to the directory.</returns> | ||
| 660 | internal IList<string> GetRelativeFilePathsInDirectoryTree( | ||
| 661 | string dir, bool includeSubdirectories) | ||
| 662 | { | ||
| 663 | IList<string> fileList = new List<string>(); | ||
| 664 | this.RecursiveGetRelativeFilePathsInDirectoryTree( | ||
| 665 | dir, String.Empty, includeSubdirectories, fileList); | ||
| 666 | return fileList; | ||
| 667 | } | ||
| 668 | |||
| 669 | /// <summary> | ||
| 670 | /// Retrieves information about one file from this archive. | ||
| 671 | /// </summary> | ||
| 672 | /// <param name="path">Path of the file in the archive.</param> | ||
| 673 | /// <returns>File information, or null if the file was not found | ||
| 674 | /// in the archive.</returns> | ||
| 675 | internal ArchiveFileInfo GetFile(string path) | ||
| 676 | { | ||
| 677 | IList<ArchiveFileInfo> files = this.InternalGetFiles( | ||
| 678 | delegate(string match) | ||
| 679 | { | ||
| 680 | return String.Compare( | ||
| 681 | match, path, true, CultureInfo.InvariantCulture) == 0; | ||
| 682 | }); | ||
| 683 | return (files != null && files.Count > 0 ? files[0] : null); | ||
| 684 | } | ||
| 685 | |||
| 686 | /// <summary> | ||
| 687 | /// Creates a compression engine that does the low-level work for | ||
| 688 | /// this object. | ||
| 689 | /// </summary> | ||
| 690 | /// <returns>A new compression engine instance that matches the specific | ||
| 691 | /// subclass of archive.</returns> | ||
| 692 | /// <remarks> | ||
| 693 | /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d | ||
| 694 | /// immediately after use. | ||
| 695 | /// </remarks> | ||
| 696 | protected abstract CompressionEngine CreateCompressionEngine(); | ||
| 697 | |||
| 698 | /// <summary> | ||
| 699 | /// Creates a case-insensitive dictionary mapping from one list of | ||
| 700 | /// strings to the other. | ||
| 701 | /// </summary> | ||
| 702 | /// <param name="keys">List of keys.</param> | ||
| 703 | /// <param name="values">List of values that are mapped 1-to-1 to | ||
| 704 | /// the keys.</param> | ||
| 705 | /// <returns>A filled dictionary of the strings.</returns> | ||
| 706 | private static IDictionary<string, string> CreateStringDictionary( | ||
| 707 | IList<string> keys, IList<string> values) | ||
| 708 | { | ||
| 709 | IDictionary<string, string> stringDict = | ||
| 710 | new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
| 711 | for (int i = 0; i < keys.Count; i++) | ||
| 712 | { | ||
| 713 | stringDict.Add(keys[i], values[i]); | ||
| 714 | } | ||
| 715 | |||
| 716 | return stringDict; | ||
| 717 | } | ||
| 718 | |||
| 719 | /// <summary> | ||
| 720 | /// Recursive-descent helper function for | ||
| 721 | /// GetRelativeFilePathsInDirectoryTree. | ||
| 722 | /// </summary> | ||
| 723 | /// <param name="dir">The root directory of the search.</param> | ||
| 724 | /// <param name="relativeDir">The relative directory to be | ||
| 725 | /// processed now.</param> | ||
| 726 | /// <param name="includeSubdirectories">True to descend into | ||
| 727 | /// subdirectories.</param> | ||
| 728 | /// <param name="fileList">List of files found so far.</param> | ||
| 729 | private void RecursiveGetRelativeFilePathsInDirectoryTree( | ||
| 730 | string dir, | ||
| 731 | string relativeDir, | ||
| 732 | bool includeSubdirectories, | ||
| 733 | IList<string> fileList) | ||
| 734 | { | ||
| 735 | foreach (string file in System.IO.Directory.GetFiles(dir)) | ||
| 736 | { | ||
| 737 | string fileName = Path.GetFileName(file); | ||
| 738 | fileList.Add(Path.Combine(relativeDir, fileName)); | ||
| 739 | } | ||
| 740 | |||
| 741 | if (includeSubdirectories) | ||
| 742 | { | ||
| 743 | foreach (string subDir in System.IO.Directory.GetDirectories(dir)) | ||
| 744 | { | ||
| 745 | string subDirName = Path.GetFileName(subDir); | ||
| 746 | this.RecursiveGetRelativeFilePathsInDirectoryTree( | ||
| 747 | Path.Combine(dir, subDirName), | ||
| 748 | Path.Combine(relativeDir, subDirName), | ||
| 749 | includeSubdirectories, | ||
| 750 | fileList); | ||
| 751 | } | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | /// <summary> | ||
| 756 | /// Uses a CompressionEngine to get ArchiveFileInfo objects from this | ||
| 757 | /// archive, and then associates them with this ArchiveInfo instance. | ||
| 758 | /// </summary> | ||
| 759 | /// <param name="fileFilter">Optional predicate that can determine | ||
| 760 | /// which files to process.</param> | ||
| 761 | /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each | ||
| 762 | /// containing information about a file in the archive.</returns> | ||
| 763 | private IList<ArchiveFileInfo> InternalGetFiles(Predicate<string> fileFilter) | ||
| 764 | { | ||
| 765 | using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) | ||
| 766 | { | ||
| 767 | ArchiveFileStreamContext streamContext = | ||
| 768 | new ArchiveFileStreamContext(this.FullName, null, null); | ||
| 769 | streamContext.EnableOffsetOpen = true; | ||
| 770 | IList<ArchiveFileInfo> files = | ||
| 771 | compressionEngine.GetFileInfo(streamContext, fileFilter); | ||
| 772 | for (int i = 0; i < files.Count; i++) | ||
| 773 | { | ||
| 774 | files[i].Archive = this; | ||
| 775 | } | ||
| 776 | |||
| 777 | return files; | ||
| 778 | } | ||
| 779 | } | ||
| 780 | } | ||
| 781 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.Text; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Contains the data reported in an archive progress event. | ||
| 11 | /// </summary> | ||
| 12 | public class ArchiveProgressEventArgs : EventArgs | ||
| 13 | { | ||
| 14 | private ArchiveProgressType progressType; | ||
| 15 | |||
| 16 | private string currentFileName; | ||
| 17 | private int currentFileNumber; | ||
| 18 | private int totalFiles; | ||
| 19 | private long currentFileBytesProcessed; | ||
| 20 | private long currentFileTotalBytes; | ||
| 21 | |||
| 22 | private string currentArchiveName; | ||
| 23 | private short currentArchiveNumber; | ||
| 24 | private short totalArchives; | ||
| 25 | private long currentArchiveBytesProcessed; | ||
| 26 | private long currentArchiveTotalBytes; | ||
| 27 | |||
| 28 | private long fileBytesProcessed; | ||
| 29 | private long totalFileBytes; | ||
| 30 | |||
| 31 | /// <summary> | ||
| 32 | /// Creates a new ArchiveProgressEventArgs object from specified event parameters. | ||
| 33 | /// </summary> | ||
| 34 | /// <param name="progressType">type of status message</param> | ||
| 35 | /// <param name="currentFileName">name of the file being processed</param> | ||
| 36 | /// <param name="currentFileNumber">number of the current file being processed</param> | ||
| 37 | /// <param name="totalFiles">total number of files to be processed</param> | ||
| 38 | /// <param name="currentFileBytesProcessed">number of bytes processed so far when compressing or extracting a file</param> | ||
| 39 | /// <param name="currentFileTotalBytes">total number of bytes in the current file</param> | ||
| 40 | /// <param name="currentArchiveName">name of the current Archive</param> | ||
| 41 | /// <param name="currentArchiveNumber">current Archive number, when processing a chained set of Archives</param> | ||
| 42 | /// <param name="totalArchives">total number of Archives in a chained set</param> | ||
| 43 | /// <param name="currentArchiveBytesProcessed">number of compressed bytes processed so far during an extraction</param> | ||
| 44 | /// <param name="currentArchiveTotalBytes">total number of compressed bytes to be processed during an extraction</param> | ||
| 45 | /// <param name="fileBytesProcessed">number of uncompressed file bytes processed so far</param> | ||
| 46 | /// <param name="totalFileBytes">total number of uncompressed file bytes to be processed</param> | ||
| 47 | public ArchiveProgressEventArgs( | ||
| 48 | ArchiveProgressType progressType, | ||
| 49 | string currentFileName, | ||
| 50 | int currentFileNumber, | ||
| 51 | int totalFiles, | ||
| 52 | long currentFileBytesProcessed, | ||
| 53 | long currentFileTotalBytes, | ||
| 54 | string currentArchiveName, | ||
| 55 | int currentArchiveNumber, | ||
| 56 | int totalArchives, | ||
| 57 | long currentArchiveBytesProcessed, | ||
| 58 | long currentArchiveTotalBytes, | ||
| 59 | long fileBytesProcessed, | ||
| 60 | long totalFileBytes) | ||
| 61 | { | ||
| 62 | this.progressType = progressType; | ||
| 63 | this.currentFileName = currentFileName; | ||
| 64 | this.currentFileNumber = currentFileNumber; | ||
| 65 | this.totalFiles = totalFiles; | ||
| 66 | this.currentFileBytesProcessed = currentFileBytesProcessed; | ||
| 67 | this.currentFileTotalBytes = currentFileTotalBytes; | ||
| 68 | this.currentArchiveName = currentArchiveName; | ||
| 69 | this.currentArchiveNumber = (short) currentArchiveNumber; | ||
| 70 | this.totalArchives = (short) totalArchives; | ||
| 71 | this.currentArchiveBytesProcessed = currentArchiveBytesProcessed; | ||
| 72 | this.currentArchiveTotalBytes = currentArchiveTotalBytes; | ||
| 73 | this.fileBytesProcessed = fileBytesProcessed; | ||
| 74 | this.totalFileBytes = totalFileBytes; | ||
| 75 | } | ||
| 76 | |||
| 77 | /// <summary> | ||
| 78 | /// Gets the type of status message. | ||
| 79 | /// </summary> | ||
| 80 | /// <value>A <see cref="ArchiveProgressType"/> value indicating what type of progress event occurred.</value> | ||
| 81 | /// <remarks> | ||
| 82 | /// The handler may choose to ignore some types of progress events. | ||
| 83 | /// For example, if the handler will only list each file as it is | ||
| 84 | /// compressed/extracted, it can ignore events that | ||
| 85 | /// are not of type <see cref="ArchiveProgressType.FinishFile"/>. | ||
| 86 | /// </remarks> | ||
| 87 | public ArchiveProgressType ProgressType | ||
| 88 | { | ||
| 89 | get | ||
| 90 | { | ||
| 91 | return this.progressType; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | /// <summary> | ||
| 96 | /// Gets the name of the file being processed. (The name of the file within the Archive; not the external | ||
| 97 | /// file path.) Also includes the internal path of the file, if any. Valid for | ||
| 98 | /// <see cref="ArchiveProgressType.StartFile"/>, <see cref="ArchiveProgressType.PartialFile"/>, | ||
| 99 | /// and <see cref="ArchiveProgressType.FinishFile"/> messages. | ||
| 100 | /// </summary> | ||
| 101 | /// <value>The name of the file currently being processed, or null if processing | ||
| 102 | /// is currently at the stream or archive level.</value> | ||
| 103 | public string CurrentFileName | ||
| 104 | { | ||
| 105 | get | ||
| 106 | { | ||
| 107 | return this.currentFileName; | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | /// <summary> | ||
| 112 | /// Gets the number of the current file being processed. The first file is number 0, and the last file | ||
| 113 | /// is <see cref="TotalFiles"/>-1. Valid for <see cref="ArchiveProgressType.StartFile"/>, | ||
| 114 | /// <see cref="ArchiveProgressType.PartialFile"/>, and <see cref="ArchiveProgressType.FinishFile"/> messages. | ||
| 115 | /// </summary> | ||
| 116 | /// <value>The number of the file currently being processed, or the most recent | ||
| 117 | /// file processed if processing is currently at the stream or archive level.</value> | ||
| 118 | public int CurrentFileNumber | ||
| 119 | { | ||
| 120 | get | ||
| 121 | { | ||
| 122 | return this.currentFileNumber; | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | /// <summary> | ||
| 127 | /// Gets the total number of files to be processed. Valid for all message types. | ||
| 128 | /// </summary> | ||
| 129 | /// <value>The total number of files to be processed that are known so far.</value> | ||
| 130 | public int TotalFiles | ||
| 131 | { | ||
| 132 | get | ||
| 133 | { | ||
| 134 | return this.totalFiles; | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | /// <summary> | ||
| 139 | /// Gets the number of bytes processed so far when compressing or extracting a file. Valid for | ||
| 140 | /// <see cref="ArchiveProgressType.StartFile"/>, <see cref="ArchiveProgressType.PartialFile"/>, | ||
| 141 | /// and <see cref="ArchiveProgressType.FinishFile"/> messages. | ||
| 142 | /// </summary> | ||
| 143 | /// <value>The number of uncompressed bytes processed so far for the current file, | ||
| 144 | /// or 0 if processing is currently at the stream or archive level.</value> | ||
| 145 | public long CurrentFileBytesProcessed | ||
| 146 | { | ||
| 147 | get | ||
| 148 | { | ||
| 149 | return this.currentFileBytesProcessed; | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | /// <summary> | ||
| 154 | /// Gets the total number of bytes in the current file. Valid for <see cref="ArchiveProgressType.StartFile"/>, | ||
| 155 | /// <see cref="ArchiveProgressType.PartialFile"/>, and <see cref="ArchiveProgressType.FinishFile"/> messages. | ||
| 156 | /// </summary> | ||
| 157 | /// <value>The uncompressed size of the current file being processed, | ||
| 158 | /// or 0 if processing is currently at the stream or archive level.</value> | ||
| 159 | public long CurrentFileTotalBytes | ||
| 160 | { | ||
| 161 | get | ||
| 162 | { | ||
| 163 | return this.currentFileTotalBytes; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | /// <summary> | ||
| 168 | /// Gets the name of the current archive. Not necessarily the name of the archive on disk. | ||
| 169 | /// Valid for all message types. | ||
| 170 | /// </summary> | ||
| 171 | /// <value>The name of the current archive, or an empty string if no name was specified.</value> | ||
| 172 | public string CurrentArchiveName | ||
| 173 | { | ||
| 174 | get | ||
| 175 | { | ||
| 176 | return this.currentArchiveName; | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | /// <summary> | ||
| 181 | /// Gets the current archive number, when processing a chained set of archives. Valid for all message types. | ||
| 182 | /// </summary> | ||
| 183 | /// <value>The number of the current archive.</value> | ||
| 184 | /// <remarks>The first archive is number 0, and the last archive is | ||
| 185 | /// <see cref="TotalArchives"/>-1.</remarks> | ||
| 186 | public int CurrentArchiveNumber | ||
| 187 | { | ||
| 188 | get | ||
| 189 | { | ||
| 190 | return this.currentArchiveNumber; | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | /// <summary> | ||
| 195 | /// Gets the total number of known archives in a chained set. Valid for all message types. | ||
| 196 | /// </summary> | ||
| 197 | /// <value>The total number of known archives in a chained set.</value> | ||
| 198 | /// <remarks> | ||
| 199 | /// When using the compression option to auto-split into multiple archives based on data size, | ||
| 200 | /// this value will not be accurate until the end. | ||
| 201 | /// </remarks> | ||
| 202 | public int TotalArchives | ||
| 203 | { | ||
| 204 | get | ||
| 205 | { | ||
| 206 | return this.totalArchives; | ||
| 207 | } | ||
| 208 | } | ||
| 209 | |||
| 210 | /// <summary> | ||
| 211 | /// Gets the number of compressed bytes processed so far during extraction | ||
| 212 | /// of the current archive. Valid for all extraction messages. | ||
| 213 | /// </summary> | ||
| 214 | /// <value>The number of compressed bytes processed so far during extraction | ||
| 215 | /// of the current archive.</value> | ||
| 216 | public long CurrentArchiveBytesProcessed | ||
| 217 | { | ||
| 218 | get | ||
| 219 | { | ||
| 220 | return this.currentArchiveBytesProcessed; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | /// <summary> | ||
| 225 | /// Gets the total number of compressed bytes to be processed during extraction | ||
| 226 | /// of the current archive. Valid for all extraction messages. | ||
| 227 | /// </summary> | ||
| 228 | /// <value>The total number of compressed bytes to be processed during extraction | ||
| 229 | /// of the current archive.</value> | ||
| 230 | public long CurrentArchiveTotalBytes | ||
| 231 | { | ||
| 232 | get | ||
| 233 | { | ||
| 234 | return this.currentArchiveTotalBytes; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | /// <summary> | ||
| 239 | /// Gets the number of uncompressed bytes processed so far among all files. Valid for all message types. | ||
| 240 | /// </summary> | ||
| 241 | /// <value>The number of uncompressed file bytes processed so far among all files.</value> | ||
| 242 | /// <remarks> | ||
| 243 | /// When compared to <see cref="TotalFileBytes"/>, this can be used as a measure of overall progress. | ||
| 244 | /// </remarks> | ||
| 245 | public long FileBytesProcessed | ||
| 246 | { | ||
| 247 | get | ||
| 248 | { | ||
| 249 | return this.fileBytesProcessed; | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | /// <summary> | ||
| 254 | /// Gets the total number of uncompressed file bytes to be processed. Valid for all message types. | ||
| 255 | /// </summary> | ||
| 256 | /// <value>The total number of uncompressed bytes to be processed among all files.</value> | ||
| 257 | public long TotalFileBytes | ||
| 258 | { | ||
| 259 | get | ||
| 260 | { | ||
| 261 | return this.totalFileBytes; | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 265 | #if DEBUG | ||
| 266 | |||
| 267 | /// <summary> | ||
| 268 | /// Creates a string representation of the progress event. | ||
| 269 | /// </summary> | ||
| 270 | /// <returns>a listing of all event parameters and values</returns> | ||
| 271 | public override string ToString() | ||
| 272 | { | ||
| 273 | string formatString = | ||
| 274 | "{0}\n" + | ||
| 275 | "\t CurrentFileName = {1}\n" + | ||
| 276 | "\t CurrentFileNumber = {2}\n" + | ||
| 277 | "\t TotalFiles = {3}\n" + | ||
| 278 | "\t CurrentFileBytesProcessed = {4}\n" + | ||
| 279 | "\t CurrentFileTotalBytes = {5}\n" + | ||
| 280 | "\t CurrentArchiveName = {6}\n" + | ||
| 281 | "\t CurrentArchiveNumber = {7}\n" + | ||
| 282 | "\t TotalArchives = {8}\n" + | ||
| 283 | "\t CurrentArchiveBytesProcessed = {9}\n" + | ||
| 284 | "\t CurrentArchiveTotalBytes = {10}\n" + | ||
| 285 | "\t FileBytesProcessed = {11}\n" + | ||
| 286 | "\t TotalFileBytes = {12}\n"; | ||
| 287 | return String.Format( | ||
| 288 | System.Globalization.CultureInfo.InvariantCulture, | ||
| 289 | formatString, | ||
| 290 | this.ProgressType, | ||
| 291 | this.CurrentFileName, | ||
| 292 | this.CurrentFileNumber, | ||
| 293 | this.TotalFiles, | ||
| 294 | this.CurrentFileBytesProcessed, | ||
| 295 | this.CurrentFileTotalBytes, | ||
| 296 | this.CurrentArchiveName, | ||
| 297 | this.CurrentArchiveNumber, | ||
| 298 | this.TotalArchives, | ||
| 299 | this.CurrentArchiveBytesProcessed, | ||
| 300 | this.CurrentArchiveTotalBytes, | ||
| 301 | this.FileBytesProcessed, | ||
| 302 | this.TotalFileBytes); | ||
| 303 | } | ||
| 304 | |||
| 305 | #endif | ||
| 306 | } | ||
| 307 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.Text; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// The type of progress event. | ||
| 11 | /// </summary> | ||
| 12 | /// <remarks> | ||
| 13 | /// <p>PACKING EXAMPLE: The following sequence of events might be received when | ||
| 14 | /// extracting a simple archive file with 2 files.</p> | ||
| 15 | /// <list type="table"> | ||
| 16 | /// <listheader><term>Message Type</term><description>Description</description></listheader> | ||
| 17 | /// <item><term>StartArchive</term> <description>Begin extracting archive</description></item> | ||
| 18 | /// <item><term>StartFile</term> <description>Begin extracting first file</description></item> | ||
| 19 | /// <item><term>PartialFile</term> <description>Extracting first file</description></item> | ||
| 20 | /// <item><term>PartialFile</term> <description>Extracting first file</description></item> | ||
| 21 | /// <item><term>FinishFile</term> <description>Finished extracting first file</description></item> | ||
| 22 | /// <item><term>StartFile</term> <description>Begin extracting second file</description></item> | ||
| 23 | /// <item><term>PartialFile</term> <description>Extracting second file</description></item> | ||
| 24 | /// <item><term>FinishFile</term> <description>Finished extracting second file</description></item> | ||
| 25 | /// <item><term>FinishArchive</term><description>Finished extracting archive</description></item> | ||
| 26 | /// </list> | ||
| 27 | /// <p></p> | ||
| 28 | /// <p>UNPACKING EXAMPLE: Packing 3 files into 2 archive chunks, where the second file is | ||
| 29 | /// continued to the second archive chunk.</p> | ||
| 30 | /// <list type="table"> | ||
| 31 | /// <listheader><term>Message Type</term><description>Description</description></listheader> | ||
| 32 | /// <item><term>StartFile</term> <description>Begin compressing first file</description></item> | ||
| 33 | /// <item><term>FinishFile</term> <description>Finished compressing first file</description></item> | ||
| 34 | /// <item><term>StartFile</term> <description>Begin compressing second file</description></item> | ||
| 35 | /// <item><term>PartialFile</term> <description>Compressing second file</description></item> | ||
| 36 | /// <item><term>PartialFile</term> <description>Compressing second file</description></item> | ||
| 37 | /// <item><term>FinishFile</term> <description>Finished compressing second file</description></item> | ||
| 38 | /// <item><term>StartArchive</term> <description>Begin writing first archive</description></item> | ||
| 39 | /// <item><term>PartialArchive</term><description>Writing first archive</description></item> | ||
| 40 | /// <item><term>FinishArchive</term> <description>Finished writing first archive</description></item> | ||
| 41 | /// <item><term>StartFile</term> <description>Begin compressing third file</description></item> | ||
| 42 | /// <item><term>PartialFile</term> <description>Compressing third file</description></item> | ||
| 43 | /// <item><term>FinishFile</term> <description>Finished compressing third file</description></item> | ||
| 44 | /// <item><term>StartArchive</term> <description>Begin writing second archive</description></item> | ||
| 45 | /// <item><term>PartialArchive</term><description>Writing second archive</description></item> | ||
| 46 | /// <item><term>FinishArchive</term> <description>Finished writing second archive</description></item> | ||
| 47 | /// </list> | ||
| 48 | /// </remarks> | ||
| 49 | public enum ArchiveProgressType : int | ||
| 50 | { | ||
| 51 | /// <summary>Status message before beginning the packing or unpacking an individual file.</summary> | ||
| 52 | StartFile, | ||
| 53 | |||
| 54 | /// <summary>Status message (possibly reported multiple times) during the process of packing or unpacking a file.</summary> | ||
| 55 | PartialFile, | ||
| 56 | |||
| 57 | /// <summary>Status message after completion of the packing or unpacking an individual file.</summary> | ||
| 58 | FinishFile, | ||
| 59 | |||
| 60 | /// <summary>Status message before beginning the packing or unpacking an archive.</summary> | ||
| 61 | StartArchive, | ||
| 62 | |||
| 63 | /// <summary>Status message (possibly reported multiple times) during the process of packing or unpacking an archiv.</summary> | ||
| 64 | PartialArchive, | ||
| 65 | |||
| 66 | /// <summary>Status message after completion of the packing or unpacking of an archive.</summary> | ||
| 67 | FinishArchive, | ||
| 68 | } | ||
| 69 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Diagnostics.CodeAnalysis; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Stream context used to extract a single file from an archive into a memory stream. | ||
| 11 | /// </summary> | ||
| 12 | [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] | ||
| 13 | public class BasicUnpackStreamContext : IUnpackStreamContext | ||
| 14 | { | ||
| 15 | private Stream archiveStream; | ||
| 16 | private Stream fileStream; | ||
| 17 | |||
| 18 | /// <summary> | ||
| 19 | /// Creates a new BasicExtractStreamContext that reads from the specified archive stream. | ||
| 20 | /// </summary> | ||
| 21 | /// <param name="archiveStream">Archive stream to read from.</param> | ||
| 22 | public BasicUnpackStreamContext(Stream archiveStream) | ||
| 23 | { | ||
| 24 | this.archiveStream = archiveStream; | ||
| 25 | } | ||
| 26 | |||
| 27 | /// <summary> | ||
| 28 | /// Gets the stream for the extracted file, or null if no file was extracted. | ||
| 29 | /// </summary> | ||
| 30 | public Stream FileStream | ||
| 31 | { | ||
| 32 | get | ||
| 33 | { | ||
| 34 | return this.fileStream; | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | /// <summary> | ||
| 39 | /// Opens the archive stream for reading. Returns a DuplicateStream instance, | ||
| 40 | /// so the stream may be virtually opened multiple times. | ||
| 41 | /// </summary> | ||
| 42 | /// <param name="archiveNumber">The archive number to open (ignored; 0 is assumed).</param> | ||
| 43 | /// <param name="archiveName">The name of the archive being opened.</param> | ||
| 44 | /// <param name="compressionEngine">Instance of the compression engine doing the operations.</param> | ||
| 45 | /// <returns>A stream from which archive bytes are read.</returns> | ||
| 46 | public Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine) | ||
| 47 | { | ||
| 48 | return new DuplicateStream(this.archiveStream); | ||
| 49 | } | ||
| 50 | |||
| 51 | /// <summary> | ||
| 52 | /// Does *not* close the stream. The archive stream should be managed by | ||
| 53 | /// the code that invokes the archive extraction. | ||
| 54 | /// </summary> | ||
| 55 | /// <param name="archiveNumber">The archive number of the stream to close.</param> | ||
| 56 | /// <param name="archiveName">The name of the archive being closed.</param> | ||
| 57 | /// <param name="stream">The stream being closed.</param> | ||
| 58 | public void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream) | ||
| 59 | { | ||
| 60 | // Do nothing. | ||
| 61 | } | ||
| 62 | |||
| 63 | /// <summary> | ||
| 64 | /// Opens a stream for writing extracted file bytes. The returned stream is a MemoryStream | ||
| 65 | /// instance, so the file is extracted straight into memory. | ||
| 66 | /// </summary> | ||
| 67 | /// <param name="path">Path of the file within the archive.</param> | ||
| 68 | /// <param name="fileSize">The uncompressed size of the file to be extracted.</param> | ||
| 69 | /// <param name="lastWriteTime">The last write time of the file.</param> | ||
| 70 | /// <returns>A stream where extracted file bytes are to be written.</returns> | ||
| 71 | public Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime) | ||
| 72 | { | ||
| 73 | this.fileStream = new MemoryStream(new byte[fileSize], 0, (int) fileSize, true, true); | ||
| 74 | return this.fileStream; | ||
| 75 | } | ||
| 76 | |||
| 77 | /// <summary> | ||
| 78 | /// Does *not* close the file stream. The file stream is saved in memory so it can | ||
| 79 | /// be read later. | ||
| 80 | /// </summary> | ||
| 81 | /// <param name="path">Path of the file within the archive.</param> | ||
| 82 | /// <param name="stream">The file stream to be closed.</param> | ||
| 83 | /// <param name="attributes">The attributes of the extracted file.</param> | ||
| 84 | /// <param name="lastWriteTime">The last write time of the file.</param> | ||
| 85 | public void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime) | ||
| 86 | { | ||
| 87 | // Do nothing. | ||
| 88 | } | ||
| 89 | } | ||
| 90 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.IO; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Wraps a source stream and carries additional items that are disposed when the stream is closed. | ||
| 11 | /// </summary> | ||
| 12 | public class CargoStream : Stream | ||
| 13 | { | ||
| 14 | private Stream source; | ||
| 15 | private List<IDisposable> cargo; | ||
| 16 | |||
| 17 | /// <summary> | ||
| 18 | /// Creates a new a cargo stream. | ||
| 19 | /// </summary> | ||
| 20 | /// <param name="source">source of the stream</param> | ||
| 21 | /// <param name="cargo">List of additional items that are disposed when the stream is closed. | ||
| 22 | /// The order of the list is the order in which the items are disposed.</param> | ||
| 23 | public CargoStream(Stream source, params IDisposable[] cargo) | ||
| 24 | { | ||
| 25 | if (source == null) | ||
| 26 | { | ||
| 27 | throw new ArgumentNullException("source"); | ||
| 28 | } | ||
| 29 | |||
| 30 | this.source = source; | ||
| 31 | this.cargo = new List<IDisposable>(cargo); | ||
| 32 | } | ||
| 33 | |||
| 34 | /// <summary> | ||
| 35 | /// Gets the source stream of the cargo stream. | ||
| 36 | /// </summary> | ||
| 37 | public Stream Source | ||
| 38 | { | ||
| 39 | get | ||
| 40 | { | ||
| 41 | return this.source; | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | /// <summary> | ||
| 46 | /// Gets the list of additional items that are disposed when the stream is closed. | ||
| 47 | /// The order of the list is the order in which the items are disposed. The contents can be modified any time. | ||
| 48 | /// </summary> | ||
| 49 | public IList<IDisposable> Cargo | ||
| 50 | { | ||
| 51 | get | ||
| 52 | { | ||
| 53 | return this.cargo; | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | /// <summary> | ||
| 58 | /// Gets a value indicating whether the source stream supports reading. | ||
| 59 | /// </summary> | ||
| 60 | /// <value>true if the stream supports reading; otherwise, false.</value> | ||
| 61 | public override bool CanRead | ||
| 62 | { | ||
| 63 | get | ||
| 64 | { | ||
| 65 | return this.source.CanRead; | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// <summary> | ||
| 70 | /// Gets a value indicating whether the source stream supports writing. | ||
| 71 | /// </summary> | ||
| 72 | /// <value>true if the stream supports writing; otherwise, false.</value> | ||
| 73 | public override bool CanWrite | ||
| 74 | { | ||
| 75 | get | ||
| 76 | { | ||
| 77 | return this.source.CanWrite; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// <summary> | ||
| 82 | /// Gets a value indicating whether the source stream supports seeking. | ||
| 83 | /// </summary> | ||
| 84 | /// <value>true if the stream supports seeking; otherwise, false.</value> | ||
| 85 | public override bool CanSeek | ||
| 86 | { | ||
| 87 | get | ||
| 88 | { | ||
| 89 | return this.source.CanSeek; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | /// <summary> | ||
| 94 | /// Gets the length of the source stream. | ||
| 95 | /// </summary> | ||
| 96 | public override long Length | ||
| 97 | { | ||
| 98 | get | ||
| 99 | { | ||
| 100 | return this.source.Length; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | /// <summary> | ||
| 105 | /// Gets or sets the position of the source stream. | ||
| 106 | /// </summary> | ||
| 107 | public override long Position | ||
| 108 | { | ||
| 109 | get | ||
| 110 | { | ||
| 111 | return this.source.Position; | ||
| 112 | } | ||
| 113 | |||
| 114 | set | ||
| 115 | { | ||
| 116 | this.source.Position = value; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | /// <summary> | ||
| 121 | /// Flushes the source stream. | ||
| 122 | /// </summary> | ||
| 123 | public override void Flush() | ||
| 124 | { | ||
| 125 | this.source.Flush(); | ||
| 126 | } | ||
| 127 | |||
| 128 | /// <summary> | ||
| 129 | /// Sets the length of the source stream. | ||
| 130 | /// </summary> | ||
| 131 | /// <param name="value">The desired length of the stream in bytes.</param> | ||
| 132 | public override void SetLength(long value) | ||
| 133 | { | ||
| 134 | this.source.SetLength(value); | ||
| 135 | } | ||
| 136 | |||
| 137 | /// <summary> | ||
| 138 | /// Closes the source stream and also closes the additional objects that are carried. | ||
| 139 | /// </summary> | ||
| 140 | public override void Close() | ||
| 141 | { | ||
| 142 | this.source.Close(); | ||
| 143 | |||
| 144 | foreach (IDisposable cargoObject in this.cargo) | ||
| 145 | { | ||
| 146 | cargoObject.Dispose(); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | /// <summary> | ||
| 151 | /// Reads from the source stream. | ||
| 152 | /// </summary> | ||
| 153 | /// <param name="buffer">An array of bytes. When this method returns, the buffer | ||
| 154 | /// contains the specified byte array with the values between offset and | ||
| 155 | /// (offset + count - 1) replaced by the bytes read from the source.</param> | ||
| 156 | /// <param name="offset">The zero-based byte offset in buffer at which to begin | ||
| 157 | /// storing the data read from the stream.</param> | ||
| 158 | /// <param name="count">The maximum number of bytes to be read from the stream.</param> | ||
| 159 | /// <returns>The total number of bytes read into the buffer. This can be less | ||
| 160 | /// than the number of bytes requested if that many bytes are not currently available, | ||
| 161 | /// or zero (0) if the end of the stream has been reached.</returns> | ||
| 162 | public override int Read(byte[] buffer, int offset, int count) | ||
| 163 | { | ||
| 164 | return this.source.Read(buffer, offset, count); | ||
| 165 | } | ||
| 166 | |||
| 167 | /// <summary> | ||
| 168 | /// Writes to the source stream. | ||
| 169 | /// </summary> | ||
| 170 | /// <param name="buffer">An array of bytes. This method copies count | ||
| 171 | /// bytes from buffer to the stream.</param> | ||
| 172 | /// <param name="offset">The zero-based byte offset in buffer at which | ||
| 173 | /// to begin copying bytes to the stream.</param> | ||
| 174 | /// <param name="count">The number of bytes to be written to the stream.</param> | ||
| 175 | public override void Write(byte[] buffer, int offset, int count) | ||
| 176 | { | ||
| 177 | this.source.Write(buffer, offset, count); | ||
| 178 | } | ||
| 179 | |||
| 180 | /// <summary> | ||
| 181 | /// Changes the position of the source stream. | ||
| 182 | /// </summary> | ||
| 183 | /// <param name="offset">A byte offset relative to the origin parameter.</param> | ||
| 184 | /// <param name="origin">A value of type SeekOrigin indicating the reference | ||
| 185 | /// point used to obtain the new position.</param> | ||
| 186 | /// <returns>The new position within the stream.</returns> | ||
| 187 | public override long Seek(long offset, SeekOrigin origin) | ||
| 188 | { | ||
| 189 | return this.source.Seek(offset, origin); | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <ClassDiagram MajorVersion="1" MinorVersion="1"> | ||
| 3 | <Comment CommentText="File-based classes"> | ||
| 4 | <Position X="2.35" Y="1.442" Height="0.408" Width="0.783" /> | ||
| 5 | </Comment> | ||
| 6 | <Comment CommentText="Stream-based classes"> | ||
| 7 | <Position X="9.649" Y="1.317" Height="0.4" Width="0.996" /> | ||
| 8 | </Comment> | ||
| 9 | <Class Name="WixToolset.Dtf.Compression.ArchiveException" Collapsed="true"> | ||
| 10 | <Position X="3" Y="4.25" Width="2" /> | ||
| 11 | <TypeIdentifier> | ||
| 12 | <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> | ||
| 13 | <FileName>ArchiveException.cs</FileName> | ||
| 14 | </TypeIdentifier> | ||
| 15 | </Class> | ||
| 16 | <Class Name="WixToolset.Dtf.Compression.ArchiveFileInfo"> | ||
| 17 | <Position X="3" Y="0.5" Width="2" /> | ||
| 18 | <Members> | ||
| 19 | <Method Name="ArchiveFileInfo" Hidden="true" /> | ||
| 20 | <Field Name="archiveInfo" Hidden="true" /> | ||
| 21 | <Field Name="archiveNumber" Hidden="true" /> | ||
| 22 | <Field Name="attributes" Hidden="true" /> | ||
| 23 | <Field Name="exists" Hidden="true" /> | ||
| 24 | <Method Name="GetObjectData" Hidden="true" /> | ||
| 25 | <Field Name="initialized" Hidden="true" /> | ||
| 26 | <Field Name="lastWriteTime" Hidden="true" /> | ||
| 27 | <Field Name="length" Hidden="true" /> | ||
| 28 | <Field Name="name" Hidden="true" /> | ||
| 29 | <Field Name="path" Hidden="true" /> | ||
| 30 | </Members> | ||
| 31 | <TypeIdentifier> | ||
| 32 | <HashCode>AAAgAAAAIRJAAIMEAEACgARwAAEEEAAAASAAAAEAIAA=</HashCode> | ||
| 33 | <FileName>ArchiveFileInfo.cs</FileName> | ||
| 34 | </TypeIdentifier> | ||
| 35 | </Class> | ||
| 36 | <Class Name="WixToolset.Dtf.Compression.ArchiveInfo"> | ||
| 37 | <Position X="0.5" Y="0.5" Width="2.25" /> | ||
| 38 | <Members> | ||
| 39 | <Method Name="ArchiveInfo" Hidden="true" /> | ||
| 40 | <Method Name="CreateStringDictionary" Hidden="true" /> | ||
| 41 | <Method Name="GetFile" Hidden="true" /> | ||
| 42 | <Method Name="GetRelativeFilePathsInDirectoryTree" Hidden="true" /> | ||
| 43 | <Method Name="InternalGetFiles" Hidden="true" /> | ||
| 44 | <Method Name="RecursiveGetRelativeFilePathsInDirectoryTree" Hidden="true" /> | ||
| 45 | </Members> | ||
| 46 | <TypeIdentifier> | ||
| 47 | <HashCode>AAEAABAAIAAAAgQEAAgBAARAHAEJACAAAABEAAkAMAI=</HashCode> | ||
| 48 | <FileName>ArchiveInfo.cs</FileName> | ||
| 49 | </TypeIdentifier> | ||
| 50 | </Class> | ||
| 51 | <Class Name="WixToolset.Dtf.Compression.ArchiveFileStreamContext"> | ||
| 52 | <Position X="12.75" Y="0.75" Width="2.25" /> | ||
| 53 | <Members> | ||
| 54 | <Field Name="archiveFiles" Hidden="true" /> | ||
| 55 | <Method Name="ArchiveFileStreamContext" Hidden="true" /> | ||
| 56 | <Method Name="CloseArchiveReadStream" Hidden="true" /> | ||
| 57 | <Method Name="CloseArchiveWriteStream" Hidden="true" /> | ||
| 58 | <Method Name="CloseFileReadStream" Hidden="true" /> | ||
| 59 | <Method Name="CloseFileWriteStream" Hidden="true" /> | ||
| 60 | <Field Name="directory" Hidden="true" /> | ||
| 61 | <Field Name="enableOffsetOpen" Hidden="true" /> | ||
| 62 | <Field Name="extractOnlyNewerFiles" Hidden="true" /> | ||
| 63 | <Field Name="files" Hidden="true" /> | ||
| 64 | <Method Name="GetArchiveName" Hidden="true" /> | ||
| 65 | <Method Name="GetOption" Hidden="true" /> | ||
| 66 | <Method Name="OpenArchiveReadStream" Hidden="true" /> | ||
| 67 | <Method Name="OpenArchiveWriteStream" Hidden="true" /> | ||
| 68 | <Method Name="OpenFileReadStream" Hidden="true" /> | ||
| 69 | <Method Name="OpenFileWriteStream" Hidden="true" /> | ||
| 70 | <Method Name="TranslateFilePath" Hidden="true" /> | ||
| 71 | </Members> | ||
| 72 | <TypeIdentifier> | ||
| 73 | <HashCode>AEQAABgAAACQAACACACAAgAQAAIgAAAAACAMgAAEAKA=</HashCode> | ||
| 74 | <FileName>ArchiveFileStreamContext.cs</FileName> | ||
| 75 | </TypeIdentifier> | ||
| 76 | <Lollipop Position="0.2" /> | ||
| 77 | </Class> | ||
| 78 | <Class Name="WixToolset.Dtf.Compression.ArchiveProgressEventArgs"> | ||
| 79 | <Position X="5.25" Y="0.5" Width="2.25" /> | ||
| 80 | <Members> | ||
| 81 | <Method Name="ArchiveProgressEventArgs" Hidden="true" /> | ||
| 82 | <Field Name="currentArchiveBytesProcessed" Hidden="true" /> | ||
| 83 | <Field Name="currentArchiveName" Hidden="true" /> | ||
| 84 | <Field Name="currentArchiveNumber" Hidden="true" /> | ||
| 85 | <Field Name="currentArchiveTotalBytes" Hidden="true" /> | ||
| 86 | <Field Name="currentFileBytesProcessed" Hidden="true" /> | ||
| 87 | <Field Name="currentFileName" Hidden="true" /> | ||
| 88 | <Field Name="currentFileNumber" Hidden="true" /> | ||
| 89 | <Field Name="currentFileTotalBytes" Hidden="true" /> | ||
| 90 | <Field Name="fileBytesProcessed" Hidden="true" /> | ||
| 91 | <Field Name="progressType" Hidden="true" /> | ||
| 92 | <Field Name="totalArchives" Hidden="true" /> | ||
| 93 | <Field Name="totalFileBytes" Hidden="true" /> | ||
| 94 | <Field Name="totalFiles" Hidden="true" /> | ||
| 95 | </Members> | ||
| 96 | <TypeIdentifier> | ||
| 97 | <HashCode>AAMCAQASACAAABBBAAASUAAAQBAAAMAAAAGQAAgBEAA=</HashCode> | ||
| 98 | <FileName>ArchiveProgressEventArgs.cs</FileName> | ||
| 99 | </TypeIdentifier> | ||
| 100 | </Class> | ||
| 101 | <Class Name="WixToolset.Dtf.Compression.BasicUnpackStreamContext"> | ||
| 102 | <Position X="12.75" Y="3" Width="2.25" /> | ||
| 103 | <Members> | ||
| 104 | <Field Name="archiveStream" Hidden="true" /> | ||
| 105 | <Method Name="BasicUnpackStreamContext" Hidden="true" /> | ||
| 106 | <Method Name="CloseArchiveReadStream" Hidden="true" /> | ||
| 107 | <Method Name="CloseFileWriteStream" Hidden="true" /> | ||
| 108 | <Field Name="fileStream" Hidden="true" /> | ||
| 109 | <Method Name="OpenArchiveReadStream" Hidden="true" /> | ||
| 110 | <Method Name="OpenFileWriteStream" Hidden="true" /> | ||
| 111 | </Members> | ||
| 112 | <TypeIdentifier> | ||
| 113 | <HashCode>AAAAAAgAAACEAAAAAAAAAAAAAAAgAAAAIAAMAAAAAAA=</HashCode> | ||
| 114 | <FileName>BasicUnpackStreamContext.cs</FileName> | ||
| 115 | </TypeIdentifier> | ||
| 116 | <Lollipop Position="0.2" /> | ||
| 117 | </Class> | ||
| 118 | <Class Name="WixToolset.Dtf.Compression.CompressionEngine"> | ||
| 119 | <Position X="8" Y="0.5" Width="2.25" /> | ||
| 120 | <Members> | ||
| 121 | <Method Name="~CompressionEngine" Hidden="true" /> | ||
| 122 | <Method Name="CompressionEngine" Hidden="true" /> | ||
| 123 | <Field Name="compressionLevel" Hidden="true" /> | ||
| 124 | <Field Name="dontUseTempFiles" Hidden="true" /> | ||
| 125 | </Members> | ||
| 126 | <TypeIdentifier> | ||
| 127 | <HashCode>AAAEAAAABCBAACRgAAAAAAQAAEAAAAAAQAEAAAiAAAI=</HashCode> | ||
| 128 | <FileName>CompressionEngine.cs</FileName> | ||
| 129 | </TypeIdentifier> | ||
| 130 | <Lollipop Position="0.2" /> | ||
| 131 | </Class> | ||
| 132 | <Class Name="WixToolset.Dtf.Compression.DuplicateStream" Collapsed="true"> | ||
| 133 | <Position X="10.5" Y="4.25" Width="2" /> | ||
| 134 | <TypeIdentifier> | ||
| 135 | <HashCode>AAAAAEAAAgAAQAIgGAAAIABgAAAAAAAAAAAAAAGIACA=</HashCode> | ||
| 136 | <FileName>DuplicateStream.cs</FileName> | ||
| 137 | </TypeIdentifier> | ||
| 138 | </Class> | ||
| 139 | <Class Name="WixToolset.Dtf.Compression.OffsetStream" Collapsed="true"> | ||
| 140 | <Position X="8" Y="4.25" Width="2" /> | ||
| 141 | <TypeIdentifier> | ||
| 142 | <HashCode>AAAAAAAAAgAAQAIgGAAAAABgAAAAAEAgAAAAAAGIwCA=</HashCode> | ||
| 143 | <FileName>OffsetStream.cs</FileName> | ||
| 144 | </TypeIdentifier> | ||
| 145 | </Class> | ||
| 146 | <Interface Name="WixToolset.Dtf.Compression.IPackStreamContext"> | ||
| 147 | <Position X="10.5" Y="0.5" Width="2" /> | ||
| 148 | <TypeIdentifier> | ||
| 149 | <HashCode>AAAAAAAAAAAAAACAAACAAAAQAAAgAAAAACAIAAAAAAA=</HashCode> | ||
| 150 | <FileName>IPackStreamContext.cs</FileName> | ||
| 151 | </TypeIdentifier> | ||
| 152 | </Interface> | ||
| 153 | <Interface Name="WixToolset.Dtf.Compression.IUnpackStreamContext"> | ||
| 154 | <Position X="10.5" Y="2.5" Width="2" /> | ||
| 155 | <TypeIdentifier> | ||
| 156 | <HashCode>AAAAAAgAAACAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAA=</HashCode> | ||
| 157 | <FileName>IUnpackStreamContext.cs</FileName> | ||
| 158 | </TypeIdentifier> | ||
| 159 | </Interface> | ||
| 160 | <Enum Name="WixToolset.Dtf.Compression.ArchiveProgressType" Collapsed="true"> | ||
| 161 | <Position X="5.25" Y="3.75" Width="2" /> | ||
| 162 | <TypeIdentifier> | ||
| 163 | <HashCode>QAAAAAAAAAAAAIAAgAAAAAAAAAQAAAAACIAAAAAAAAA=</HashCode> | ||
| 164 | <FileName>ArchiveProgressType.cs</FileName> | ||
| 165 | </TypeIdentifier> | ||
| 166 | </Enum> | ||
| 167 | <Enum Name="WixToolset.Dtf.Compression.CompressionLevel" Collapsed="true"> | ||
| 168 | <Position X="5.25" Y="4.5" Width="2" /> | ||
| 169 | <TypeIdentifier> | ||
| 170 | <HashCode>AAAAAAAAABAAAAAAEAAAAAAAAAAIAAAAAAAAAAEAAAA=</HashCode> | ||
| 171 | <FileName>CompressionLevel.cs</FileName> | ||
| 172 | </TypeIdentifier> | ||
| 173 | </Enum> | ||
| 174 | <Font Name="Verdana" Size="8" /> | ||
| 175 | </ClassDiagram> \ 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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Collections.Generic; | ||
| 8 | using System.Globalization; | ||
| 9 | |||
| 10 | /// <summary> | ||
| 11 | /// Base class for an engine capable of packing and unpacking a particular | ||
| 12 | /// compressed file format. | ||
| 13 | /// </summary> | ||
| 14 | public abstract class CompressionEngine : IDisposable | ||
| 15 | { | ||
| 16 | private CompressionLevel compressionLevel; | ||
| 17 | private bool dontUseTempFiles; | ||
| 18 | |||
| 19 | /// <summary> | ||
| 20 | /// Creates a new instance of the compression engine base class. | ||
| 21 | /// </summary> | ||
| 22 | protected CompressionEngine() | ||
| 23 | { | ||
| 24 | this.compressionLevel = CompressionLevel.Normal; | ||
| 25 | } | ||
| 26 | |||
| 27 | /// <summary> | ||
| 28 | /// Disposes the compression engine. | ||
| 29 | /// </summary> | ||
| 30 | ~CompressionEngine() | ||
| 31 | { | ||
| 32 | this.Dispose(false); | ||
| 33 | } | ||
| 34 | |||
| 35 | /// <summary> | ||
| 36 | /// Occurs when the compression engine reports progress in packing | ||
| 37 | /// or unpacking an archive. | ||
| 38 | /// </summary> | ||
| 39 | /// <seealso cref="ArchiveProgressType"/> | ||
| 40 | public event EventHandler<ArchiveProgressEventArgs> Progress; | ||
| 41 | |||
| 42 | /// <summary> | ||
| 43 | /// Gets or sets a flag indicating whether temporary files are created | ||
| 44 | /// and used during compression. | ||
| 45 | /// </summary> | ||
| 46 | /// <value>True if temporary files are used; false if compression is done | ||
| 47 | /// entirely in-memory.</value> | ||
| 48 | /// <remarks>The value of this property is true by default. Using temporary | ||
| 49 | /// files can greatly reduce the memory requirement of compression, | ||
| 50 | /// especially when compressing large archives. However, setting this property | ||
| 51 | /// to false may yield slightly better performance when creating small | ||
| 52 | /// archives. Or it may be necessary if the process does not have sufficient | ||
| 53 | /// privileges to create temporary files.</remarks> | ||
| 54 | public bool UseTempFiles | ||
| 55 | { | ||
| 56 | get | ||
| 57 | { | ||
| 58 | return !this.dontUseTempFiles; | ||
| 59 | } | ||
| 60 | |||
| 61 | set | ||
| 62 | { | ||
| 63 | this.dontUseTempFiles = !value; | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | /// <summary> | ||
| 68 | /// Compression level to use when compressing files. | ||
| 69 | /// </summary> | ||
| 70 | /// <value>A compression level ranging from minimum to maximum compression, | ||
| 71 | /// or no compression.</value> | ||
| 72 | public CompressionLevel CompressionLevel | ||
| 73 | { | ||
| 74 | get | ||
| 75 | { | ||
| 76 | return this.compressionLevel; | ||
| 77 | } | ||
| 78 | |||
| 79 | set | ||
| 80 | { | ||
| 81 | this.compressionLevel = value; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | /// <summary> | ||
| 86 | /// Disposes of resources allocated by the compression engine. | ||
| 87 | /// </summary> | ||
| 88 | public void Dispose() | ||
| 89 | { | ||
| 90 | this.Dispose(true); | ||
| 91 | GC.SuppressFinalize(this); | ||
| 92 | } | ||
| 93 | |||
| 94 | /// <summary> | ||
| 95 | /// Creates an archive. | ||
| 96 | /// </summary> | ||
| 97 | /// <param name="streamContext">A context interface to handle opening | ||
| 98 | /// and closing of archive and file streams.</param> | ||
| 99 | /// <param name="files">The paths of the files in the archive | ||
| 100 | /// (not external file paths).</param> | ||
| 101 | /// <exception cref="ArchiveException">The archive could not be | ||
| 102 | /// created.</exception> | ||
| 103 | /// <remarks> | ||
| 104 | /// The stream context implementation may provide a mapping from the | ||
| 105 | /// file paths within the archive to the external file paths. | ||
| 106 | /// </remarks> | ||
| 107 | public void Pack(IPackStreamContext streamContext, IEnumerable<string> files) | ||
| 108 | { | ||
| 109 | if (files == null) | ||
| 110 | { | ||
| 111 | throw new ArgumentNullException("files"); | ||
| 112 | } | ||
| 113 | |||
| 114 | this.Pack(streamContext, files, 0); | ||
| 115 | } | ||
| 116 | |||
| 117 | /// <summary> | ||
| 118 | /// Creates an archive or chain of archives. | ||
| 119 | /// </summary> | ||
| 120 | /// <param name="streamContext">A context interface to handle opening | ||
| 121 | /// and closing of archive and file streams.</param> | ||
| 122 | /// <param name="files">The paths of the files in the archive (not | ||
| 123 | /// external file paths).</param> | ||
| 124 | /// <param name="maxArchiveSize">The maximum number of bytes for one | ||
| 125 | /// archive before the contents are chained to the next archive, or zero | ||
| 126 | /// for unlimited archive size.</param> | ||
| 127 | /// <exception cref="ArchiveException">The archive could not be | ||
| 128 | /// created.</exception> | ||
| 129 | /// <remarks> | ||
| 130 | /// The stream context implementation may provide a mapping from the file | ||
| 131 | /// paths within the archive to the external file paths. | ||
| 132 | /// </remarks> | ||
| 133 | public abstract void Pack( | ||
| 134 | IPackStreamContext streamContext, | ||
| 135 | IEnumerable<string> files, | ||
| 136 | long maxArchiveSize); | ||
| 137 | |||
| 138 | /// <summary> | ||
| 139 | /// Checks whether a Stream begins with a header that indicates | ||
| 140 | /// it is a valid archive. | ||
| 141 | /// </summary> | ||
| 142 | /// <param name="stream">Stream for reading the archive file.</param> | ||
| 143 | /// <returns>True if the stream is a valid archive | ||
| 144 | /// (with no offset); false otherwise.</returns> | ||
| 145 | public abstract bool IsArchive(Stream stream); | ||
| 146 | |||
| 147 | /// <summary> | ||
| 148 | /// Gets the offset of an archive that is positioned 0 or more bytes | ||
| 149 | /// from the start of the Stream. | ||
| 150 | /// </summary> | ||
| 151 | /// <param name="stream">A stream for reading the archive.</param> | ||
| 152 | /// <returns>The offset in bytes of the archive, | ||
| 153 | /// or -1 if no archive is found in the Stream.</returns> | ||
| 154 | /// <remarks>The archive must begin on a 4-byte boundary.</remarks> | ||
| 155 | public virtual long FindArchiveOffset(Stream stream) | ||
| 156 | { | ||
| 157 | if (stream == null) | ||
| 158 | { | ||
| 159 | throw new ArgumentNullException("stream"); | ||
| 160 | } | ||
| 161 | |||
| 162 | long sectionSize = 4; | ||
| 163 | long length = stream.Length; | ||
| 164 | for (long offset = 0; offset <= length - sectionSize; offset += sectionSize) | ||
| 165 | { | ||
| 166 | stream.Seek(offset, SeekOrigin.Begin); | ||
| 167 | if (this.IsArchive(stream)) | ||
| 168 | { | ||
| 169 | return offset; | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | return -1; | ||
| 174 | } | ||
| 175 | |||
| 176 | /// <summary> | ||
| 177 | /// Gets information about all files in an archive stream. | ||
| 178 | /// </summary> | ||
| 179 | /// <param name="stream">A stream for reading the archive.</param> | ||
| 180 | /// <returns>Information about all files in the archive stream.</returns> | ||
| 181 | /// <exception cref="ArchiveException">The stream is not a valid | ||
| 182 | /// archive.</exception> | ||
| 183 | public IList<ArchiveFileInfo> GetFileInfo(Stream stream) | ||
| 184 | { | ||
| 185 | return this.GetFileInfo(new BasicUnpackStreamContext(stream), null); | ||
| 186 | } | ||
| 187 | |||
| 188 | /// <summary> | ||
| 189 | /// Gets information about files in an archive or archive chain. | ||
| 190 | /// </summary> | ||
| 191 | /// <param name="streamContext">A context interface to handle opening | ||
| 192 | /// and closing of archive and file streams.</param> | ||
| 193 | /// <param name="fileFilter">A predicate that can determine | ||
| 194 | /// which files to process, optional.</param> | ||
| 195 | /// <returns>Information about files in the archive stream.</returns> | ||
| 196 | /// <exception cref="ArchiveException">The archive provided | ||
| 197 | /// by the stream context is not valid.</exception> | ||
| 198 | /// <remarks> | ||
| 199 | /// The <paramref name="fileFilter"/> predicate takes an internal file | ||
| 200 | /// path and returns true to include the file or false to exclude it. | ||
| 201 | /// </remarks> | ||
| 202 | public abstract IList<ArchiveFileInfo> GetFileInfo( | ||
| 203 | IUnpackStreamContext streamContext, | ||
| 204 | Predicate<string> fileFilter); | ||
| 205 | |||
| 206 | /// <summary> | ||
| 207 | /// Gets the list of files in an archive Stream. | ||
| 208 | /// </summary> | ||
| 209 | /// <param name="stream">A stream for reading the archive.</param> | ||
| 210 | /// <returns>A list of the paths of all files contained in the | ||
| 211 | /// archive.</returns> | ||
| 212 | /// <exception cref="ArchiveException">The stream is not a valid | ||
| 213 | /// archive.</exception> | ||
| 214 | public IList<string> GetFiles(Stream stream) | ||
| 215 | { | ||
| 216 | return this.GetFiles(new BasicUnpackStreamContext(stream), null); | ||
| 217 | } | ||
| 218 | |||
| 219 | /// <summary> | ||
| 220 | /// Gets the list of files in an archive or archive chain. | ||
| 221 | /// </summary> | ||
| 222 | /// <param name="streamContext">A context interface to handle opening | ||
| 223 | /// and closing of archive and file streams.</param> | ||
| 224 | /// <param name="fileFilter">A predicate that can determine | ||
| 225 | /// which files to process, optional.</param> | ||
| 226 | /// <returns>An array containing the names of all files contained in | ||
| 227 | /// the archive or archive chain.</returns> | ||
| 228 | /// <exception cref="ArchiveException">The archive provided | ||
| 229 | /// by the stream context is not valid.</exception> | ||
| 230 | /// <remarks> | ||
| 231 | /// The <paramref name="fileFilter"/> predicate takes an internal file | ||
| 232 | /// path and returns true to include the file or false to exclude it. | ||
| 233 | /// </remarks> | ||
| 234 | public IList<string> GetFiles( | ||
| 235 | IUnpackStreamContext streamContext, | ||
| 236 | Predicate<string> fileFilter) | ||
| 237 | { | ||
| 238 | if (streamContext == null) | ||
| 239 | { | ||
| 240 | throw new ArgumentNullException("streamContext"); | ||
| 241 | } | ||
| 242 | |||
| 243 | IList<ArchiveFileInfo> files = | ||
| 244 | this.GetFileInfo(streamContext, fileFilter); | ||
| 245 | IList<string> fileNames = new List<string>(files.Count); | ||
| 246 | for (int i = 0; i < files.Count; i++) | ||
| 247 | { | ||
| 248 | fileNames.Add(files[i].Name); | ||
| 249 | } | ||
| 250 | |||
| 251 | return fileNames; | ||
| 252 | } | ||
| 253 | |||
| 254 | /// <summary> | ||
| 255 | /// Reads a single file from an archive stream. | ||
| 256 | /// </summary> | ||
| 257 | /// <param name="stream">A stream for reading the archive.</param> | ||
| 258 | /// <param name="path">The path of the file within the archive | ||
| 259 | /// (not the external file path).</param> | ||
| 260 | /// <returns>A stream for reading the extracted file, or null | ||
| 261 | /// if the file does not exist in the archive.</returns> | ||
| 262 | /// <exception cref="ArchiveException">The stream is not a valid | ||
| 263 | /// archive.</exception> | ||
| 264 | /// <remarks>The entire extracted file is cached in memory, so this | ||
| 265 | /// method requires enough free memory to hold the file.</remarks> | ||
| 266 | public Stream Unpack(Stream stream, string path) | ||
| 267 | { | ||
| 268 | if (stream == null) | ||
| 269 | { | ||
| 270 | throw new ArgumentNullException("stream"); | ||
| 271 | } | ||
| 272 | |||
| 273 | if (path == null) | ||
| 274 | { | ||
| 275 | throw new ArgumentNullException("path"); | ||
| 276 | } | ||
| 277 | |||
| 278 | BasicUnpackStreamContext streamContext = | ||
| 279 | new BasicUnpackStreamContext(stream); | ||
| 280 | this.Unpack( | ||
| 281 | streamContext, | ||
| 282 | delegate(string match) | ||
| 283 | { | ||
| 284 | return String.Compare( | ||
| 285 | match, path, true, CultureInfo.InvariantCulture) == 0; | ||
| 286 | }); | ||
| 287 | |||
| 288 | Stream extractStream = streamContext.FileStream; | ||
| 289 | if (extractStream != null) | ||
| 290 | { | ||
| 291 | extractStream.Position = 0; | ||
| 292 | } | ||
| 293 | |||
| 294 | return extractStream; | ||
| 295 | } | ||
| 296 | |||
| 297 | /// <summary> | ||
| 298 | /// Extracts files from an archive or archive chain. | ||
| 299 | /// </summary> | ||
| 300 | /// <param name="streamContext">A context interface to handle opening | ||
| 301 | /// and closing of archive and file streams.</param> | ||
| 302 | /// <param name="fileFilter">An optional predicate that can determine | ||
| 303 | /// which files to process.</param> | ||
| 304 | /// <exception cref="ArchiveException">The archive provided | ||
| 305 | /// by the stream context is not valid.</exception> | ||
| 306 | /// <remarks> | ||
| 307 | /// The <paramref name="fileFilter"/> predicate takes an internal file | ||
| 308 | /// path and returns true to include the file or false to exclude it. | ||
| 309 | /// </remarks> | ||
| 310 | public abstract void Unpack( | ||
| 311 | IUnpackStreamContext streamContext, | ||
| 312 | Predicate<string> fileFilter); | ||
| 313 | |||
| 314 | /// <summary> | ||
| 315 | /// Called by sublcasses to distribute a packing or unpacking progress | ||
| 316 | /// event to listeners. | ||
| 317 | /// </summary> | ||
| 318 | /// <param name="e">Event details.</param> | ||
| 319 | protected void OnProgress(ArchiveProgressEventArgs e) | ||
| 320 | { | ||
| 321 | if (this.Progress != null) | ||
| 322 | { | ||
| 323 | this.Progress(this, e); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | |||
| 327 | /// <summary> | ||
| 328 | /// Disposes of resources allocated by the compression engine. | ||
| 329 | /// </summary> | ||
| 330 | /// <param name="disposing">If true, the method has been called | ||
| 331 | /// directly or indirectly by a user's code, so managed and unmanaged | ||
| 332 | /// resources will be disposed. If false, the method has been called by | ||
| 333 | /// the runtime from inside the finalizer, and only unmanaged resources | ||
| 334 | /// will be disposed.</param> | ||
| 335 | protected virtual void Dispose(bool disposing) | ||
| 336 | { | ||
| 337 | } | ||
| 338 | |||
| 339 | /// <summary> | ||
| 340 | /// Compresion utility function for converting old-style | ||
| 341 | /// date and time values to a DateTime structure. | ||
| 342 | /// </summary> | ||
| 343 | public static void DosDateAndTimeToDateTime( | ||
| 344 | short dosDate, short dosTime, out DateTime dateTime) | ||
| 345 | { | ||
| 346 | if (dosDate == 0 && dosTime == 0) | ||
| 347 | { | ||
| 348 | dateTime = DateTime.MinValue; | ||
| 349 | } | ||
| 350 | else | ||
| 351 | { | ||
| 352 | long fileTime; | ||
| 353 | SafeNativeMethods.DosDateTimeToFileTime(dosDate, dosTime, out fileTime); | ||
| 354 | dateTime = DateTime.FromFileTimeUtc(fileTime); | ||
| 355 | dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Local); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | /// <summary> | ||
| 360 | /// Compresion utility function for converting a DateTime structure | ||
| 361 | /// to old-style date and time values. | ||
| 362 | /// </summary> | ||
| 363 | public static void DateTimeToDosDateAndTime( | ||
| 364 | DateTime dateTime, out short dosDate, out short dosTime) | ||
| 365 | { | ||
| 366 | dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc); | ||
| 367 | long filetime = dateTime.ToFileTimeUtc(); | ||
| 368 | SafeNativeMethods.FileTimeToDosDateTime(ref filetime, out dosDate, out dosTime); | ||
| 369 | } | ||
| 370 | } | ||
| 371 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.Text; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// Specifies the compression level ranging from minimum compresion to | ||
| 11 | /// maximum compression, or no compression at all. | ||
| 12 | /// </summary> | ||
| 13 | /// <remarks> | ||
| 14 | /// Although only four values are enumerated, any integral value between | ||
| 15 | /// <see cref="CompressionLevel.Min"/> and <see cref="CompressionLevel.Max"/> can also be used. | ||
| 16 | /// </remarks> | ||
| 17 | public enum CompressionLevel | ||
| 18 | { | ||
| 19 | /// <summary>Do not compress files, only store.</summary> | ||
| 20 | None = 0, | ||
| 21 | |||
| 22 | /// <summary>Minimum compression; fastest.</summary> | ||
| 23 | Min = 1, | ||
| 24 | |||
| 25 | /// <summary>A compromize between speed and compression efficiency.</summary> | ||
| 26 | Normal = 6, | ||
| 27 | |||
| 28 | /// <summary>Maximum compression; slowest.</summary> | ||
| 29 | Max = 10 | ||
| 30 | } | ||
| 31 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | |||
| 8 | /// <summary> | ||
| 9 | /// Duplicates a source stream by maintaining a separate position. | ||
| 10 | /// </summary> | ||
| 11 | /// <remarks> | ||
| 12 | /// WARNING: duplicate streams are not thread-safe with respect to each other or the original stream. | ||
| 13 | /// If multiple threads use duplicate copies of the same stream, they must synchronize for any operations. | ||
| 14 | /// </remarks> | ||
| 15 | public class DuplicateStream : Stream | ||
| 16 | { | ||
| 17 | private Stream source; | ||
| 18 | private long position; | ||
| 19 | |||
| 20 | /// <summary> | ||
| 21 | /// Creates a new duplicate of a stream. | ||
| 22 | /// </summary> | ||
| 23 | /// <param name="source">source of the duplicate</param> | ||
| 24 | public DuplicateStream(Stream source) | ||
| 25 | { | ||
| 26 | if (source == null) | ||
| 27 | { | ||
| 28 | throw new ArgumentNullException("source"); | ||
| 29 | } | ||
| 30 | |||
| 31 | this.source = DuplicateStream.OriginalStream(source); | ||
| 32 | } | ||
| 33 | |||
| 34 | /// <summary> | ||
| 35 | /// Gets the original stream that was used to create the duplicate. | ||
| 36 | /// </summary> | ||
| 37 | public Stream Source | ||
| 38 | { | ||
| 39 | get | ||
| 40 | { | ||
| 41 | return this.source; | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | /// <summary> | ||
| 46 | /// Gets a value indicating whether the source stream supports reading. | ||
| 47 | /// </summary> | ||
| 48 | /// <value>true if the stream supports reading; otherwise, false.</value> | ||
| 49 | public override bool CanRead | ||
| 50 | { | ||
| 51 | get | ||
| 52 | { | ||
| 53 | return this.source.CanRead; | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | /// <summary> | ||
| 58 | /// Gets a value indicating whether the source stream supports writing. | ||
| 59 | /// </summary> | ||
| 60 | /// <value>true if the stream supports writing; otherwise, false.</value> | ||
| 61 | public override bool CanWrite | ||
| 62 | { | ||
| 63 | get | ||
| 64 | { | ||
| 65 | return this.source.CanWrite; | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// <summary> | ||
| 70 | /// Gets a value indicating whether the source stream supports seeking. | ||
| 71 | /// </summary> | ||
| 72 | /// <value>true if the stream supports seeking; otherwise, false.</value> | ||
| 73 | public override bool CanSeek | ||
| 74 | { | ||
| 75 | get | ||
| 76 | { | ||
| 77 | return this.source.CanSeek; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// <summary> | ||
| 82 | /// Gets the length of the source stream. | ||
| 83 | /// </summary> | ||
| 84 | public override long Length | ||
| 85 | { | ||
| 86 | get | ||
| 87 | { | ||
| 88 | return this.source.Length; | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | /// <summary> | ||
| 93 | /// Gets or sets the position of the current stream, | ||
| 94 | /// ignoring the position of the source stream. | ||
| 95 | /// </summary> | ||
| 96 | public override long Position | ||
| 97 | { | ||
| 98 | get | ||
| 99 | { | ||
| 100 | return this.position; | ||
| 101 | } | ||
| 102 | |||
| 103 | set | ||
| 104 | { | ||
| 105 | this.position = value; | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | /// <summary> | ||
| 110 | /// Retrieves the original stream from a possible duplicate stream. | ||
| 111 | /// </summary> | ||
| 112 | /// <param name="stream">Possible duplicate stream.</param> | ||
| 113 | /// <returns>If the stream is a DuplicateStream, returns | ||
| 114 | /// the duplicate's source; otherwise returns the same stream.</returns> | ||
| 115 | public static Stream OriginalStream(Stream stream) | ||
| 116 | { | ||
| 117 | DuplicateStream dupStream = stream as DuplicateStream; | ||
| 118 | return dupStream != null ? dupStream.Source : stream; | ||
| 119 | } | ||
| 120 | |||
| 121 | /// <summary> | ||
| 122 | /// Flushes the source stream. | ||
| 123 | /// </summary> | ||
| 124 | public override void Flush() | ||
| 125 | { | ||
| 126 | this.source.Flush(); | ||
| 127 | } | ||
| 128 | |||
| 129 | /// <summary> | ||
| 130 | /// Sets the length of the source stream. | ||
| 131 | /// </summary> | ||
| 132 | /// <param name="value">The desired length of the stream in bytes.</param> | ||
| 133 | public override void SetLength(long value) | ||
| 134 | { | ||
| 135 | this.source.SetLength(value); | ||
| 136 | } | ||
| 137 | |||
| 138 | /// <summary> | ||
| 139 | /// Closes the underlying stream, effectively closing ALL duplicates. | ||
| 140 | /// </summary> | ||
| 141 | public override void Close() | ||
| 142 | { | ||
| 143 | this.source.Close(); | ||
| 144 | } | ||
| 145 | |||
| 146 | /// <summary> | ||
| 147 | /// Reads from the source stream while maintaining a separate position | ||
| 148 | /// and not impacting the source stream's position. | ||
| 149 | /// </summary> | ||
| 150 | /// <param name="buffer">An array of bytes. When this method returns, the buffer | ||
| 151 | /// contains the specified byte array with the values between offset and | ||
| 152 | /// (offset + count - 1) replaced by the bytes read from the current source.</param> | ||
| 153 | /// <param name="offset">The zero-based byte offset in buffer at which to begin | ||
| 154 | /// storing the data read from the current stream.</param> | ||
| 155 | /// <param name="count">The maximum number of bytes to be read from the current stream.</param> | ||
| 156 | /// <returns>The total number of bytes read into the buffer. This can be less | ||
| 157 | /// than the number of bytes requested if that many bytes are not currently available, | ||
| 158 | /// or zero (0) if the end of the stream has been reached.</returns> | ||
| 159 | public override int Read(byte[] buffer, int offset, int count) | ||
| 160 | { | ||
| 161 | long saveSourcePosition = this.source.Position; | ||
| 162 | this.source.Position = this.position; | ||
| 163 | int read = this.source.Read(buffer, offset, count); | ||
| 164 | this.position = this.source.Position; | ||
| 165 | this.source.Position = saveSourcePosition; | ||
| 166 | return read; | ||
| 167 | } | ||
| 168 | |||
| 169 | /// <summary> | ||
| 170 | /// Writes to the source stream while maintaining a separate position | ||
| 171 | /// and not impacting the source stream's position. | ||
| 172 | /// </summary> | ||
| 173 | /// <param name="buffer">An array of bytes. This method copies count | ||
| 174 | /// bytes from buffer to the current stream.</param> | ||
| 175 | /// <param name="offset">The zero-based byte offset in buffer at which | ||
| 176 | /// to begin copying bytes to the current stream.</param> | ||
| 177 | /// <param name="count">The number of bytes to be written to the | ||
| 178 | /// current stream.</param> | ||
| 179 | public override void Write(byte[] buffer, int offset, int count) | ||
| 180 | { | ||
| 181 | long saveSourcePosition = this.source.Position; | ||
| 182 | this.source.Position = this.position; | ||
| 183 | this.source.Write(buffer, offset, count); | ||
| 184 | this.position = this.source.Position; | ||
| 185 | this.source.Position = saveSourcePosition; | ||
| 186 | } | ||
| 187 | |||
| 188 | /// <summary> | ||
| 189 | /// Changes the position of this stream without impacting the | ||
| 190 | /// source stream's position. | ||
| 191 | /// </summary> | ||
| 192 | /// <param name="offset">A byte offset relative to the origin parameter.</param> | ||
| 193 | /// <param name="origin">A value of type SeekOrigin indicating the reference | ||
| 194 | /// point used to obtain the new position.</param> | ||
| 195 | /// <returns>The new position within the current stream.</returns> | ||
| 196 | public override long Seek(long offset, SeekOrigin origin) | ||
| 197 | { | ||
| 198 | long originPosition = 0; | ||
| 199 | if (origin == SeekOrigin.Current) | ||
| 200 | { | ||
| 201 | originPosition = this.position; | ||
| 202 | } | ||
| 203 | else if (origin == SeekOrigin.End) | ||
| 204 | { | ||
| 205 | originPosition = this.Length; | ||
| 206 | } | ||
| 207 | |||
| 208 | this.position = originPosition + offset; | ||
| 209 | return this.position; | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | using System.Diagnostics.CodeAnalysis; | ||
| 8 | |||
| 9 | /// <summary> | ||
| 10 | /// This interface provides the methods necessary for the | ||
| 11 | /// <see cref="CompressionEngine"/> to open and close streams for archives | ||
| 12 | /// and files. The implementor of this interface can use any kind of logic | ||
| 13 | /// to determine what kind of streams to open and where. | ||
| 14 | /// </summary> | ||
| 15 | public interface IPackStreamContext | ||
| 16 | { | ||
| 17 | /// <summary> | ||
| 18 | /// Gets the name of the archive with a specified number. | ||
| 19 | /// </summary> | ||
| 20 | /// <param name="archiveNumber">The 0-based index of the archive | ||
| 21 | /// within the chain.</param> | ||
| 22 | /// <returns>The name of the requested archive. May be an empty string | ||
| 23 | /// for non-chained archives, but may never be null.</returns> | ||
| 24 | /// <remarks>The archive name is the name stored within the archive, used for | ||
| 25 | /// identification of the archive especially among archive chains. That | ||
| 26 | /// name is often, but not necessarily the same as the filename of the | ||
| 27 | /// archive package.</remarks> | ||
| 28 | string GetArchiveName(int archiveNumber); | ||
| 29 | |||
| 30 | /// <summary> | ||
| 31 | /// Opens a stream for writing an archive package. | ||
| 32 | /// </summary> | ||
| 33 | /// <param name="archiveNumber">The 0-based index of the archive within | ||
| 34 | /// the chain.</param> | ||
| 35 | /// <param name="archiveName">The name of the archive that was returned | ||
| 36 | /// by <see cref="GetArchiveName"/>.</param> | ||
| 37 | /// <param name="truncate">True if the stream should be truncated when | ||
| 38 | /// opened (if it already exists); false if an existing stream is being | ||
| 39 | /// re-opened for writing additional data.</param> | ||
| 40 | /// <param name="compressionEngine">Instance of the compression engine | ||
| 41 | /// doing the operations.</param> | ||
| 42 | /// <returns>A writable Stream where the compressed archive bytes will be | ||
| 43 | /// written, or null to cancel the archive creation.</returns> | ||
| 44 | /// <remarks> | ||
| 45 | /// If this method returns null, the archive engine will throw a | ||
| 46 | /// FileNotFoundException. | ||
| 47 | /// </remarks> | ||
| 48 | Stream OpenArchiveWriteStream( | ||
| 49 | int archiveNumber, | ||
| 50 | string archiveName, | ||
| 51 | bool truncate, | ||
| 52 | CompressionEngine compressionEngine); | ||
| 53 | |||
| 54 | /// <summary> | ||
| 55 | /// Closes a stream where an archive package was written. | ||
| 56 | /// </summary> | ||
| 57 | /// <param name="archiveNumber">The 0-based index of the archive within | ||
| 58 | /// the chain.</param> | ||
| 59 | /// <param name="archiveName">The name of the archive that was previously | ||
| 60 | /// returned by | ||
| 61 | /// <see cref="GetArchiveName"/>.</param> | ||
| 62 | /// <param name="stream">A stream that was previously returned by | ||
| 63 | /// <see cref="OpenArchiveWriteStream"/> and is now ready to be closed.</param> | ||
| 64 | /// <remarks> | ||
| 65 | /// If there is another archive package in the chain, then after this stream | ||
| 66 | /// is closed a new stream will be opened. | ||
| 67 | /// </remarks> | ||
| 68 | void CloseArchiveWriteStream(int archiveNumber, string archiveName, Stream stream); | ||
| 69 | |||
| 70 | /// <summary> | ||
| 71 | /// Opens a stream to read a file that is to be included in an archive. | ||
| 72 | /// </summary> | ||
| 73 | /// <param name="path">The path of the file within the archive. This is often, | ||
| 74 | /// but not necessarily, the same as the relative path of the file outside | ||
| 75 | /// the archive.</param> | ||
| 76 | /// <param name="attributes">Returned attributes of the opened file, to be | ||
| 77 | /// stored in the archive.</param> | ||
| 78 | /// <param name="lastWriteTime">Returned last-modified time of the opened file, | ||
| 79 | /// to be stored in the archive.</param> | ||
| 80 | /// <returns>A readable Stream where the file bytes will be read from before | ||
| 81 | /// they are compressed, or null to skip inclusion of the file and continue to | ||
| 82 | /// the next file.</returns> | ||
| 83 | [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")] | ||
| 84 | Stream OpenFileReadStream( | ||
| 85 | string path, | ||
| 86 | out FileAttributes attributes, | ||
| 87 | out DateTime lastWriteTime); | ||
| 88 | |||
| 89 | /// <summary> | ||
| 90 | /// Closes a stream that has been used to read a file. | ||
| 91 | /// </summary> | ||
| 92 | /// <param name="path">The path of the file within the archive; the same as | ||
| 93 | /// the path provided | ||
| 94 | /// when the stream was opened.</param> | ||
| 95 | /// <param name="stream">A stream that was previously returned by | ||
| 96 | /// <see cref="OpenFileReadStream"/> and is now ready to be closed.</param> | ||
| 97 | void CloseFileReadStream(string path, Stream stream); | ||
| 98 | |||
| 99 | /// <summary> | ||
| 100 | /// Gets extended parameter information specific to the compression | ||
| 101 | /// format being used. | ||
| 102 | /// </summary> | ||
| 103 | /// <param name="optionName">Name of the option being requested.</param> | ||
| 104 | /// <param name="parameters">Parameters for the option; for per-file options, | ||
| 105 | /// the first parameter is typically the internal file path.</param> | ||
| 106 | /// <returns>Option value, or null to use the default behavior.</returns> | ||
| 107 | /// <remarks> | ||
| 108 | /// This method provides a way to set uncommon options during packaging, or a | ||
| 109 | /// way to handle aspects of compression formats not supported by the base library. | ||
| 110 | /// <para>For example, this may be used by the zip compression library to | ||
| 111 | /// specify different compression methods/levels on a per-file basis.</para> | ||
| 112 | /// <para>The available option names, parameters, and expected return values | ||
| 113 | /// should be documented by each compression library.</para> | ||
| 114 | /// </remarks> | ||
| 115 | object GetOption(string optionName, object[] parameters); | ||
| 116 | } | ||
| 117 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | |||
| 8 | /// <summary> | ||
| 9 | /// This interface provides the methods necessary for the <see cref="CompressionEngine"/> to open | ||
| 10 | /// and close streams for archives and files. The implementor of this interface can use any | ||
| 11 | /// kind of logic to determine what kind of streams to open and where | ||
| 12 | /// </summary> | ||
| 13 | public interface IUnpackStreamContext | ||
| 14 | { | ||
| 15 | /// <summary> | ||
| 16 | /// Opens the archive stream for reading. | ||
| 17 | /// </summary> | ||
| 18 | /// <param name="archiveNumber">The zero-based index of the archive to open.</param> | ||
| 19 | /// <param name="archiveName">The name of the archive being opened.</param> | ||
| 20 | /// <param name="compressionEngine">Instance of the compression engine doing the operations.</param> | ||
| 21 | /// <returns>A stream from which archive bytes are read, or null to cancel extraction | ||
| 22 | /// of the archive.</returns> | ||
| 23 | /// <remarks> | ||
| 24 | /// When the first archive in a chain is opened, the name is not yet known, so the | ||
| 25 | /// provided value will be an empty string. When opening further archives, the | ||
| 26 | /// provided value is the next-archive name stored in the previous archive. This | ||
| 27 | /// name is often, but not necessarily, the same as the filename of the archive | ||
| 28 | /// package to be opened. | ||
| 29 | /// <para>If this method returns null, the archive engine will throw a | ||
| 30 | /// FileNotFoundException.</para> | ||
| 31 | /// </remarks> | ||
| 32 | Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine); | ||
| 33 | |||
| 34 | /// <summary> | ||
| 35 | /// Closes a stream where an archive package was read. | ||
| 36 | /// </summary> | ||
| 37 | /// <param name="archiveNumber">The archive number of the stream to close.</param> | ||
| 38 | /// <param name="archiveName">The name of the archive being closed.</param> | ||
| 39 | /// <param name="stream">The stream that was previously returned by | ||
| 40 | /// <see cref="OpenArchiveReadStream"/> and is now ready to be closed.</param> | ||
| 41 | void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream); | ||
| 42 | |||
| 43 | /// <summary> | ||
| 44 | /// Opens a stream for writing extracted file bytes. | ||
| 45 | /// </summary> | ||
| 46 | /// <param name="path">The path of the file within the archive. This is often, but | ||
| 47 | /// not necessarily, the same as the relative path of the file outside the archive.</param> | ||
| 48 | /// <param name="fileSize">The uncompressed size of the file to be extracted.</param> | ||
| 49 | /// <param name="lastWriteTime">The last write time of the file to be extracted.</param> | ||
| 50 | /// <returns>A stream where extracted file bytes are to be written, or null to skip | ||
| 51 | /// extraction of the file and continue to the next file.</returns> | ||
| 52 | /// <remarks> | ||
| 53 | /// The implementor may use the path, size and date information to dynamically | ||
| 54 | /// decide whether or not the file should be extracted. | ||
| 55 | /// </remarks> | ||
| 56 | Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime); | ||
| 57 | |||
| 58 | /// <summary> | ||
| 59 | /// Closes a stream where an extracted file was written. | ||
| 60 | /// </summary> | ||
| 61 | /// <param name="path">The path of the file within the archive.</param> | ||
| 62 | /// <param name="stream">The stream that was previously returned by <see cref="OpenFileWriteStream"/> | ||
| 63 | /// and is now ready to be closed.</param> | ||
| 64 | /// <param name="attributes">The attributes of the extracted file.</param> | ||
| 65 | /// <param name="lastWriteTime">The last write time of the file.</param> | ||
| 66 | /// <remarks> | ||
| 67 | /// The implementor may wish to apply the attributes and date to the newly-extracted file. | ||
| 68 | /// </remarks> | ||
| 69 | void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime); | ||
| 70 | } | ||
| 71 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.IO; | ||
| 7 | |||
| 8 | /// <summary> | ||
| 9 | /// Wraps a source stream and offsets all read/write/seek calls by a given value. | ||
| 10 | /// </summary> | ||
| 11 | /// <remarks> | ||
| 12 | /// This class is used to trick archive an packing or unpacking process | ||
| 13 | /// into reading or writing at an offset into a file, primarily for | ||
| 14 | /// self-extracting packages. | ||
| 15 | /// </remarks> | ||
| 16 | public class OffsetStream : Stream | ||
| 17 | { | ||
| 18 | private Stream source; | ||
| 19 | private long sourceOffset; | ||
| 20 | |||
| 21 | /// <summary> | ||
| 22 | /// Creates a new OffsetStream instance from a source stream | ||
| 23 | /// and using a specified offset. | ||
| 24 | /// </summary> | ||
| 25 | /// <param name="source">Underlying stream for which all calls will be offset.</param> | ||
| 26 | /// <param name="offset">Positive or negative number of bytes to offset.</param> | ||
| 27 | public OffsetStream(Stream source, long offset) | ||
| 28 | { | ||
| 29 | if (source == null) | ||
| 30 | { | ||
| 31 | throw new ArgumentNullException("source"); | ||
| 32 | } | ||
| 33 | |||
| 34 | this.source = source; | ||
| 35 | this.sourceOffset = offset; | ||
| 36 | |||
| 37 | this.source.Seek(this.sourceOffset, SeekOrigin.Current); | ||
| 38 | } | ||
| 39 | |||
| 40 | /// <summary> | ||
| 41 | /// Gets the underlying stream that this OffsetStream calls into. | ||
| 42 | /// </summary> | ||
| 43 | public Stream Source | ||
| 44 | { | ||
| 45 | get { return this.source; } | ||
| 46 | } | ||
| 47 | |||
| 48 | /// <summary> | ||
| 49 | /// Gets the number of bytes to offset all calls before | ||
| 50 | /// redirecting to the underlying stream. | ||
| 51 | /// </summary> | ||
| 52 | public long Offset | ||
| 53 | { | ||
| 54 | get { return this.sourceOffset; } | ||
| 55 | } | ||
| 56 | |||
| 57 | /// <summary> | ||
| 58 | /// Gets a value indicating whether the source stream supports reading. | ||
| 59 | /// </summary> | ||
| 60 | /// <value>true if the stream supports reading; otherwise, false.</value> | ||
| 61 | public override bool CanRead | ||
| 62 | { | ||
| 63 | get | ||
| 64 | { | ||
| 65 | return this.source.CanRead; | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// <summary> | ||
| 70 | /// Gets a value indicating whether the source stream supports writing. | ||
| 71 | /// </summary> | ||
| 72 | /// <value>true if the stream supports writing; otherwise, false.</value> | ||
| 73 | public override bool CanWrite | ||
| 74 | { | ||
| 75 | get | ||
| 76 | { | ||
| 77 | return this.source.CanWrite; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// <summary> | ||
| 82 | /// Gets a value indicating whether the source stream supports seeking. | ||
| 83 | /// </summary> | ||
| 84 | /// <value>true if the stream supports seeking; otherwise, false.</value> | ||
| 85 | public override bool CanSeek | ||
| 86 | { | ||
| 87 | get | ||
| 88 | { | ||
| 89 | return this.source.CanSeek; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | /// <summary> | ||
| 94 | /// Gets the effective length of the stream, which is equal to | ||
| 95 | /// the length of the source stream minus the offset. | ||
| 96 | /// </summary> | ||
| 97 | public override long Length | ||
| 98 | { | ||
| 99 | get { return this.source.Length - this.sourceOffset; } | ||
| 100 | } | ||
| 101 | |||
| 102 | /// <summary> | ||
| 103 | /// Gets or sets the effective position of the stream, which | ||
| 104 | /// is equal to the position of the source stream minus the offset. | ||
| 105 | /// </summary> | ||
| 106 | public override long Position | ||
| 107 | { | ||
| 108 | get { return this.source.Position - this.sourceOffset; } | ||
| 109 | set { this.source.Position = value + this.sourceOffset; } | ||
| 110 | } | ||
| 111 | |||
| 112 | /// <summary> | ||
| 113 | /// Reads a sequence of bytes from the source stream and advances | ||
| 114 | /// the position within the stream by the number of bytes read. | ||
| 115 | /// </summary> | ||
| 116 | /// <param name="buffer">An array of bytes. When this method returns, the buffer | ||
| 117 | /// contains the specified byte array with the values between offset and | ||
| 118 | /// (offset + count - 1) replaced by the bytes read from the current source.</param> | ||
| 119 | /// <param name="offset">The zero-based byte offset in buffer at which to begin | ||
| 120 | /// storing the data read from the current stream.</param> | ||
| 121 | /// <param name="count">The maximum number of bytes to be read from the current stream.</param> | ||
| 122 | /// <returns>The total number of bytes read into the buffer. This can be less | ||
| 123 | /// than the number of bytes requested if that many bytes are not currently available, | ||
| 124 | /// or zero (0) if the end of the stream has been reached.</returns> | ||
| 125 | public override int Read(byte[] buffer, int offset, int count) | ||
| 126 | { | ||
| 127 | return this.source.Read(buffer, offset, count); | ||
| 128 | } | ||
| 129 | |||
| 130 | /// <summary> | ||
| 131 | /// Writes a sequence of bytes to the source stream and advances the | ||
| 132 | /// current position within this stream by the number of bytes written. | ||
| 133 | /// </summary> | ||
| 134 | /// <param name="buffer">An array of bytes. This method copies count | ||
| 135 | /// bytes from buffer to the current stream.</param> | ||
| 136 | /// <param name="offset">The zero-based byte offset in buffer at which | ||
| 137 | /// to begin copying bytes to the current stream.</param> | ||
| 138 | /// <param name="count">The number of bytes to be written to the | ||
| 139 | /// current stream.</param> | ||
| 140 | public override void Write(byte[] buffer, int offset, int count) | ||
| 141 | { | ||
| 142 | this.source.Write(buffer, offset, count); | ||
| 143 | } | ||
| 144 | |||
| 145 | /// <summary> | ||
| 146 | /// Reads a byte from the stream and advances the position within the | ||
| 147 | /// source stream by one byte, or returns -1 if at the end of the stream. | ||
| 148 | /// </summary> | ||
| 149 | /// <returns>The unsigned byte cast to an Int32, or -1 if at the | ||
| 150 | /// end of the stream.</returns> | ||
| 151 | public override int ReadByte() | ||
| 152 | { | ||
| 153 | return this.source.ReadByte(); | ||
| 154 | } | ||
| 155 | |||
| 156 | /// <summary> | ||
| 157 | /// Writes a byte to the current position in the source stream and | ||
| 158 | /// advances the position within the stream by one byte. | ||
| 159 | /// </summary> | ||
| 160 | /// <param name="value">The byte to write to the stream.</param> | ||
| 161 | public override void WriteByte(byte value) | ||
| 162 | { | ||
| 163 | this.source.WriteByte(value); | ||
| 164 | } | ||
| 165 | |||
| 166 | /// <summary> | ||
| 167 | /// Flushes the source stream. | ||
| 168 | /// </summary> | ||
| 169 | public override void Flush() | ||
| 170 | { | ||
| 171 | this.source.Flush(); | ||
| 172 | } | ||
| 173 | |||
| 174 | /// <summary> | ||
| 175 | /// Sets the position within the current stream, which is | ||
| 176 | /// equal to the position within the source stream minus the offset. | ||
| 177 | /// </summary> | ||
| 178 | /// <param name="offset">A byte offset relative to the origin parameter.</param> | ||
| 179 | /// <param name="origin">A value of type SeekOrigin indicating | ||
| 180 | /// the reference point used to obtain the new position.</param> | ||
| 181 | /// <returns>The new position within the current stream.</returns> | ||
| 182 | public override long Seek(long offset, SeekOrigin origin) | ||
| 183 | { | ||
| 184 | return this.source.Seek(offset + (origin == SeekOrigin.Begin ? this.sourceOffset : 0), origin) - this.sourceOffset; | ||
| 185 | } | ||
| 186 | |||
| 187 | /// <summary> | ||
| 188 | /// Sets the effective length of the stream, which is equal to | ||
| 189 | /// the length of the source stream minus the offset. | ||
| 190 | /// </summary> | ||
| 191 | /// <param name="value">The desired length of the | ||
| 192 | /// current stream in bytes.</param> | ||
| 193 | public override void SetLength(long value) | ||
| 194 | { | ||
| 195 | this.source.SetLength(value + this.sourceOffset); | ||
| 196 | } | ||
| 197 | |||
| 198 | /// <summary> | ||
| 199 | /// Closes the underlying stream. | ||
| 200 | /// </summary> | ||
| 201 | public override void Close() | ||
| 202 | { | ||
| 203 | this.source.Close(); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
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 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Dtf.Compression | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Security; | ||
| 7 | using System.Runtime.InteropServices; | ||
| 8 | |||
| 9 | [SuppressUnmanagedCodeSecurity] | ||
| 10 | internal static class SafeNativeMethods | ||
| 11 | { | ||
| 12 | [DllImport("kernel32.dll", SetLastError = true)] | ||
| 13 | [return: MarshalAs(UnmanagedType.Bool)] | ||
| 14 | internal static extern bool DosDateTimeToFileTime( | ||
| 15 | short wFatDate, short wFatTime, out long fileTime); | ||
| 16 | |||
| 17 | [DllImport("kernel32.dll", SetLastError = true)] | ||
| 18 | [return: MarshalAs(UnmanagedType.Bool)] | ||
| 19 | internal static extern bool FileTimeToDosDateTime( | ||
| 20 | ref long fileTime, out short wFatDate, out short wFatTime); | ||
| 21 | } | ||
| 22 | } | ||
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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <!-- 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. --> | ||
| 3 | |||
| 4 | <Project Sdk="Microsoft.NET.Sdk"> | ||
| 5 | <PropertyGroup> | ||
| 6 | <RootNamespace>WixToolset.Dtf.Compression</RootNamespace> | ||
| 7 | <AssemblyName>WixToolset.Dtf.Compression</AssemblyName> | ||
| 8 | <TargetFrameworks>netstandard2.0;net20</TargetFrameworks> | ||
| 9 | <Description>Abstract base libraries for archive packing and unpacking</Description> | ||
| 10 | <CreateDocumentationFile>true</CreateDocumentationFile> | ||
| 11 | </PropertyGroup> | ||
| 12 | |||
| 13 | <ItemGroup> | ||
| 14 | <None Include="Compression.cd" /> | ||
| 15 | </ItemGroup> | ||
| 16 | |||
| 17 | <ItemGroup> | ||
| 18 | <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> | ||
| 19 | <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" /> | ||
| 20 | </ItemGroup> | ||
| 21 | </Project> | ||
