aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.Compression
diff options
context:
space:
mode:
Diffstat (limited to 'src/dtf/WixToolset.Dtf.Compression')
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs57
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs430
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs664
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs781
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs307
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs69
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs90
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CargoStream.cs192
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/Compression.cd175
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs371
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs31
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs212
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs117
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs71
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs206
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs22
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj21
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
3namespace 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
3namespace 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
3namespace 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&lt;string&gt;,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
3namespace 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 /// &quot;*.txt&quot;.</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
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using 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
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using 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
3namespace 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
3namespace 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
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.IO;
7using System.Collections.Generic;
8using 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
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using 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
3namespace 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
3namespace 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
3namespace 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
3namespace 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
3namespace 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>