aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.Compression
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs5
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs164
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs154
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs141
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs82
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs653
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs566
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs337
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resourcesbin0 -> 1465 bytes
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt35
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs76
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs407
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj26
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs5
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs157
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs250
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj21
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs80
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs478
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs60
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs104
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs697
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs82
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs489
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs336
-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
42 files changed, 9221 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs
new file mode 100644
index 00000000..aea5bf2e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs
@@ -0,0 +1,5 @@
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
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "WixToolset.Dtf.Compression.Cab")]
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs
new file mode 100644
index 00000000..ab135fd8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs
@@ -0,0 +1,164 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8
9 /// <summary>
10 /// Engine capable of packing and unpacking archives in the cabinet format.
11 /// </summary>
12 public class CabEngine : CompressionEngine
13 {
14 private CabPacker packer;
15 private CabUnpacker unpacker;
16
17 /// <summary>
18 /// Creates a new instance of the cabinet engine.
19 /// </summary>
20 public CabEngine()
21 : base()
22 {
23 }
24
25 /// <summary>
26 /// Disposes of resources allocated by the cabinet engine.
27 /// </summary>
28 /// <param name="disposing">If true, the method has been called directly
29 /// or indirectly by a user's code, so managed and unmanaged resources
30 /// will be disposed. If false, the method has been called by the runtime
31 /// from inside the finalizer, and only unmanaged resources will be
32 /// disposed.</param>
33 protected override void Dispose(bool disposing)
34 {
35 if (disposing)
36 {
37 if (packer != null)
38 {
39 packer.Dispose();
40 packer = null;
41 }
42 if (unpacker != null)
43 {
44 unpacker.Dispose();
45 unpacker = null;
46 }
47 }
48
49 base.Dispose(disposing);
50 }
51
52 private CabPacker Packer
53 {
54 get
55 {
56 if (this.packer == null)
57 {
58 this.packer = new CabPacker(this);
59 }
60
61 return this.packer;
62 }
63 }
64
65 private CabUnpacker Unpacker
66 {
67 get
68 {
69 if (this.unpacker == null)
70 {
71 this.unpacker = new CabUnpacker(this);
72 }
73
74 return this.unpacker;
75 }
76 }
77
78 /// <summary>
79 /// Creates a cabinet or chain of cabinets.
80 /// </summary>
81 /// <param name="streamContext">A context interface to handle opening
82 /// and closing of cabinet and file streams.</param>
83 /// <param name="files">The paths of the files in the archive (not
84 /// external file paths).</param>
85 /// <param name="maxArchiveSize">The maximum number of bytes for one
86 /// cabinet before the contents are chained to the next cabinet, or zero
87 /// for unlimited cabinet size.</param>
88 /// <exception cref="ArchiveException">The cabinet could not be
89 /// created.</exception>
90 /// <remarks>
91 /// The stream context implementation may provide a mapping from the
92 /// file paths within the cabinet to the external file paths.
93 /// <para>Smaller folder sizes can make it more efficient to extract
94 /// individual files out of large cabinet packages.</para>
95 /// </remarks>
96 public override void Pack(
97 IPackStreamContext streamContext,
98 IEnumerable<string> files,
99 long maxArchiveSize)
100 {
101 this.Packer.CompressionLevel = this.CompressionLevel;
102 this.Packer.UseTempFiles = this.UseTempFiles;
103 this.Packer.Pack(streamContext, files, maxArchiveSize);
104 }
105
106 /// <summary>
107 /// Checks whether a Stream begins with a header that indicates
108 /// it is a valid cabinet file.
109 /// </summary>
110 /// <param name="stream">Stream for reading the cabinet file.</param>
111 /// <returns>True if the stream is a valid cabinet file
112 /// (with no offset); false otherwise.</returns>
113 public override bool IsArchive(Stream stream)
114 {
115 return this.Unpacker.IsArchive(stream);
116 }
117
118 /// <summary>
119 /// Gets information about files in a cabinet or cabinet chain.
120 /// </summary>
121 /// <param name="streamContext">A context interface to handle opening
122 /// and closing of cabinet and file streams.</param>
123 /// <param name="fileFilter">A predicate that can determine
124 /// which files to process, optional.</param>
125 /// <returns>Information about files in the cabinet stream.</returns>
126 /// <exception cref="ArchiveException">The cabinet provided
127 /// by the stream context is not valid.</exception>
128 /// <remarks>
129 /// The <paramref name="fileFilter"/> predicate takes an internal file
130 /// path and returns true to include the file or false to exclude it.
131 /// </remarks>
132 public override IList<ArchiveFileInfo> GetFileInfo(
133 IUnpackStreamContext streamContext,
134 Predicate<string> fileFilter)
135 {
136 return this.Unpacker.GetFileInfo(streamContext, fileFilter);
137 }
138
139 /// <summary>
140 /// Extracts files from a cabinet or cabinet chain.
141 /// </summary>
142 /// <param name="streamContext">A context interface to handle opening
143 /// and closing of cabinet and file streams.</param>
144 /// <param name="fileFilter">An optional predicate that can determine
145 /// which files to process.</param>
146 /// <exception cref="ArchiveException">The cabinet provided
147 /// by the stream context is not valid.</exception>
148 /// <remarks>
149 /// The <paramref name="fileFilter"/> predicate takes an internal file
150 /// path and returns true to include the file or false to exclude it.
151 /// </remarks>
152 public override void Unpack(
153 IUnpackStreamContext streamContext,
154 Predicate<string> fileFilter)
155 {
156 this.Unpacker.Unpack(streamContext, fileFilter);
157 }
158
159 internal void ReportProgress(ArchiveProgressEventArgs e)
160 {
161 base.OnProgress(e);
162 }
163 }
164}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs
new file mode 100644
index 00000000..e03f9f3a
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs
@@ -0,0 +1,154 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.Resources;
8 using System.Globalization;
9 using System.Runtime.Serialization;
10
11 /// <summary>
12 /// Exception class for cabinet operations.
13 /// </summary>
14 [Serializable]
15 public class CabException : ArchiveException
16 {
17 private static ResourceManager errorResources;
18 private int error;
19 private int errorCode;
20
21 /// <summary>
22 /// Creates a new CabException with a specified error message and a reference to the
23 /// inner exception that is the cause of this exception.
24 /// </summary>
25 /// <param name="message">The message that describes the error.</param>
26 /// <param name="innerException">The exception that is the cause of the current exception. If the
27 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
28 /// is raised in a catch block that handles the inner exception.</param>
29 public CabException(string message, Exception innerException)
30 : this(0, 0, message, innerException) { }
31
32 /// <summary>
33 /// Creates a new CabException with a specified error message.
34 /// </summary>
35 /// <param name="message">The message that describes the error.</param>
36 public CabException(string message)
37 : this(0, 0, message, null) { }
38
39 /// <summary>
40 /// Creates a new CabException.
41 /// </summary>
42 public CabException()
43 : this(0, 0, null, null) { }
44
45 internal CabException(int error, int errorCode, string message, Exception innerException)
46 : base(message, innerException)
47 {
48 this.error = error;
49 this.errorCode = errorCode;
50 }
51
52 internal CabException(int error, int errorCode, string message)
53 : this(error, errorCode, message, null) { }
54
55 /// <summary>
56 /// Initializes a new instance of the CabException class with serialized data.
57 /// </summary>
58 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
59 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
60 protected CabException(SerializationInfo info, StreamingContext context) : base(info, context)
61 {
62 if (info == null)
63 {
64 throw new ArgumentNullException("info");
65 }
66
67 this.error = info.GetInt32("cabError");
68 this.errorCode = info.GetInt32("cabErrorCode");
69 }
70
71 /// <summary>
72 /// Gets the FCI or FDI cabinet engine error number.
73 /// </summary>
74 /// <value>A cabinet engine error number, or 0 if the exception was
75 /// not related to a cabinet engine error number.</value>
76 public int Error
77 {
78 get
79 {
80 return this.error;
81 }
82 }
83
84 /// <summary>
85 /// Gets the Win32 error code.
86 /// </summary>
87 /// <value>A Win32 error code, or 0 if the exception was
88 /// not related to a Win32 error.</value>
89 public int ErrorCode
90 {
91 get
92 {
93 return this.errorCode;
94 }
95 }
96
97 internal static ResourceManager ErrorResources
98 {
99 get
100 {
101 if (errorResources == null)
102 {
103 errorResources = new ResourceManager(
104 typeof(CabException).Namespace + ".Errors",
105 typeof(CabException).Assembly);
106 }
107 return errorResources;
108 }
109 }
110
111 /// <summary>
112 /// Sets the SerializationInfo with information about the exception.
113 /// </summary>
114 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
115 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
116 public override void GetObjectData(SerializationInfo info, StreamingContext context)
117 {
118 if (info == null)
119 {
120 throw new ArgumentNullException("info");
121 }
122
123 info.AddValue("cabError", this.error);
124 info.AddValue("cabErrorCode", this.errorCode);
125 base.GetObjectData(info, context);
126 }
127
128 internal static string GetErrorMessage(int error, int errorCode, bool extracting)
129 {
130 const int FCI_ERROR_RESOURCE_OFFSET = 1000;
131 const int FDI_ERROR_RESOURCE_OFFSET = 2000;
132 int resourceOffset = (extracting ? FDI_ERROR_RESOURCE_OFFSET : FCI_ERROR_RESOURCE_OFFSET);
133
134 string msg = CabException.ErrorResources.GetString(
135 (resourceOffset + error).ToString(CultureInfo.InvariantCulture.NumberFormat),
136 CultureInfo.CurrentCulture);
137
138 if (msg == null)
139 {
140 msg = CabException.ErrorResources.GetString(
141 resourceOffset.ToString(CultureInfo.InvariantCulture.NumberFormat),
142 CultureInfo.CurrentCulture);
143 }
144
145 if (errorCode != 0)
146 {
147 const string GENERIC_ERROR_RESOURCE = "1";
148 string msg2 = CabException.ErrorResources.GetString(GENERIC_ERROR_RESOURCE, CultureInfo.CurrentCulture);
149 msg = String.Format(CultureInfo.InvariantCulture, "{0} " + msg2, msg, errorCode);
150 }
151 return msg;
152 }
153 }
154}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs
new file mode 100644
index 00000000..ad4a813c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs
@@ -0,0 +1,141 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a compressed file within a cabinet package; provides operations for getting
11 /// the file properties and extracting the file.
12 /// </summary>
13 [Serializable]
14 public class CabFileInfo : ArchiveFileInfo
15 {
16 private int cabFolder;
17
18 /// <summary>
19 /// Creates a new CabinetFileInfo object representing a file within a cabinet in a specified path.
20 /// </summary>
21 /// <param name="cabinetInfo">An object representing the cabinet containing the file.</param>
22 /// <param name="filePath">The path to the file within the cabinet. Usually, this is a simple file
23 /// name, but if the cabinet contains a directory structure this may include the directory.</param>
24 public CabFileInfo(CabInfo cabinetInfo, string filePath)
25 : base(cabinetInfo, filePath)
26 {
27 if (cabinetInfo == null)
28 {
29 throw new ArgumentNullException("cabinetInfo");
30 }
31
32 this.cabFolder = -1;
33 }
34
35 /// <summary>
36 /// Creates a new CabinetFileInfo object with all parameters specified,
37 /// used internally when reading the metadata out of a cab.
38 /// </summary>
39 /// <param name="filePath">The internal path and name of the file in the cab.</param>
40 /// <param name="cabFolder">The folder number containing the file.</param>
41 /// <param name="cabNumber">The cabinet number where the file starts.</param>
42 /// <param name="attributes">The stored attributes of the file.</param>
43 /// <param name="lastWriteTime">The stored last write time of the file.</param>
44 /// <param name="length">The uncompressed size of the file.</param>
45 internal CabFileInfo(
46 string filePath,
47 int cabFolder,
48 int cabNumber,
49 FileAttributes attributes,
50 DateTime lastWriteTime,
51 long length)
52 : base(filePath, cabNumber, attributes, lastWriteTime, length)
53 {
54 this.cabFolder = cabFolder;
55 }
56
57 /// <summary>
58 /// Initializes a new instance of the CabinetFileInfo class with serialized data.
59 /// </summary>
60 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
61 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
62 protected CabFileInfo(SerializationInfo info, StreamingContext context)
63 : base(info, context)
64 {
65 this.cabFolder = info.GetInt32("cabFolder");
66 }
67
68 /// <summary>
69 /// Sets the SerializationInfo with information about the archive.
70 /// </summary>
71 /// <param name="info">The SerializationInfo that holds the serialized object data.</param>
72 /// <param name="context">The StreamingContext that contains contextual information
73 /// about the source or destination.</param>
74 public override void GetObjectData(SerializationInfo info, StreamingContext context)
75 {
76 base.GetObjectData(info, context);
77 info.AddValue("cabFolder", this.cabFolder);
78 }
79
80 /// <summary>
81 /// Gets or sets the cabinet that contains this file.
82 /// </summary>
83 /// <value>
84 /// The CabinetInfo instance that retrieved this file information -- this
85 /// may be null if the CabinetFileInfo object was returned directly from a
86 /// stream.
87 /// </value>
88 public CabInfo Cabinet
89 {
90 get
91 {
92 return (CabInfo) this.Archive;
93 }
94 }
95
96 /// <summary>
97 /// Gets the full path of the cabinet that contains this file.
98 /// </summary>
99 /// <value>The full path of the cabinet that contains this file.</value>
100 public string CabinetName
101 {
102 get
103 {
104 return this.ArchiveName;
105 }
106 }
107
108 /// <summary>
109 /// Gets the number of the folder containing this file.
110 /// </summary>
111 /// <value>The number of the cabinet folder containing this file.</value>
112 /// <remarks>A single folder or the first folder of a cabinet
113 /// (or chain of cabinets) is numbered 0.</remarks>
114 public int CabinetFolderNumber
115 {
116 get
117 {
118 if (this.cabFolder < 0)
119 {
120 this.Refresh();
121 }
122 return this.cabFolder;
123 }
124 }
125
126 /// <summary>
127 /// Refreshes the information in this object with new data retrieved
128 /// from an archive.
129 /// </summary>
130 /// <param name="newFileInfo">Fresh instance for the same file just
131 /// read from the archive.</param>
132 /// <remarks>
133 /// This implementation refreshes the <see cref="CabinetFolderNumber"/>.
134 /// </remarks>
135 protected override void Refresh(ArchiveFileInfo newFileInfo)
136 {
137 base.Refresh(newFileInfo);
138 this.cabFolder = ((CabFileInfo) newFileInfo).cabFolder;
139 }
140 }
141}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs
new file mode 100644
index 00000000..969dcbef
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs
@@ -0,0 +1,82 @@
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.Cab
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a cabinet file on disk; provides access to
11 /// file-based operations on the cabinet file.
12 /// </summary>
13 /// <remarks>
14 /// Generally, the methods on this class are much easier to use than the
15 /// stream-based interfaces provided by the <see cref="CabEngine"/> class.
16 /// </remarks>
17 [Serializable]
18 public class CabInfo : ArchiveInfo
19 {
20 /// <summary>
21 /// Creates a new CabinetInfo object representing a cabinet file in a specified path.
22 /// </summary>
23 /// <param name="path">The path to the cabinet file. When creating a cabinet file, this file does not
24 /// necessarily exist yet.</param>
25 public CabInfo(string path)
26 : base(path)
27 {
28 }
29
30 /// <summary>
31 /// Initializes a new instance of the CabinetInfo class with serialized data.
32 /// </summary>
33 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
34 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
35 protected CabInfo(SerializationInfo info, StreamingContext context)
36 : base(info, context)
37 {
38 }
39
40 /// <summary>
41 /// Creates a compression engine that does the low-level work for
42 /// this object.
43 /// </summary>
44 /// <returns>A new <see cref="CabEngine"/> instance.</returns>
45 /// <remarks>
46 /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d
47 /// immediately after use.
48 /// </remarks>
49 protected override CompressionEngine CreateCompressionEngine()
50 {
51 return new CabEngine();
52 }
53
54 /// <summary>
55 /// Gets information about the files contained in the archive.
56 /// </summary>
57 /// <returns>A list of <see cref="CabFileInfo"/> objects, each
58 /// containing information about a file in the archive.</returns>
59 public new IList<CabFileInfo> GetFiles()
60 {
61 IList<ArchiveFileInfo> files = base.GetFiles();
62 List<CabFileInfo> cabFiles = new List<CabFileInfo>(files.Count);
63 foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile);
64 return cabFiles.AsReadOnly();
65 }
66
67 /// <summary>
68 /// Gets information about the certain files contained in the archive file.
69 /// </summary>
70 /// <param name="searchPattern">The search string, such as
71 /// &quot;*.txt&quot;.</param>
72 /// <returns>A list of <see cref="CabFileInfo"/> objects, each containing
73 /// information about a file in the archive.</returns>
74 public new IList<CabFileInfo> GetFiles(string searchPattern)
75 {
76 IList<ArchiveFileInfo> files = base.GetFiles(searchPattern);
77 List<CabFileInfo> cabFiles = new List<CabFileInfo>(files.Count);
78 foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile);
79 return cabFiles.AsReadOnly();
80 }
81 }
82}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs
new file mode 100644
index 00000000..ec6e3bda
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs
@@ -0,0 +1,653 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal class CabPacker : CabWorker
14 {
15 private const string TempStreamName = "%%TEMP%%";
16
17 private NativeMethods.FCI.Handle fciHandle;
18
19 // These delegates need to be saved as member variables
20 // so that they don't get GC'd.
21 private NativeMethods.FCI.PFNALLOC fciAllocMemHandler;
22 private NativeMethods.FCI.PFNFREE fciFreeMemHandler;
23 private NativeMethods.FCI.PFNOPEN fciOpenStreamHandler;
24 private NativeMethods.FCI.PFNREAD fciReadStreamHandler;
25 private NativeMethods.FCI.PFNWRITE fciWriteStreamHandler;
26 private NativeMethods.FCI.PFNCLOSE fciCloseStreamHandler;
27 private NativeMethods.FCI.PFNSEEK fciSeekStreamHandler;
28 private NativeMethods.FCI.PFNFILEPLACED fciFilePlacedHandler;
29 private NativeMethods.FCI.PFNDELETE fciDeleteFileHandler;
30 private NativeMethods.FCI.PFNGETTEMPFILE fciGetTempFileHandler;
31
32 private NativeMethods.FCI.PFNGETNEXTCABINET fciGetNextCabinet;
33 private NativeMethods.FCI.PFNSTATUS fciCreateStatus;
34 private NativeMethods.FCI.PFNGETOPENINFO fciGetOpenInfo;
35
36 private IPackStreamContext context;
37
38 private FileAttributes fileAttributes;
39 private DateTime fileLastWriteTime;
40
41 private int maxCabBytes;
42
43 private long totalFolderBytesProcessedInCurrentCab;
44
45 private CompressionLevel compressionLevel;
46 private bool dontUseTempFiles;
47 private IList<Stream> tempStreams;
48
49 public CabPacker(CabEngine cabEngine)
50 : base(cabEngine)
51 {
52 this.fciAllocMemHandler = this.CabAllocMem;
53 this.fciFreeMemHandler = this.CabFreeMem;
54 this.fciOpenStreamHandler = this.CabOpenStreamEx;
55 this.fciReadStreamHandler = this.CabReadStreamEx;
56 this.fciWriteStreamHandler = this.CabWriteStreamEx;
57 this.fciCloseStreamHandler = this.CabCloseStreamEx;
58 this.fciSeekStreamHandler = this.CabSeekStreamEx;
59 this.fciFilePlacedHandler = this.CabFilePlaced;
60 this.fciDeleteFileHandler = this.CabDeleteFile;
61 this.fciGetTempFileHandler = this.CabGetTempFile;
62 this.fciGetNextCabinet = this.CabGetNextCabinet;
63 this.fciCreateStatus = this.CabCreateStatus;
64 this.fciGetOpenInfo = this.CabGetOpenInfo;
65 this.tempStreams = new List<Stream>();
66 this.compressionLevel = CompressionLevel.Normal;
67 }
68
69 public bool UseTempFiles
70 {
71 get
72 {
73 return !this.dontUseTempFiles;
74 }
75
76 set
77 {
78 this.dontUseTempFiles = !value;
79 }
80 }
81
82 public CompressionLevel CompressionLevel
83 {
84 get
85 {
86 return this.compressionLevel;
87 }
88
89 set
90 {
91 this.compressionLevel = value;
92 }
93 }
94
95 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
96 private void CreateFci(long maxArchiveSize)
97 {
98 NativeMethods.FCI.CCAB ccab = new NativeMethods.FCI.CCAB();
99 if (maxArchiveSize > 0 && maxArchiveSize < ccab.cb)
100 {
101 ccab.cb = Math.Max(
102 NativeMethods.FCI.MIN_DISK, (int) maxArchiveSize);
103 }
104
105 object maxFolderSizeOption = this.context.GetOption(
106 "maxFolderSize", null);
107 if (maxFolderSizeOption != null)
108 {
109 long maxFolderSize = Convert.ToInt64(
110 maxFolderSizeOption, CultureInfo.InvariantCulture);
111 if (maxFolderSize > 0 && maxFolderSize < ccab.cbFolderThresh)
112 {
113 ccab.cbFolderThresh = (int) maxFolderSize;
114 }
115 }
116
117 this.maxCabBytes = ccab.cb;
118 ccab.szCab = this.context.GetArchiveName(0);
119 if (ccab.szCab == null)
120 {
121 throw new FileNotFoundException(
122 "Cabinet name not provided by stream context.");
123 }
124 ccab.setID = (short) new Random().Next(
125 Int16.MinValue, Int16.MaxValue + 1);
126 this.CabNumbers[ccab.szCab] = 0;
127 this.currentArchiveName = ccab.szCab;
128 this.totalArchives = 1;
129 this.CabStream = null;
130
131 this.Erf.Clear();
132 this.fciHandle = NativeMethods.FCI.Create(
133 this.ErfHandle.AddrOfPinnedObject(),
134 this.fciFilePlacedHandler,
135 this.fciAllocMemHandler,
136 this.fciFreeMemHandler,
137 this.fciOpenStreamHandler,
138 this.fciReadStreamHandler,
139 this.fciWriteStreamHandler,
140 this.fciCloseStreamHandler,
141 this.fciSeekStreamHandler,
142 this.fciDeleteFileHandler,
143 this.fciGetTempFileHandler,
144 ccab,
145 IntPtr.Zero);
146 this.CheckError(false);
147 }
148
149 public void Pack(
150 IPackStreamContext streamContext,
151 IEnumerable<string> files,
152 long maxArchiveSize)
153 {
154 if (streamContext == null)
155 {
156 throw new ArgumentNullException("streamContext");
157 }
158
159 if (files == null)
160 {
161 throw new ArgumentNullException("files");
162 }
163
164 lock (this)
165 {
166 try
167 {
168 this.context = streamContext;
169
170 this.ResetProgressData();
171
172 this.CreateFci(maxArchiveSize);
173
174 foreach (string file in files)
175 {
176 FileAttributes attributes;
177 DateTime lastWriteTime;
178 Stream fileStream = this.context.OpenFileReadStream(
179 file,
180 out attributes,
181 out lastWriteTime);
182 if (fileStream != null)
183 {
184 this.totalFileBytes += fileStream.Length;
185 this.totalFiles++;
186 this.context.CloseFileReadStream(file, fileStream);
187 }
188 }
189
190 long uncompressedBytesInFolder = 0;
191 this.currentFileNumber = -1;
192
193 foreach (string file in files)
194 {
195 FileAttributes attributes;
196 DateTime lastWriteTime;
197 Stream fileStream = this.context.OpenFileReadStream(
198 file, out attributes, out lastWriteTime);
199 if (fileStream == null)
200 {
201 continue;
202 }
203
204 if (fileStream.Length >= (long) NativeMethods.FCI.MAX_FOLDER)
205 {
206 throw new NotSupportedException(String.Format(
207 CultureInfo.InvariantCulture,
208 "File {0} exceeds maximum file size " +
209 "for cabinet format.",
210 file));
211 }
212
213 if (uncompressedBytesInFolder > 0)
214 {
215 // Automatically create a new folder if this file
216 // won't fit in the current folder.
217 bool nextFolder = uncompressedBytesInFolder
218 + fileStream.Length >= (long) NativeMethods.FCI.MAX_FOLDER;
219
220 // Otherwise ask the client if it wants to
221 // move to the next folder.
222 if (!nextFolder)
223 {
224 object nextFolderOption = streamContext.GetOption(
225 "nextFolder",
226 new object[] { file, this.currentFolderNumber });
227 nextFolder = Convert.ToBoolean(
228 nextFolderOption, CultureInfo.InvariantCulture);
229 }
230
231 if (nextFolder)
232 {
233 this.FlushFolder();
234 uncompressedBytesInFolder = 0;
235 }
236 }
237
238 if (this.currentFolderTotalBytes > 0)
239 {
240 this.currentFolderTotalBytes = 0;
241 this.currentFolderNumber++;
242 uncompressedBytesInFolder = 0;
243 }
244
245 this.currentFileName = file;
246 this.currentFileNumber++;
247
248 this.currentFileTotalBytes = fileStream.Length;
249 this.currentFileBytesProcessed = 0;
250 this.OnProgress(ArchiveProgressType.StartFile);
251
252 uncompressedBytesInFolder += fileStream.Length;
253
254 this.AddFile(
255 file,
256 fileStream,
257 attributes,
258 lastWriteTime,
259 false,
260 this.CompressionLevel);
261 }
262
263 this.FlushFolder();
264 this.FlushCabinet();
265 }
266 finally
267 {
268 if (this.CabStream != null)
269 {
270 this.context.CloseArchiveWriteStream(
271 this.currentArchiveNumber,
272 this.currentArchiveName,
273 this.CabStream);
274 this.CabStream = null;
275 }
276
277 if (this.FileStream != null)
278 {
279 this.context.CloseFileReadStream(
280 this.currentFileName, this.FileStream);
281 this.FileStream = null;
282 }
283 this.context = null;
284
285 if (this.fciHandle != null)
286 {
287 this.fciHandle.Dispose();
288 this.fciHandle = null;
289 }
290 }
291 }
292 }
293
294 internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
295 {
296 if (this.CabNumbers.ContainsKey(path))
297 {
298 Stream stream = this.CabStream;
299 if (stream == null)
300 {
301 short cabNumber = this.CabNumbers[path];
302
303 this.currentFolderTotalBytes = 0;
304
305 stream = this.context.OpenArchiveWriteStream(cabNumber, path, true, this.CabEngine);
306 if (stream == null)
307 {
308 throw new FileNotFoundException(
309 String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber));
310 }
311 this.currentArchiveName = path;
312
313 this.currentArchiveTotalBytes = Math.Min(
314 this.totalFolderBytesProcessedInCurrentCab, this.maxCabBytes);
315 this.currentArchiveBytesProcessed = 0;
316
317 this.OnProgress(ArchiveProgressType.StartArchive);
318 this.CabStream = stream;
319 }
320 path = CabWorker.CabStreamName;
321 }
322 else if (path == CabPacker.TempStreamName)
323 {
324 // Opening memory stream for a temp file.
325 Stream stream = new MemoryStream();
326 this.tempStreams.Add(stream);
327 int streamHandle = this.StreamHandles.AllocHandle(stream);
328 err = 0;
329 return streamHandle;
330 }
331 else if (path != CabWorker.CabStreamName)
332 {
333 // Opening a file on disk for a temp file.
334 path = Path.Combine(Path.GetTempPath(), path);
335 Stream stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite);
336 this.tempStreams.Add(stream);
337 stream = new DuplicateStream(stream);
338 int streamHandle = this.StreamHandles.AllocHandle(stream);
339 err = 0;
340 return streamHandle;
341 }
342 return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv);
343 }
344
345 internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
346 {
347 int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv);
348 if (count > 0 && err == 0)
349 {
350 Stream stream = this.StreamHandles[streamHandle];
351 if (DuplicateStream.OriginalStream(stream) ==
352 DuplicateStream.OriginalStream(this.CabStream))
353 {
354 this.currentArchiveBytesProcessed += cb;
355 if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes)
356 {
357 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
358 }
359 }
360 }
361 return count;
362 }
363
364 internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
365 {
366 Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]);
367
368 if (stream == DuplicateStream.OriginalStream(this.FileStream))
369 {
370 this.context.CloseFileReadStream(this.currentFileName, stream);
371 this.FileStream = null;
372 long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed;
373 this.currentFileBytesProcessed += remainder;
374 this.fileBytesProcessed += remainder;
375 this.OnProgress(ArchiveProgressType.FinishFile);
376
377 this.currentFileTotalBytes = 0;
378 this.currentFileBytesProcessed = 0;
379 this.currentFileName = null;
380 }
381 else if (stream == DuplicateStream.OriginalStream(this.CabStream))
382 {
383 if (stream.CanWrite)
384 {
385 stream.Flush();
386 }
387
388 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
389 this.OnProgress(ArchiveProgressType.FinishArchive);
390 this.currentArchiveNumber++;
391 this.totalArchives++;
392
393 this.context.CloseArchiveWriteStream(
394 this.currentArchiveNumber,
395 this.currentArchiveName,
396 stream);
397
398 this.currentArchiveName = this.NextCabinetName;
399 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0;
400 this.totalFolderBytesProcessedInCurrentCab = 0;
401
402 this.CabStream = null;
403 }
404 else // Must be a temp stream
405 {
406 stream.Close();
407 this.tempStreams.Remove(stream);
408 }
409 return base.CabCloseStreamEx(streamHandle, out err, pv);
410 }
411
412 /// <summary>
413 /// Disposes of resources allocated by the cabinet engine.
414 /// </summary>
415 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
416 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
417 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
418 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
419 protected override void Dispose(bool disposing)
420 {
421 try
422 {
423 if (disposing)
424 {
425 if (this.fciHandle != null)
426 {
427 this.fciHandle.Dispose();
428 this.fciHandle = null;
429 }
430 }
431 }
432 finally
433 {
434 base.Dispose(disposing);
435 }
436 }
437
438 private static NativeMethods.FCI.TCOMP GetCompressionType(CompressionLevel compLevel)
439 {
440 if (compLevel < CompressionLevel.Min)
441 {
442 return NativeMethods.FCI.TCOMP.TYPE_NONE;
443 }
444 else
445 {
446 if (compLevel > CompressionLevel.Max)
447 {
448 compLevel = CompressionLevel.Max;
449 }
450
451 int lzxWindowMax =
452 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_HI >> (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW) -
453 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_LO >> (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW);
454 int lzxWindow = lzxWindowMax *
455 (compLevel - CompressionLevel.Min) / (CompressionLevel.Max - CompressionLevel.Min);
456
457 return (NativeMethods.FCI.TCOMP) ((int) NativeMethods.FCI.TCOMP.TYPE_LZX |
458 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_LO +
459 (lzxWindow << (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW)));
460 }
461 }
462
463 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
464 private void AddFile(
465 string name,
466 Stream stream,
467 FileAttributes attributes,
468 DateTime lastWriteTime,
469 bool execute,
470 CompressionLevel compLevel)
471 {
472 this.FileStream = stream;
473 this.fileAttributes = attributes &
474 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
475 this.fileLastWriteTime = lastWriteTime;
476 this.currentFileName = name;
477
478 NativeMethods.FCI.TCOMP tcomp = CabPacker.GetCompressionType(compLevel);
479
480 IntPtr namePtr = IntPtr.Zero;
481 try
482 {
483 Encoding nameEncoding = Encoding.ASCII;
484 if (Encoding.UTF8.GetByteCount(name) > name.Length)
485 {
486 nameEncoding = Encoding.UTF8;
487 this.fileAttributes |= FileAttributes.Normal; // _A_NAME_IS_UTF
488 }
489
490 byte[] nameBytes = nameEncoding.GetBytes(name);
491 namePtr = Marshal.AllocHGlobal(nameBytes.Length + 1);
492 Marshal.Copy(nameBytes, 0, namePtr, nameBytes.Length);
493 Marshal.WriteByte(namePtr, nameBytes.Length, 0);
494
495 this.Erf.Clear();
496 NativeMethods.FCI.AddFile(
497 this.fciHandle,
498 String.Empty,
499 namePtr,
500 execute,
501 this.fciGetNextCabinet,
502 this.fciCreateStatus,
503 this.fciGetOpenInfo,
504 tcomp);
505 }
506 finally
507 {
508 if (namePtr != IntPtr.Zero)
509 {
510 Marshal.FreeHGlobal(namePtr);
511 }
512 }
513
514 this.CheckError(false);
515 this.FileStream = null;
516 this.currentFileName = null;
517 }
518
519 private void FlushFolder()
520 {
521 this.Erf.Clear();
522 NativeMethods.FCI.FlushFolder(this.fciHandle, this.fciGetNextCabinet, this.fciCreateStatus);
523 this.CheckError(false);
524 }
525
526 private void FlushCabinet()
527 {
528 this.Erf.Clear();
529 NativeMethods.FCI.FlushCabinet(this.fciHandle, false, this.fciGetNextCabinet, this.fciCreateStatus);
530 this.CheckError(false);
531 }
532
533 private int CabGetOpenInfo(
534 string path,
535 out short date,
536 out short time,
537 out short attribs,
538 out int err,
539 IntPtr pv)
540 {
541 CompressionEngine.DateTimeToDosDateAndTime(this.fileLastWriteTime, out date, out time);
542 attribs = (short) this.fileAttributes;
543
544 Stream stream = this.FileStream;
545 this.FileStream = new DuplicateStream(stream);
546 int streamHandle = this.StreamHandles.AllocHandle(stream);
547 err = 0;
548 return streamHandle;
549 }
550
551 private int CabFilePlaced(
552 IntPtr pccab,
553 string filePath,
554 long fileSize,
555 int continuation,
556 IntPtr pv)
557 {
558 return 0;
559 }
560
561 private int CabGetNextCabinet(IntPtr pccab, uint prevCabSize, IntPtr pv)
562 {
563 NativeMethods.FCI.CCAB nextCcab = new NativeMethods.FCI.CCAB();
564 Marshal.PtrToStructure(pccab, nextCcab);
565
566 nextCcab.szDisk = String.Empty;
567 nextCcab.szCab = this.context.GetArchiveName(nextCcab.iCab);
568 this.CabNumbers[nextCcab.szCab] = (short) nextCcab.iCab;
569 this.NextCabinetName = nextCcab.szCab;
570
571 Marshal.StructureToPtr(nextCcab, pccab, false);
572 return 1;
573 }
574
575 private int CabCreateStatus(NativeMethods.FCI.STATUS typeStatus, uint cb1, uint cb2, IntPtr pv)
576 {
577 switch (typeStatus)
578 {
579 case NativeMethods.FCI.STATUS.FILE:
580 if (cb2 > 0 && this.currentFileBytesProcessed < this.currentFileTotalBytes)
581 {
582 if (this.currentFileBytesProcessed + cb2 > this.currentFileTotalBytes)
583 {
584 cb2 = (uint) this.currentFileTotalBytes - (uint) this.currentFileBytesProcessed;
585 }
586 this.currentFileBytesProcessed += cb2;
587 this.fileBytesProcessed += cb2;
588
589 this.OnProgress(ArchiveProgressType.PartialFile);
590 }
591 break;
592
593 case NativeMethods.FCI.STATUS.FOLDER:
594 if (cb1 == 0)
595 {
596 this.currentFolderTotalBytes = cb2 - this.totalFolderBytesProcessedInCurrentCab;
597 this.totalFolderBytesProcessedInCurrentCab = cb2;
598 }
599 else if (this.currentFolderTotalBytes == 0)
600 {
601 this.OnProgress(ArchiveProgressType.PartialArchive);
602 }
603 break;
604
605 case NativeMethods.FCI.STATUS.CABINET:
606 break;
607 }
608 return 0;
609 }
610
611 private int CabGetTempFile(IntPtr tempNamePtr, int tempNameSize, IntPtr pv)
612 {
613 string tempFileName;
614 if (this.UseTempFiles)
615 {
616 tempFileName = Path.GetFileName(Path.GetTempFileName());
617 }
618 else
619 {
620 tempFileName = CabPacker.TempStreamName;
621 }
622
623 byte[] tempNameBytes = Encoding.ASCII.GetBytes(tempFileName);
624 if (tempNameBytes.Length >= tempNameSize)
625 {
626 return -1;
627 }
628
629 Marshal.Copy(tempNameBytes, 0, tempNamePtr, tempNameBytes.Length);
630 Marshal.WriteByte(tempNamePtr, tempNameBytes.Length, 0); // null-terminator
631 return 1;
632 }
633
634 private int CabDeleteFile(string path, out int err, IntPtr pv)
635 {
636 try
637 {
638 // Deleting a temp file - don't bother if it is only a memory stream.
639 if (path != CabPacker.TempStreamName)
640 {
641 path = Path.Combine(Path.GetTempPath(), path);
642 File.Delete(path);
643 }
644 }
645 catch (IOException)
646 {
647 // Failure to delete a temp file is not fatal.
648 }
649 err = 0;
650 return 1;
651 }
652 }
653}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs
new file mode 100644
index 00000000..b0be4a15
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs
@@ -0,0 +1,566 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal class CabUnpacker : CabWorker
14 {
15 private NativeMethods.FDI.Handle fdiHandle;
16
17 // These delegates need to be saved as member variables
18 // so that they don't get GC'd.
19 private NativeMethods.FDI.PFNALLOC fdiAllocMemHandler;
20 private NativeMethods.FDI.PFNFREE fdiFreeMemHandler;
21 private NativeMethods.FDI.PFNOPEN fdiOpenStreamHandler;
22 private NativeMethods.FDI.PFNREAD fdiReadStreamHandler;
23 private NativeMethods.FDI.PFNWRITE fdiWriteStreamHandler;
24 private NativeMethods.FDI.PFNCLOSE fdiCloseStreamHandler;
25 private NativeMethods.FDI.PFNSEEK fdiSeekStreamHandler;
26
27 private IUnpackStreamContext context;
28
29 private List<ArchiveFileInfo> fileList;
30
31 private int folderId;
32
33 private Predicate<string> filter;
34
35 public CabUnpacker(CabEngine cabEngine)
36 : base(cabEngine)
37 {
38 this.fdiAllocMemHandler = this.CabAllocMem;
39 this.fdiFreeMemHandler = this.CabFreeMem;
40 this.fdiOpenStreamHandler = this.CabOpenStream;
41 this.fdiReadStreamHandler = this.CabReadStream;
42 this.fdiWriteStreamHandler = this.CabWriteStream;
43 this.fdiCloseStreamHandler = this.CabCloseStream;
44 this.fdiSeekStreamHandler = this.CabSeekStream;
45
46 this.fdiHandle = NativeMethods.FDI.Create(
47 this.fdiAllocMemHandler,
48 this.fdiFreeMemHandler,
49 this.fdiOpenStreamHandler,
50 this.fdiReadStreamHandler,
51 this.fdiWriteStreamHandler,
52 this.fdiCloseStreamHandler,
53 this.fdiSeekStreamHandler,
54 NativeMethods.FDI.CPU_80386,
55 this.ErfHandle.AddrOfPinnedObject());
56 if (this.Erf.Error)
57 {
58 int error = this.Erf.Oper;
59 int errorCode = this.Erf.Type;
60 this.ErfHandle.Free();
61 throw new CabException(
62 error,
63 errorCode,
64 CabException.GetErrorMessage(error, errorCode, true));
65 }
66 }
67
68 public bool IsArchive(Stream stream)
69 {
70 if (stream == null)
71 {
72 throw new ArgumentNullException("stream");
73 }
74
75 lock (this)
76 {
77 short id;
78 int folderCount, fileCount;
79 return this.IsCabinet(stream, out id, out folderCount, out fileCount);
80 }
81 }
82
83 public IList<ArchiveFileInfo> GetFileInfo(
84 IUnpackStreamContext streamContext,
85 Predicate<string> fileFilter)
86 {
87 if (streamContext == null)
88 {
89 throw new ArgumentNullException("streamContext");
90 }
91
92 lock (this)
93 {
94 this.context = streamContext;
95 this.filter = fileFilter;
96 this.NextCabinetName = String.Empty;
97 this.fileList = new List<ArchiveFileInfo>();
98 bool tmpSuppress = this.SuppressProgressEvents;
99 this.SuppressProgressEvents = true;
100 try
101 {
102 for (short cabNumber = 0;
103 this.NextCabinetName != null;
104 cabNumber++)
105 {
106 this.Erf.Clear();
107 this.CabNumbers[this.NextCabinetName] = cabNumber;
108
109 NativeMethods.FDI.Copy(
110 this.fdiHandle,
111 this.NextCabinetName,
112 String.Empty,
113 0,
114 this.CabListNotify,
115 IntPtr.Zero,
116 IntPtr.Zero);
117 this.CheckError(true);
118 }
119
120 List<ArchiveFileInfo> tmpFileList = this.fileList;
121 this.fileList = null;
122 return tmpFileList.AsReadOnly();
123 }
124 finally
125 {
126 this.SuppressProgressEvents = tmpSuppress;
127
128 if (this.CabStream != null)
129 {
130 this.context.CloseArchiveReadStream(
131 this.currentArchiveNumber,
132 this.currentArchiveName,
133 this.CabStream);
134 this.CabStream = null;
135 }
136
137 this.context = null;
138 }
139 }
140 }
141
142 public void Unpack(
143 IUnpackStreamContext streamContext,
144 Predicate<string> fileFilter)
145 {
146 lock (this)
147 {
148 IList<ArchiveFileInfo> files =
149 this.GetFileInfo(streamContext, fileFilter);
150
151 this.ResetProgressData();
152
153 if (files != null)
154 {
155 this.totalFiles = files.Count;
156
157 for (int i = 0; i < files.Count; i++)
158 {
159 this.totalFileBytes += files[i].Length;
160 if (files[i].ArchiveNumber >= this.totalArchives)
161 {
162 int totalArchives = files[i].ArchiveNumber + 1;
163 this.totalArchives = (short) totalArchives;
164 }
165 }
166 }
167
168 this.context = streamContext;
169 this.fileList = null;
170 this.NextCabinetName = String.Empty;
171 this.folderId = -1;
172 this.currentFileNumber = -1;
173
174 try
175 {
176 for (short cabNumber = 0;
177 this.NextCabinetName != null;
178 cabNumber++)
179 {
180 this.Erf.Clear();
181 this.CabNumbers[this.NextCabinetName] = cabNumber;
182
183 NativeMethods.FDI.Copy(
184 this.fdiHandle,
185 this.NextCabinetName,
186 String.Empty,
187 0,
188 this.CabExtractNotify,
189 IntPtr.Zero,
190 IntPtr.Zero);
191 this.CheckError(true);
192 }
193 }
194 finally
195 {
196 if (this.CabStream != null)
197 {
198 this.context.CloseArchiveReadStream(
199 this.currentArchiveNumber,
200 this.currentArchiveName,
201 this.CabStream);
202 this.CabStream = null;
203 }
204
205 if (this.FileStream != null)
206 {
207 this.context.CloseFileWriteStream(this.currentFileName, this.FileStream, FileAttributes.Normal, DateTime.Now);
208 this.FileStream = null;
209 }
210
211 this.context = null;
212 }
213 }
214 }
215
216 internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
217 {
218 if (this.CabNumbers.ContainsKey(path))
219 {
220 Stream stream = this.CabStream;
221 if (stream == null)
222 {
223 short cabNumber = this.CabNumbers[path];
224
225 stream = this.context.OpenArchiveReadStream(cabNumber, path, this.CabEngine);
226 if (stream == null)
227 {
228 throw new FileNotFoundException(String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber));
229 }
230 this.currentArchiveName = path;
231 this.currentArchiveNumber = cabNumber;
232 if (this.totalArchives <= this.currentArchiveNumber)
233 {
234 int totalArchives = this.currentArchiveNumber + 1;
235 this.totalArchives = (short) totalArchives;
236 }
237 this.currentArchiveTotalBytes = stream.Length;
238 this.currentArchiveBytesProcessed = 0;
239
240 if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab
241 {
242 this.OnProgress(ArchiveProgressType.StartArchive);
243 }
244 this.CabStream = stream;
245 }
246 path = CabWorker.CabStreamName;
247 }
248 return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv);
249 }
250
251 internal override int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
252 {
253 int count = base.CabReadStreamEx(streamHandle, memory, cb, out err, pv);
254 if (err == 0 && this.CabStream != null)
255 {
256 if (this.fileList == null)
257 {
258 Stream stream = this.StreamHandles[streamHandle];
259 if (DuplicateStream.OriginalStream(stream) ==
260 DuplicateStream.OriginalStream(this.CabStream))
261 {
262 this.currentArchiveBytesProcessed += cb;
263 if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes)
264 {
265 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
266 }
267 }
268 }
269 }
270 return count;
271 }
272
273 internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
274 {
275 int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv);
276 if (count > 0 && err == 0)
277 {
278 this.currentFileBytesProcessed += cb;
279 this.fileBytesProcessed += cb;
280 this.OnProgress(ArchiveProgressType.PartialFile);
281 }
282 return count;
283 }
284
285 internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
286 {
287 Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]);
288
289 if (stream == DuplicateStream.OriginalStream(this.CabStream))
290 {
291 if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab
292 {
293 this.OnProgress(ArchiveProgressType.FinishArchive);
294 }
295
296 this.context.CloseArchiveReadStream(this.currentArchiveNumber, this.currentArchiveName, stream);
297
298 this.currentArchiveName = this.NextCabinetName;
299 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0;
300
301 this.CabStream = null;
302 }
303 return base.CabCloseStreamEx(streamHandle, out err, pv);
304 }
305
306 /// <summary>
307 /// Disposes of resources allocated by the cabinet engine.
308 /// </summary>
309 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
310 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
311 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
312 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
313 protected override void Dispose(bool disposing)
314 {
315 try
316 {
317 if (disposing)
318 {
319 if (this.fdiHandle != null)
320 {
321 this.fdiHandle.Dispose();
322 this.fdiHandle = null;
323 }
324 }
325 }
326 finally
327 {
328 base.Dispose(disposing);
329 }
330 }
331
332 private static string GetFileName(NativeMethods.FDI.NOTIFICATION notification)
333 {
334 bool utf8Name = (notification.attribs & (ushort) FileAttributes.Normal) != 0; // _A_NAME_IS_UTF
335
336 // Non-utf8 names should be completely ASCII. But for compatibility with
337 // legacy tools, interpret them using the current (Default) ANSI codepage.
338 Encoding nameEncoding = utf8Name ? Encoding.UTF8 : Encoding.Default;
339
340 // Find how many bytes are in the string.
341 // Unfortunately there is no faster way.
342 int nameBytesCount = 0;
343 while (Marshal.ReadByte(notification.psz1, nameBytesCount) != 0)
344 {
345 nameBytesCount++;
346 }
347
348 byte[] nameBytes = new byte[nameBytesCount];
349 Marshal.Copy(notification.psz1, nameBytes, 0, nameBytesCount);
350 string name = nameEncoding.GetString(nameBytes);
351 if (Path.IsPathRooted(name))
352 {
353 name = name.Replace("" + Path.VolumeSeparatorChar, "");
354 }
355
356 return name;
357 }
358
359 private bool IsCabinet(Stream cabStream, out short id, out int cabFolderCount, out int fileCount)
360 {
361 int streamHandle = this.StreamHandles.AllocHandle(cabStream);
362 try
363 {
364 this.Erf.Clear();
365 NativeMethods.FDI.CABINFO fdici;
366 bool isCabinet = 0 != NativeMethods.FDI.IsCabinet(this.fdiHandle, streamHandle, out fdici);
367
368 if (this.Erf.Error)
369 {
370 if (((NativeMethods.FDI.ERROR) this.Erf.Oper) == NativeMethods.FDI.ERROR.UNKNOWN_CABINET_VERSION)
371 {
372 isCabinet = false;
373 }
374 else
375 {
376 throw new CabException(
377 this.Erf.Oper,
378 this.Erf.Type,
379 CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, true));
380 }
381 }
382
383 id = fdici.setID;
384 cabFolderCount = (int) fdici.cFolders;
385 fileCount = (int) fdici.cFiles;
386 return isCabinet;
387 }
388 finally
389 {
390 this.StreamHandles.FreeHandle(streamHandle);
391 }
392 }
393
394 private int CabListNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification)
395 {
396 switch (notificationType)
397 {
398 case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO:
399 {
400 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
401 this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null);
402 return 0; // Continue
403 }
404 case NativeMethods.FDI.NOTIFICATIONTYPE.PARTIAL_FILE:
405 {
406 // This notification can occur when examining the contents of a non-first cab file.
407 return 0; // Continue
408 }
409 case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE:
410 {
411 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
412
413 string name = CabUnpacker.GetFileName(notification);
414
415 if (this.filter == null || this.filter(name))
416 {
417 if (this.fileList != null)
418 {
419 FileAttributes attributes = (FileAttributes) notification.attribs &
420 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
421 if (attributes == (FileAttributes) 0)
422 {
423 attributes = FileAttributes.Normal;
424 }
425 DateTime lastWriteTime;
426 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
427 long length = notification.cb;
428
429 CabFileInfo fileInfo = new CabFileInfo(
430 name,
431 notification.iFolder,
432 notification.iCabinet,
433 attributes,
434 lastWriteTime,
435 length);
436 this.fileList.Add(fileInfo);
437 this.currentFileNumber = this.fileList.Count - 1;
438 this.fileBytesProcessed += notification.cb;
439 }
440 }
441
442 this.totalFiles++;
443 this.totalFileBytes += notification.cb;
444 return 0; // Continue
445 }
446 }
447 return 0;
448 }
449
450 private int CabExtractNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification)
451 {
452 switch (notificationType)
453 {
454 case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO:
455 {
456 if (this.NextCabinetName != null && this.NextCabinetName.StartsWith("?", StringComparison.Ordinal))
457 {
458 // We are just continuing the copy of a file that spanned cabinets.
459 // The next cabinet name needs to be preserved.
460 this.NextCabinetName = this.NextCabinetName.Substring(1);
461 }
462 else
463 {
464 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
465 this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null);
466 }
467 return 0; // Continue
468 }
469 case NativeMethods.FDI.NOTIFICATIONTYPE.NEXT_CABINET:
470 {
471 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
472 this.CabNumbers[nextCab] = (short) notification.iCabinet;
473 this.NextCabinetName = "?" + this.NextCabinetName;
474 return 0; // Continue
475 }
476 case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE:
477 {
478 return this.CabExtractCopyFile(notification);
479 }
480 case NativeMethods.FDI.NOTIFICATIONTYPE.CLOSE_FILE_INFO:
481 {
482 return this.CabExtractCloseFile(notification);
483 }
484 }
485 return 0;
486 }
487
488 private int CabExtractCopyFile(NativeMethods.FDI.NOTIFICATION notification)
489 {
490 if (notification.iFolder != this.folderId)
491 {
492 if (notification.iFolder != -3) // -3 is a special folderId used when continuing a folder from a previous cab
493 {
494 if (this.folderId != -1) // -1 means we just started the extraction sequence
495 {
496 this.currentFolderNumber++;
497 }
498 }
499 this.folderId = notification.iFolder;
500 }
501
502 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
503
504 string name = CabUnpacker.GetFileName(notification);
505
506 if (this.filter == null || this.filter(name))
507 {
508 this.currentFileNumber++;
509 this.currentFileName = name;
510
511 this.currentFileBytesProcessed = 0;
512 this.currentFileTotalBytes = notification.cb;
513 this.OnProgress(ArchiveProgressType.StartFile);
514
515 DateTime lastWriteTime;
516 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
517
518 Stream stream = this.context.OpenFileWriteStream(name, notification.cb, lastWriteTime);
519 if (stream != null)
520 {
521 this.FileStream = stream;
522 int streamHandle = this.StreamHandles.AllocHandle(stream);
523 return streamHandle;
524 }
525 else
526 {
527 this.fileBytesProcessed += notification.cb;
528 this.OnProgress(ArchiveProgressType.FinishFile);
529 this.currentFileName = null;
530 }
531 }
532 return 0; // Continue
533 }
534
535 private int CabExtractCloseFile(NativeMethods.FDI.NOTIFICATION notification)
536 {
537 Stream stream = this.StreamHandles[notification.hf];
538 this.StreamHandles.FreeHandle(notification.hf);
539
540 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
541
542 string name = CabUnpacker.GetFileName(notification);
543
544 FileAttributes attributes = (FileAttributes) notification.attribs &
545 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
546 if (attributes == (FileAttributes) 0)
547 {
548 attributes = FileAttributes.Normal;
549 }
550 DateTime lastWriteTime;
551 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
552
553 stream.Flush();
554 this.context.CloseFileWriteStream(name, stream, attributes, lastWriteTime);
555 this.FileStream = null;
556
557 long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed;
558 this.currentFileBytesProcessed += remainder;
559 this.fileBytesProcessed += remainder;
560 this.OnProgress(ArchiveProgressType.FinishFile);
561 this.currentFileName = null;
562
563 return 1; // Continue
564 }
565 }
566}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs
new file mode 100644
index 00000000..cb2a7263
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs
@@ -0,0 +1,337 @@
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.Cab
4{
5 using System;
6 using System.IO;
7 using System.IO.IsolatedStorage;
8 using System.Text;
9 using System.Security;
10 using System.Collections.Generic;
11 using System.Runtime.InteropServices;
12 using System.Diagnostics.CodeAnalysis;
13
14 internal abstract class CabWorker : IDisposable
15 {
16 internal const string CabStreamName = "%%CAB%%";
17
18 private CabEngine cabEngine;
19
20 private HandleManager<Stream> streamHandles;
21 private Stream cabStream;
22 private Stream fileStream;
23
24 private NativeMethods.ERF erf;
25 private GCHandle erfHandle;
26
27 private IDictionary<string, short> cabNumbers;
28 private string nextCabinetName;
29
30 private bool suppressProgressEvents;
31
32 private byte[] buf;
33
34 // Progress data
35 protected string currentFileName;
36 protected int currentFileNumber;
37 protected int totalFiles;
38 protected long currentFileBytesProcessed;
39 protected long currentFileTotalBytes;
40 protected short currentFolderNumber;
41 protected long currentFolderTotalBytes;
42 protected string currentArchiveName;
43 protected short currentArchiveNumber;
44 protected short totalArchives;
45 protected long currentArchiveBytesProcessed;
46 protected long currentArchiveTotalBytes;
47 protected long fileBytesProcessed;
48 protected long totalFileBytes;
49
50 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
51 protected CabWorker(CabEngine cabEngine)
52 {
53 this.cabEngine = cabEngine;
54 this.streamHandles = new HandleManager<Stream>();
55 this.erf = new NativeMethods.ERF();
56 this.erfHandle = GCHandle.Alloc(this.erf, GCHandleType.Pinned);
57 this.cabNumbers = new Dictionary<string, short>(1);
58
59 // 32K seems to be the size of the largest chunks processed by cabinet.dll.
60 // But just in case, this buffer will auto-enlarge.
61 this.buf = new byte[32768];
62 }
63
64 ~CabWorker()
65 {
66 this.Dispose(false);
67 }
68
69 public CabEngine CabEngine
70 {
71 get
72 {
73 return this.cabEngine;
74 }
75 }
76
77 internal NativeMethods.ERF Erf
78 {
79 get
80 {
81 return this.erf;
82 }
83 }
84
85 internal GCHandle ErfHandle
86 {
87 get
88 {
89 return this.erfHandle;
90 }
91 }
92
93 internal HandleManager<Stream> StreamHandles
94 {
95 get
96 {
97 return this.streamHandles;
98 }
99 }
100
101 internal bool SuppressProgressEvents
102 {
103 get
104 {
105 return this.suppressProgressEvents;
106 }
107
108 set
109 {
110 this.suppressProgressEvents = value;
111 }
112 }
113
114 internal IDictionary<string, short> CabNumbers
115 {
116 get
117 {
118 return this.cabNumbers;
119 }
120 }
121
122 internal string NextCabinetName
123 {
124 get
125 {
126 return this.nextCabinetName;
127 }
128
129 set
130 {
131 this.nextCabinetName = value;
132 }
133 }
134
135 internal Stream CabStream
136 {
137 get
138 {
139 return this.cabStream;
140 }
141
142 set
143 {
144 this.cabStream = value;
145 }
146 }
147
148 internal Stream FileStream
149 {
150 get
151 {
152 return this.fileStream;
153 }
154
155 set
156 {
157 this.fileStream = value;
158 }
159 }
160
161 public void Dispose()
162 {
163 this.Dispose(true);
164 GC.SuppressFinalize(this);
165 }
166
167 protected void ResetProgressData()
168 {
169 this.currentFileName = null;
170 this.currentFileNumber = 0;
171 this.totalFiles = 0;
172 this.currentFileBytesProcessed = 0;
173 this.currentFileTotalBytes = 0;
174 this.currentFolderNumber = 0;
175 this.currentFolderTotalBytes = 0;
176 this.currentArchiveName = null;
177 this.currentArchiveNumber = 0;
178 this.totalArchives = 0;
179 this.currentArchiveBytesProcessed = 0;
180 this.currentArchiveTotalBytes = 0;
181 this.fileBytesProcessed = 0;
182 this.totalFileBytes = 0;
183 }
184
185 protected void OnProgress(ArchiveProgressType progressType)
186 {
187 if (!this.suppressProgressEvents)
188 {
189 ArchiveProgressEventArgs e = new ArchiveProgressEventArgs(
190 progressType,
191 this.currentFileName,
192 this.currentFileNumber >= 0 ? this.currentFileNumber : 0,
193 this.totalFiles,
194 this.currentFileBytesProcessed,
195 this.currentFileTotalBytes,
196 this.currentArchiveName,
197 this.currentArchiveNumber,
198 this.totalArchives,
199 this.currentArchiveBytesProcessed,
200 this.currentArchiveTotalBytes,
201 this.fileBytesProcessed,
202 this.totalFileBytes);
203 this.CabEngine.ReportProgress(e);
204 }
205 }
206
207 internal IntPtr CabAllocMem(int byteCount)
208 {
209 IntPtr memPointer = Marshal.AllocHGlobal((IntPtr) byteCount);
210 return memPointer;
211 }
212
213 internal void CabFreeMem(IntPtr memPointer)
214 {
215 Marshal.FreeHGlobal(memPointer);
216 }
217
218 internal int CabOpenStream(string path, int openFlags, int shareMode)
219 {
220 int err; return this.CabOpenStreamEx(path, openFlags, shareMode, out err, IntPtr.Zero);
221 }
222
223 internal virtual int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
224 {
225 path = path.Trim();
226 Stream stream = this.cabStream;
227 this.cabStream = new DuplicateStream(stream);
228 int streamHandle = this.streamHandles.AllocHandle(stream);
229 err = 0;
230 return streamHandle;
231 }
232
233 internal int CabReadStream(int streamHandle, IntPtr memory, int cb)
234 {
235 int err; return this.CabReadStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero);
236 }
237
238 internal virtual int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
239 {
240 Stream stream = this.streamHandles[streamHandle];
241 int count = (int) cb;
242 if (count > this.buf.Length)
243 {
244 this.buf = new byte[count];
245 }
246 count = stream.Read(this.buf, 0, count);
247 Marshal.Copy(this.buf, 0, memory, count);
248 err = 0;
249 return count;
250 }
251
252 internal int CabWriteStream(int streamHandle, IntPtr memory, int cb)
253 {
254 int err; return this.CabWriteStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero);
255 }
256
257 internal virtual int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
258 {
259 Stream stream = this.streamHandles[streamHandle];
260 int count = (int) cb;
261 if (count > this.buf.Length)
262 {
263 this.buf = new byte[count];
264 }
265 Marshal.Copy(memory, this.buf, 0, count);
266 stream.Write(this.buf, 0, count);
267 err = 0;
268 return cb;
269 }
270
271 internal int CabCloseStream(int streamHandle)
272 {
273 int err; return this.CabCloseStreamEx(streamHandle, out err, IntPtr.Zero);
274 }
275
276 internal virtual int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
277 {
278 this.streamHandles.FreeHandle(streamHandle);
279 err = 0;
280 return 0;
281 }
282
283 internal int CabSeekStream(int streamHandle, int offset, int seekOrigin)
284 {
285 int err; return this.CabSeekStreamEx(streamHandle, offset, seekOrigin, out err, IntPtr.Zero);
286 }
287
288 internal virtual int CabSeekStreamEx(int streamHandle, int offset, int seekOrigin, out int err, IntPtr pv)
289 {
290 Stream stream = this.streamHandles[streamHandle];
291 offset = (int) stream.Seek(offset, (SeekOrigin) seekOrigin);
292 err = 0;
293 return offset;
294 }
295
296 /// <summary>
297 /// Disposes of resources allocated by the cabinet engine.
298 /// </summary>
299 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
300 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
301 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
302 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
303 protected virtual void Dispose(bool disposing)
304 {
305 if (disposing)
306 {
307 if (this.cabStream != null)
308 {
309 this.cabStream.Close();
310 this.cabStream = null;
311 }
312
313 if (this.fileStream != null)
314 {
315 this.fileStream.Close();
316 this.fileStream = null;
317 }
318 }
319
320 if (this.erfHandle.IsAllocated)
321 {
322 this.erfHandle.Free();
323 }
324 }
325
326 protected void CheckError(bool extracting)
327 {
328 if (this.Erf.Error)
329 {
330 throw new CabException(
331 this.Erf.Oper,
332 this.Erf.Type,
333 CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, extracting));
334 }
335 }
336 }
337}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources
new file mode 100644
index 00000000..d53d263c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources
Binary files differ
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt
new file mode 100644
index 00000000..df5a95d3
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt
@@ -0,0 +1,35 @@
1;
2; Cabinet Error Messages
3;
4
5; Generic error message.
61=Error code: {1}
7
8;
9; Cabinet creation messages - offset by 1000
10;
111000=Unknown error creating cabinet.
121001=Failure opening file to be stored in cabinet.
131002=Failure reading file to be stored in cabinet.
141003=Could not allocate enough memory to create cabinet.
151004=Could not create a temporary file.
161005=Unknown compression type.
171006=Could not create cabinet file.
181007=Client requested abort.
191008=Failure compressing data.
20
21;
22; Cabinet extraction messages - offset by 2000
23;
242000=Unknown error extracting cabinet.
252001=Cabinet not found.
262002=Cabinet file does not have the correct format.
272003=Cabinet file has an unknown version number.
282004=Cabinet file is corrupt.
292005=Could not allocate enough memory to extract cabinet.
302006=Unknown compression type in a cabinet folder.
312007=Failure decompressing data from a cabinet file.
322008=Failure writing to target file.
332009=Cabinets in a set do not have the same RESERVE sizes.
342010=Cabinet returned on NEXT_CABINET is incorrect.
352011=Client requested abort.
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs
new file mode 100644
index 00000000..aad9a317
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs
@@ -0,0 +1,76 @@
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.Cab
4{
5 using System;
6 using System.Collections.Generic;
7
8 /// <summary>
9 /// Generic class for managing allocations of integer handles
10 /// for objects of a certain type.
11 /// </summary>
12 /// <typeparam name="T">The type of objects the handles refer to.</typeparam>
13 internal sealed class HandleManager<T> where T : class
14 {
15 /// <summary>
16 /// Auto-resizing list of objects for which handles have been allocated.
17 /// Each handle is just an index into this list. When a handle is freed,
18 /// the list item at that index is set to null.
19 /// </summary>
20 private List<T> handles;
21
22 /// <summary>
23 /// Creates a new HandleManager instance.
24 /// </summary>
25 public HandleManager()
26 {
27 this.handles = new List<T>();
28 }
29
30 /// <summary>
31 /// Gets the object of a handle, or null if the handle is invalid.
32 /// </summary>
33 /// <param name="handle">The integer handle previously allocated
34 /// for the desired object.</param>
35 /// <returns>The object for which the handle was allocated.</returns>
36 public T this[int handle]
37 {
38 get
39 {
40 if (handle > 0 && handle <= this.handles.Count)
41 {
42 return this.handles[handle - 1];
43 }
44 else
45 {
46 return null;
47 }
48 }
49 }
50
51 /// <summary>
52 /// Allocates a new handle for an object.
53 /// </summary>
54 /// <param name="obj">Object that the handle will refer to.</param>
55 /// <returns>New handle that can be later used to retrieve the object.</returns>
56 public int AllocHandle(T obj)
57 {
58 this.handles.Add(obj);
59 int handle = this.handles.Count;
60 return handle;
61 }
62
63 /// <summary>
64 /// Frees a handle that was previously allocated. Afterward the handle
65 /// will be invalid and the object it referred to can no longer retrieved.
66 /// </summary>
67 /// <param name="handle">Handle to be freed.</param>
68 public void FreeHandle(int handle)
69 {
70 if (handle > 0 && handle <= this.handles.Count)
71 {
72 this.handles[handle - 1] = null;
73 }
74 }
75 }
76}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs
new file mode 100644
index 00000000..562e96dd
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs
@@ -0,0 +1,407 @@
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.Cab
4{
5using System;
6using System.Text;
7using System.Security;
8using System.Runtime.InteropServices;
9using System.Diagnostics.CodeAnalysis;
10
11/// <summary>
12/// Native DllImport methods and related structures and constants used for
13/// cabinet creation and extraction via cabinet.dll.
14/// </summary>
15internal static class NativeMethods
16{
17 /// <summary>
18 /// A direct import of constants, enums, structures, delegates, and functions from fci.h.
19 /// Refer to comments in fci.h for documentation.
20 /// </summary>
21 internal static class FCI
22 {
23 internal const int MIN_DISK = 32768;
24 internal const int MAX_DISK = Int32.MaxValue;
25 internal const int MAX_FOLDER = 0x7FFF8000;
26 internal const int MAX_FILENAME = 256;
27 internal const int MAX_CABINET_NAME = 256;
28 internal const int MAX_CAB_PATH = 256;
29 internal const int MAX_DISK_NAME = 256;
30
31 internal const int CPU_80386 = 1;
32
33 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb);
34 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv);
35
36 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode, out int err, IntPtr pv);
37 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv);
38 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv);
39 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int fileHandle, out int err, IntPtr pv);
40 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int fileHandle, int dist, int seekType, out int err, IntPtr pv);
41 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNDELETE(string path, out int err, IntPtr pv);
42
43 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETNEXTCABINET(IntPtr pccab, uint cbPrevCab, IntPtr pv);
44 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNFILEPLACED(IntPtr pccab, string path, long fileSize, int continuation, IntPtr pv);
45 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETOPENINFO(string path, out short date, out short time, out short pattribs, out int err, IntPtr pv);
46 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSTATUS(STATUS typeStatus, uint cb1, uint cb2, IntPtr pv);
47 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETTEMPFILE(IntPtr tempNamePtr, int tempNameSize, IntPtr pv);
48
49 /// <summary>
50 /// Error codes that can be returned by FCI.
51 /// </summary>
52 internal enum ERROR : int
53 {
54 NONE,
55 OPEN_SRC,
56 READ_SRC,
57 ALLOC_FAIL,
58 TEMP_FILE,
59 BAD_COMPR_TYPE,
60 CAB_FILE,
61 USER_ABORT,
62 MCI_FAIL,
63 }
64
65 /// <summary>
66 /// FCI compression algorithm types and parameters.
67 /// </summary>
68 internal enum TCOMP : ushort
69 {
70 MASK_TYPE = 0x000F,
71 TYPE_NONE = 0x0000,
72 TYPE_MSZIP = 0x0001,
73 TYPE_QUANTUM = 0x0002,
74 TYPE_LZX = 0x0003,
75 BAD = 0x000F,
76
77 MASK_LZX_WINDOW = 0x1F00,
78 LZX_WINDOW_LO = 0x0F00,
79 LZX_WINDOW_HI = 0x1500,
80 SHIFT_LZX_WINDOW = 0x0008,
81
82 MASK_QUANTUM_LEVEL = 0x00F0,
83 QUANTUM_LEVEL_LO = 0x0010,
84 QUANTUM_LEVEL_HI = 0x0070,
85 SHIFT_QUANTUM_LEVEL = 0x0004,
86
87 MASK_QUANTUM_MEM = 0x1F00,
88 QUANTUM_MEM_LO = 0x0A00,
89 QUANTUM_MEM_HI = 0x1500,
90 SHIFT_QUANTUM_MEM = 0x0008,
91
92 MASK_RESERVED = 0xE000,
93 }
94
95 /// <summary>
96 /// Reason for FCI status callback.
97 /// </summary>
98 internal enum STATUS : uint
99 {
100 FILE = 0,
101 FOLDER = 1,
102 CABINET = 2,
103 }
104
105 [SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments")]
106 [DllImport("cabinet.dll", EntryPoint = "FCICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
107 internal static extern Handle Create(IntPtr perf, PFNFILEPLACED pfnfcifp, PFNALLOC pfna, PFNFREE pfnf, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, PFNDELETE pfndelete, PFNGETTEMPFILE pfnfcigtf, [MarshalAs(UnmanagedType.LPStruct)] CCAB pccab, IntPtr pv);
108
109 [DllImport("cabinet.dll", EntryPoint = "FCIAddFile", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
110 internal static extern int AddFile(Handle hfci, string pszSourceFile, IntPtr pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fExecute, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis, PFNGETOPENINFO pfnfcigoi, TCOMP typeCompress);
111
112 [DllImport("cabinet.dll", EntryPoint = "FCIFlushCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
113 internal static extern int FlushCabinet(Handle hfci, [MarshalAs(UnmanagedType.Bool)] bool fGetNextCab, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis);
114
115 [DllImport("cabinet.dll", EntryPoint = "FCIFlushFolder", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
116 internal static extern int FlushFolder(Handle hfci, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis);
117
118 [SuppressUnmanagedCodeSecurity]
119 [DllImport("cabinet.dll", EntryPoint = "FCIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
120 [return: MarshalAs(UnmanagedType.Bool)]
121 internal static extern bool Destroy(IntPtr hfci);
122
123 /// <summary>
124 /// Cabinet information structure used for FCI initialization and GetNextCabinet callback.
125 /// </summary>
126 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
127 internal class CCAB
128 {
129 internal int cb = MAX_DISK;
130 internal int cbFolderThresh = MAX_FOLDER;
131 internal int cbReserveCFHeader;
132 internal int cbReserveCFFolder;
133 internal int cbReserveCFData;
134 internal int iCab;
135 internal int iDisk;
136 internal int fFailOnIncompressible;
137 internal short setID;
138 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_DISK_NAME )] internal string szDisk = String.Empty;
139 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_CABINET_NAME)] internal string szCab = String.Empty;
140 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_CAB_PATH )] internal string szCabPath = String.Empty;
141 }
142
143 /// <summary>
144 /// Ensures that the FCI handle is safely released.
145 /// </summary>
146 internal class Handle : SafeHandle
147 {
148 /// <summary>
149 /// Creates a new unintialized handle. The handle will be initialized
150 /// when it is marshalled back from native code.
151 /// </summary>
152 internal Handle()
153 : base(IntPtr.Zero, true)
154 {
155 }
156
157 /// <summary>
158 /// Checks if the handle is invalid. An FCI handle is invalid when it is zero.
159 /// </summary>
160 public override bool IsInvalid
161 {
162 get
163 {
164 return this.handle == IntPtr.Zero;
165 }
166 }
167
168 /// <summary>
169 /// Releases the handle by calling FDIDestroy().
170 /// </summary>
171 /// <returns>True if the release succeeded.</returns>
172 protected override bool ReleaseHandle()
173 {
174 return FCI.Destroy(this.handle);
175 }
176 }
177 }
178
179 /// <summary>
180 /// A direct import of constants, enums, structures, delegates, and functions from fdi.h.
181 /// Refer to comments in fdi.h for documentation.
182 /// </summary>
183 internal static class FDI
184 {
185 internal const int MAX_DISK = Int32.MaxValue;
186 internal const int MAX_FILENAME = 256;
187 internal const int MAX_CABINET_NAME = 256;
188 internal const int MAX_CAB_PATH = 256;
189 internal const int MAX_DISK_NAME = 256;
190
191 internal const int CPU_80386 = 1;
192
193 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb);
194 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv);
195
196 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode);
197 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int hf, IntPtr pv, int cb);
198 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int hf, IntPtr pv, int cb);
199 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int hf);
200 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int hf, int dist, int seektype);
201
202 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNNOTIFY(NOTIFICATIONTYPE fdint, NOTIFICATION fdin);
203
204 /// <summary>
205 /// Error codes that can be returned by FDI.
206 /// </summary>
207 internal enum ERROR : int
208 {
209 NONE,
210 CABINET_NOT_FOUND,
211 NOT_A_CABINET,
212 UNKNOWN_CABINET_VERSION,
213 CORRUPT_CABINET,
214 ALLOC_FAIL,
215 BAD_COMPR_TYPE,
216 MDI_FAIL,
217 TARGET_FILE,
218 RESERVE_MISMATCH,
219 WRONG_CABINET,
220 USER_ABORT,
221 }
222
223 /// <summary>
224 /// Type of notification message for the FDI Notify callback.
225 /// </summary>
226 internal enum NOTIFICATIONTYPE : int
227 {
228 CABINET_INFO,
229 PARTIAL_FILE,
230 COPY_FILE,
231 CLOSE_FILE_INFO,
232 NEXT_CABINET,
233 ENUMERATE,
234 }
235
236 [DllImport("cabinet.dll", EntryPoint = "FDICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
237 internal static extern Handle Create([MarshalAs(UnmanagedType.FunctionPtr)] PFNALLOC pfnalloc, [MarshalAs(UnmanagedType.FunctionPtr)] PFNFREE pfnfree, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, int cpuType, IntPtr perf);
238
239 [DllImport("cabinet.dll", EntryPoint = "FDICopy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
240 internal static extern int Copy(Handle hfdi, string pszCabinet, string pszCabPath, int flags, PFNNOTIFY pfnfdin, IntPtr pfnfdid, IntPtr pvUser);
241
242 [SuppressUnmanagedCodeSecurity]
243 [DllImport("cabinet.dll", EntryPoint = "FDIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
244 [return: MarshalAs(UnmanagedType.Bool)]
245 internal static extern bool Destroy(IntPtr hfdi);
246
247 [DllImport("cabinet.dll", EntryPoint = "FDIIsCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
248 [SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", Justification="FDI file handles definitely remain 4 bytes on 64bit platforms.")]
249 internal static extern int IsCabinet(Handle hfdi, int hf, out CABINFO pfdici);
250
251 /// <summary>
252 /// Cabinet information structure filled in by FDI IsCabinet.
253 /// </summary>
254 [StructLayout(LayoutKind.Sequential)]
255 internal struct CABINFO
256 {
257 internal int cbCabinet;
258 internal short cFolders;
259 internal short cFiles;
260 internal short setID;
261 internal short iCabinet;
262 internal int fReserve;
263 internal int hasprev;
264 internal int hasnext;
265 }
266
267 /// <summary>
268 /// Cabinet notification details passed to the FDI Notify callback.
269 /// </summary>
270 [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
271 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
272 internal class NOTIFICATION
273 {
274 internal int cb;
275 internal IntPtr psz1;
276 internal IntPtr psz2;
277 internal IntPtr psz3;
278 internal IntPtr pv;
279
280 internal IntPtr hf_ptr;
281
282 internal short date;
283 internal short time;
284 internal short attribs;
285 internal short setID;
286 internal short iCabinet;
287 internal short iFolder;
288 internal int fdie;
289
290 // Unlike all the other file handles in FCI/FDI, this one is
291 // actually pointer-sized. Use a property to pretend it isn't.
292 internal int hf
293 {
294 get { return (int) this.hf_ptr; }
295 }
296 }
297
298 /// <summary>
299 /// Ensures that the FDI handle is safely released.
300 /// </summary>
301 internal class Handle : SafeHandle
302 {
303 /// <summary>
304 /// Creates a new unintialized handle. The handle will be initialized
305 /// when it is marshalled back from native code.
306 /// </summary>
307 internal Handle()
308 : base(IntPtr.Zero, true)
309 {
310 }
311
312 /// <summary>
313 /// Checks if the handle is invalid. An FDI handle is invalid when it is zero.
314 /// </summary>
315 public override bool IsInvalid
316 {
317 get
318 {
319 return this.handle == IntPtr.Zero;
320 }
321 }
322
323 /// <summary>
324 /// Releases the handle by calling FDIDestroy().
325 /// </summary>
326 /// <returns>True if the release succeeded.</returns>
327 protected override bool ReleaseHandle()
328 {
329 return FDI.Destroy(this.handle);
330 }
331 }
332 }
333
334 /// <summary>
335 /// Error info structure for FCI and FDI.
336 /// </summary>
337 /// <remarks>Before being passed to FCI or FDI, this structure is
338 /// pinned in memory via a GCHandle. The pinning is necessary
339 /// to be able to read the results, since the ERF structure doesn't
340 /// get marshalled back out after an error.</remarks>
341 [StructLayout(LayoutKind.Sequential)]
342 internal class ERF
343 {
344 private int erfOper;
345 private int erfType;
346 private int fError;
347
348 /// <summary>
349 /// Gets or sets the cabinet error code.
350 /// </summary>
351 internal int Oper
352 {
353 get
354 {
355 return this.erfOper;
356 }
357
358 set
359 {
360 this.erfOper = value;
361 }
362 }
363
364 /// <summary>
365 /// Gets or sets the Win32 error code.
366 /// </summary>
367 internal int Type
368 {
369 get
370 {
371 return this.erfType;
372 }
373
374 set
375 {
376 this.erfType = value;
377 }
378 }
379
380 /// <summary>
381 /// GCHandle doesn't like the bool type, so use an int underneath.
382 /// </summary>
383 internal bool Error
384 {
385 get
386 {
387 return this.fError != 0;
388 }
389
390 set
391 {
392 this.fError = value ? 1 : 0;
393 }
394 }
395
396 /// <summary>
397 /// Clears the error information.
398 /// </summary>
399 internal void Clear()
400 {
401 this.Oper = 0;
402 this.Type = 0;
403 this.Error = false;
404 }
405 }
406}
407}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj b/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj
new file mode 100644
index 00000000..6b2c8cf8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj
@@ -0,0 +1,26 @@
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.Cab</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Compression.Cab</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Managed libraries for cabinet archive packing and unpacking</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <None Include="Errors.txt" />
15 <EmbeddedResource Include="Errors.resources" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
20 </ItemGroup>
21
22 <ItemGroup>
23 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
24 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
25 </ItemGroup>
26</Project>
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs
new file mode 100644
index 00000000..f782bbb8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs
@@ -0,0 +1,5 @@
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
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "WixToolset.Dtf.Compression.Zip")]
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs
new file mode 100644
index 00000000..20d675d9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs
@@ -0,0 +1,157 @@
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.Zip
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// Used to trick a DeflateStream into reading from or writing to
10 /// a series of (chunked) streams instead of a single steream.
11 /// </summary>
12 internal class ConcatStream : Stream
13 {
14 private Stream source;
15 private long position;
16 private long length;
17 private Action<ConcatStream> nextStreamHandler;
18
19 public ConcatStream(Action<ConcatStream> nextStreamHandler)
20 {
21 if (nextStreamHandler == null)
22 {
23 throw new ArgumentNullException("nextStreamHandler");
24 }
25
26 this.nextStreamHandler = nextStreamHandler;
27 this.length = Int64.MaxValue;
28 }
29
30 public Stream Source
31 {
32 get { return this.source; }
33 set { this.source = value; }
34 }
35
36 public override bool CanRead
37 {
38 get { return true; }
39 }
40
41 public override bool CanWrite
42 {
43 get { return true; }
44 }
45
46 public override bool CanSeek
47 {
48 get { return false; }
49 }
50
51 public override long Length
52 {
53 get
54 {
55 return this.length;
56 }
57 }
58
59 public override long Position
60 {
61 get { return this.position; }
62 set { throw new NotSupportedException(); }
63 }
64
65 public override int Read(byte[] buffer, int offset, int count)
66 {
67 if (this.source == null)
68 {
69 this.nextStreamHandler(this);
70 }
71
72 count = (int) Math.Min(count, this.length - this.position);
73
74 int bytesRemaining = count;
75 while (bytesRemaining > 0)
76 {
77 if (this.source == null)
78 {
79 throw new InvalidOperationException();
80 }
81
82 int partialCount = (int) Math.Min(bytesRemaining,
83 this.source.Length - this.source.Position);
84
85 if (partialCount == 0)
86 {
87 this.nextStreamHandler(this);
88 continue;
89 }
90
91 partialCount = this.source.Read(
92 buffer, offset + count - bytesRemaining, partialCount);
93 bytesRemaining -= partialCount;
94 this.position += partialCount;
95 }
96
97 return count;
98 }
99
100 public override void Write(byte[] buffer, int offset, int count)
101 {
102 if (this.source == null)
103 {
104 this.nextStreamHandler(this);
105 }
106
107 int bytesRemaining = count;
108 while (bytesRemaining > 0)
109 {
110 if (this.source == null)
111 {
112 throw new InvalidOperationException();
113 }
114
115 int partialCount = (int) Math.Min(bytesRemaining,
116 Math.Max(0, this.length - this.source.Position));
117
118 if (partialCount == 0)
119 {
120 this.nextStreamHandler(this);
121 continue;
122 }
123
124 this.source.Write(
125 buffer, offset + count - bytesRemaining, partialCount);
126 bytesRemaining -= partialCount;
127 this.position += partialCount;
128 }
129 }
130
131 public override void Flush()
132 {
133 if (this.source != null)
134 {
135 this.source.Flush();
136 }
137 }
138
139 public override long Seek(long offset, SeekOrigin origin)
140 {
141 throw new NotSupportedException();
142 }
143
144 public override void SetLength(long value)
145 {
146 this.length = value;
147 }
148
149 public override void Close()
150 {
151 if (this.source != null)
152 {
153 this.source.Close();
154 }
155 }
156 }
157}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs
new file mode 100644
index 00000000..c645ecc1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs
@@ -0,0 +1,250 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.Diagnostics.CodeAnalysis;
8
9 /// <summary>
10 /// Wraps a source stream and calcaluates a CRC over all bytes that are read or written.
11 /// </summary>
12 /// <remarks>
13 /// The CRC algorithm matches that used in the standard ZIP file format.
14 /// </remarks>
15 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")]
16 public class CrcStream : Stream
17 {
18 private Stream source;
19 private uint crc;
20
21 /// <summary>
22 /// Creates a new CrcStream instance from a source stream.
23 /// </summary>
24 /// <param name="source">Underlying stream where bytes will be read from or written to.</param>
25 public CrcStream(Stream source)
26 {
27 this.source = source;
28 }
29
30 /// <summary>
31 /// Gets the current CRC over all bytes that have been read or written
32 /// since this instance was created.
33 /// </summary>
34 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")]
35 public uint Crc
36 {
37 get
38 {
39 return this.crc;
40 }
41 }
42
43 /// <summary>
44 /// Gets the underlying stream that this stream reads from or writes to.
45 /// </summary>
46 public Stream Source
47 {
48 get
49 {
50 return this.source;
51 }
52 }
53
54 /// <summary>
55 /// Gets a value indicating whether the source stream supports reading.
56 /// </summary>
57 /// <value>true if the stream supports reading; otherwise, false.</value>
58 public override bool CanRead
59 {
60 get
61 {
62 return this.source.CanRead;
63 }
64 }
65
66 /// <summary>
67 /// Gets a value indicating whether the source stream supports writing.
68 /// </summary>
69 /// <value>true if the stream supports writing; otherwise, false.</value>
70 public override bool CanWrite
71 {
72 get
73 {
74 return this.source.CanWrite;
75 }
76 }
77
78 /// <summary>
79 /// Gets a value indicating whether the source stream supports seeking.
80 /// </summary>
81 /// <value>true if the stream supports seeking; otherwise, false.</value>
82 public override bool CanSeek
83 {
84 get
85 {
86 return this.source.CanSeek;
87 }
88 }
89
90 /// <summary>
91 /// Gets the length of the source stream.
92 /// </summary>
93 public override long Length
94 {
95 get
96 {
97 return this.source.Length;
98 }
99 }
100
101 /// <summary>
102 /// Gets or sets the position of the source stream.
103 /// </summary>
104 public override long Position
105 {
106 get
107 {
108 return this.source.Position;
109 }
110
111 set
112 {
113 this.source.Position = value;
114 }
115 }
116
117 /// <summary>
118 /// Sets the position within the source stream.
119 /// </summary>
120 /// <param name="offset">A byte offset relative to the origin parameter.</param>
121 /// <param name="origin">A value of type SeekOrigin indicating
122 /// the reference point used to obtain the new position.</param>
123 /// <returns>The new position within the source stream.</returns>
124 /// <remarks>
125 /// Note the CRC is only calculated over bytes that are actually read or
126 /// written, so any bytes skipped by seeking will not contribute to the CRC.
127 /// </remarks>
128 public override long Seek(long offset, SeekOrigin origin)
129 {
130 return this.source.Seek(offset, origin);
131 }
132
133 /// <summary>
134 /// Sets the length of the source stream.
135 /// </summary>
136 /// <param name="value">The desired length of the
137 /// stream in bytes.</param>
138 public override void SetLength(long value)
139 {
140 this.source.SetLength(value);
141 }
142
143 /// <summary>
144 /// Reads a sequence of bytes from the source stream and advances
145 /// the position within the stream by the number of bytes read.
146 /// </summary>
147 /// <param name="buffer">An array of bytes. When this method returns, the buffer
148 /// contains the specified byte array with the values between offset and
149 /// (offset + count - 1) replaced by the bytes read from the current source.</param>
150 /// <param name="offset">The zero-based byte offset in buffer at which to begin
151 /// storing the data read from the current stream.</param>
152 /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
153 /// <returns>The total number of bytes read into the buffer. This can be less
154 /// than the number of bytes requested if that many bytes are not currently available,
155 /// or zero (0) if the end of the stream has been reached.</returns>
156 public override int Read(byte[] buffer, int offset, int count)
157 {
158 count = this.source.Read(buffer, offset, count);
159 this.UpdateCrc(buffer, offset, count);
160 return count;
161 }
162
163 /// <summary>
164 /// Writes a sequence of bytes to the source stream and advances the
165 /// current position within this stream by the number of bytes written.
166 /// </summary>
167 /// <param name="buffer">An array of bytes. This method copies count
168 /// bytes from buffer to the current stream.</param>
169 /// <param name="offset">The zero-based byte offset in buffer at which
170 /// to begin copying bytes to the current stream.</param>
171 /// <param name="count">The number of bytes to be written to the
172 /// current stream.</param>
173 public override void Write(byte[] buffer, int offset, int count)
174 {
175 this.source.Write(buffer, offset, count);
176 this.UpdateCrc(buffer, offset, count);
177 }
178
179 /// <summary>
180 /// Flushes the source stream.
181 /// </summary>
182 public override void Flush()
183 {
184 this.source.Flush();
185 }
186
187 /// <summary>
188 /// Closes the underlying stream.
189 /// </summary>
190 public override void Close()
191 {
192 this.source.Close();
193 base.Close();
194 }
195
196 /// <summary>
197 /// Updates the CRC with a range of bytes that were read or written.
198 /// </summary>
199 private void UpdateCrc(byte[] buffer, int offset, int count)
200 {
201 this.crc = ~this.crc;
202 for( ; count > 0; count--, offset++)
203 {
204 this.crc = (this.crc >> 8) ^
205 CrcStream.crcTable[(this.crc & 0xFF) ^ buffer[offset]];
206 }
207 this.crc = ~this.crc;
208 }
209
210 private static uint[] crcTable = MakeCrcTable();
211
212 /// <summary>
213 /// Computes a table that speeds up calculation of the CRC.
214 /// </summary>
215 private static uint[] MakeCrcTable()
216 {
217 const uint poly = 0x04C11DB7u;
218 uint[] crcTable = new uint[256];
219 for(uint n = 0; n < 256; n++)
220 {
221 uint c = CrcStream.Reflect(n, 8);
222 c = c << 24;
223 for(uint k = 0; k < 8; k++)
224 {
225 c = (c << 1) ^ ((c & 0x80000000u) != 0 ? poly : 0);
226 }
227 crcTable[n] = CrcStream.Reflect(c, 32);
228 }
229 return crcTable;
230 }
231
232 /// <summary>
233 /// Reflects the ordering of certain number of bits. For exmample when reflecting
234 /// one byte, bit one is swapped with bit eight, bit two with bit seven, etc.
235 /// </summary>
236 private static uint Reflect(uint value, int bits)
237 {
238 for (int i = 0; i < bits / 2; i++)
239 {
240 uint leftBit = 1u << (bits - 1 - i);
241 uint rightBit = 1u << i;
242 if (((value & leftBit) != 0) != ((value & rightBit) != 0))
243 {
244 value ^= leftBit | rightBit;
245 }
246 }
247 return value;
248 }
249 }
250}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj b/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj
new file mode 100644
index 00000000..2f5f2f27
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.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.Zip</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Compression.Zip</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Managed libraries for zip archive packing and unpacking</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
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>
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs
new file mode 100644
index 00000000..2e1c7567
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs
@@ -0,0 +1,80 @@
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.Zip
4{
5 using System.Diagnostics.CodeAnalysis;
6
7 /// <summary>
8 /// Identifies the compression method or &quot;algorithm&quot;
9 /// used for a single file within a zip archive.
10 /// </summary>
11 /// <remarks>
12 /// Proprietary zip implementations may define additional compression
13 /// methods outside of those included here.
14 /// </remarks>
15 public enum ZipCompressionMethod
16 {
17 /// <summary>
18 /// The file is stored (no compression)
19 /// </summary>
20 Store = 0,
21
22 /// <summary>
23 /// The file is Shrunk
24 /// </summary>
25 Shrink = 1,
26
27 /// <summary>
28 /// The file is Reduced with compression factor 1
29 /// </summary>
30 Reduce1 = 2,
31
32 /// <summary>
33 /// The file is Reduced with compression factor 2
34 /// </summary>
35 Reduce2 = 3,
36
37 /// <summary>
38 /// The file is Reduced with compression factor 3
39 /// </summary>
40 Reduce3 = 4,
41
42 /// <summary>
43 /// The file is Reduced with compression factor 4
44 /// </summary>
45 Reduce4 = 5,
46
47 /// <summary>
48 /// The file is Imploded
49 /// </summary>
50 Implode = 6,
51
52 /// <summary>
53 /// The file is Deflated;
54 /// the most common and widely-compatible form of zip compression.
55 /// </summary>
56 Deflate = 8,
57
58 /// <summary>
59 /// The file is Deflated using the enhanced Deflate64 method.
60 /// </summary>
61 Deflate64 = 9,
62
63 /// <summary>
64 /// The file is compressed using the BZIP2 algorithm.
65 /// </summary>
66 BZip2 = 12,
67
68 /// <summary>
69 /// The file is compressed using the LZMA algorithm.
70 /// </summary>
71 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lzma")]
72 Lzma = 14,
73
74 /// <summary>
75 /// The file is compressed using the PPMd algorithm.
76 /// </summary>
77 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppmd")]
78 Ppmd = 98
79 }
80}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs
new file mode 100644
index 00000000..36b4db89
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs
@@ -0,0 +1,478 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9 using System.Reflection;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// Engine capable of packing and unpacking archives in the zip format.
14 /// </summary>
15 public partial class ZipEngine : CompressionEngine
16 {
17 private static Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>
18 compressionStreamCreators;
19 private static Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>
20 decompressionStreamCreators;
21
22 private static void InitCompressionStreamCreators()
23 {
24 if (ZipEngine.compressionStreamCreators == null)
25 {
26 ZipEngine.compressionStreamCreators = new
27 Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>();
28 ZipEngine.decompressionStreamCreators = new
29 Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>();
30
31 ZipEngine.RegisterCompressionStreamCreator(
32 ZipCompressionMethod.Store,
33 CompressionMode.Compress,
34 delegate(Stream stream) {
35 return stream;
36 });
37 ZipEngine.RegisterCompressionStreamCreator(
38 ZipCompressionMethod.Deflate,
39 CompressionMode.Compress,
40 delegate(Stream stream) {
41 return new DeflateStream(stream, CompressionMode.Compress, true);
42 });
43 ZipEngine.RegisterCompressionStreamCreator(
44 ZipCompressionMethod.Store,
45 CompressionMode.Decompress,
46 delegate(Stream stream) {
47 return stream;
48 });
49 ZipEngine.RegisterCompressionStreamCreator(
50 ZipCompressionMethod.Deflate,
51 CompressionMode.Decompress,
52 delegate(Stream stream) {
53 return new DeflateStream(stream, CompressionMode.Decompress, true);
54 });
55 }
56 }
57
58 /// <summary>
59 /// Registers a delegate that can create a warpper stream for
60 /// compressing or uncompressing the data of a source stream.
61 /// </summary>
62 /// <param name="compressionMethod">Compression method being registered.</param>
63 /// <param name="compressionMode">Indicates registration for ether
64 /// compress or decompress mode.</param>
65 /// <param name="creator">Delegate being registered.</param>
66 /// <remarks>
67 /// For compression, the delegate accepts a stream that writes to the archive
68 /// and returns a wrapper stream that compresses bytes as they are written.
69 /// For decompression, the delegate accepts a stream that reads from the archive
70 /// and returns a wrapper stream that decompresses bytes as they are read.
71 /// This wrapper stream model follows the design used by
72 /// System.IO.Compression.DeflateStream, and indeed that class is used
73 /// to implement the Deflate compression method by default.
74 /// <para>To unregister a delegate, call this method again and pass
75 /// null for the delegate parameter.</para>
76 /// </remarks>
77 /// <example>
78 /// When the ZipEngine class is initialized, the Deflate compression method
79 /// is automatically registered like this:
80 /// <code>
81 /// ZipEngine.RegisterCompressionStreamCreator(
82 /// ZipCompressionMethod.Deflate,
83 /// CompressionMode.Compress,
84 /// delegate(Stream stream) {
85 /// return new DeflateStream(stream, CompressionMode.Compress, true);
86 /// });
87 /// ZipEngine.RegisterCompressionStreamCreator(
88 /// ZipCompressionMethod.Deflate,
89 /// CompressionMode.Decompress,
90 /// delegate(Stream stream) {
91 /// return new DeflateStream(stream, CompressionMode.Decompress, true);
92 /// });
93 /// </code></example>
94 public static void RegisterCompressionStreamCreator(
95 ZipCompressionMethod compressionMethod,
96 CompressionMode compressionMode,
97 Converter<Stream, Stream> creator)
98 {
99 ZipEngine.InitCompressionStreamCreators();
100 if (compressionMode == CompressionMode.Compress)
101 {
102 ZipEngine.compressionStreamCreators[compressionMethod] = creator;
103 }
104 else
105 {
106 ZipEngine.decompressionStreamCreators[compressionMethod] = creator;
107 }
108 }
109
110 // Progress data
111 private string currentFileName;
112 private int currentFileNumber;
113 private int totalFiles;
114 private long currentFileBytesProcessed;
115 private long currentFileTotalBytes;
116 private string mainArchiveName;
117 private string currentArchiveName;
118 private short currentArchiveNumber;
119 private short totalArchives;
120 private long currentArchiveBytesProcessed;
121 private long currentArchiveTotalBytes;
122 private long fileBytesProcessed;
123 private long totalFileBytes;
124 private string comment;
125
126 /// <summary>
127 /// Creates a new instance of the zip engine.
128 /// </summary>
129 public ZipEngine()
130 : base()
131 {
132 ZipEngine.InitCompressionStreamCreators();
133 }
134
135 /// <summary>
136 /// Gets the comment from the last-examined archive,
137 /// or sets the comment to be added to any created archives.
138 /// </summary>
139 public string ArchiveComment
140 {
141 get
142 {
143 return this.comment;
144 }
145 set
146 {
147 this.comment = value;
148 }
149 }
150
151 /// <summary>
152 /// Checks whether a Stream begins with a header that indicates
153 /// it is a valid archive file.
154 /// </summary>
155 /// <param name="stream">Stream for reading the archive file.</param>
156 /// <returns>True if the stream is a valid zip archive
157 /// (with no offset); false otherwise.</returns>
158 public override bool IsArchive(Stream stream)
159 {
160 if (stream == null)
161 {
162 throw new ArgumentNullException("stream");
163 }
164
165 if (stream.Length - stream.Position < 4)
166 {
167 return false;
168 }
169
170 BinaryReader reader = new BinaryReader(stream);
171 uint sig = reader.ReadUInt32();
172 switch (sig)
173 {
174 case ZipFileHeader.LFHSIG:
175 case ZipEndOfCentralDirectory.EOCDSIG:
176 case ZipEndOfCentralDirectory.EOCD64SIG:
177 case ZipFileHeader.SPANSIG:
178 case ZipFileHeader.SPANSIG2:
179 return true;
180 default:
181 return false;
182 }
183 }
184
185 /// <summary>
186 /// Gets the offset of an archive that is positioned 0 or more bytes
187 /// from the start of the Stream.
188 /// </summary>
189 /// <param name="stream">A stream for reading the archive.</param>
190 /// <returns>The offset in bytes of the archive,
191 /// or -1 if no archive is found in the Stream.</returns>
192 /// <remarks>The archive must begin on a 4-byte boundary.</remarks>
193 public override long FindArchiveOffset(Stream stream)
194 {
195 long offset = base.FindArchiveOffset(stream);
196 if (offset > 0)
197 {
198 // Some self-extract packages include the exe stub in file offset calculations.
199 // Check the first header directory offset to decide whether the entire
200 // archive needs to be offset or not.
201
202 ZipEndOfCentralDirectory eocd = this.GetEOCD(null, stream);
203 if (eocd != null && eocd.totalEntries > 0)
204 {
205 stream.Seek(eocd.dirOffset, SeekOrigin.Begin);
206
207 ZipFileHeader header = new ZipFileHeader();
208 if (header.Read(stream, true) && header.localHeaderOffset < stream.Length)
209 {
210 stream.Seek(header.localHeaderOffset, SeekOrigin.Begin);
211 if (header.Read(stream, false))
212 {
213 return 0;
214 }
215 }
216 }
217 }
218
219 return offset;
220 }
221
222 /// <summary>
223 /// Gets information about files in a zip archive or archive chain.
224 /// </summary>
225 /// <param name="streamContext">A context interface to handle opening
226 /// and closing of archive and file streams.</param>
227 /// <param name="fileFilter">A predicate that can determine
228 /// which files to process, optional.</param>
229 /// <returns>Information about files in the archive stream.</returns>
230 /// <exception cref="ArchiveException">The archive provided
231 /// by the stream context is not valid.</exception>
232 /// <remarks>
233 /// The <paramref name="fileFilter"/> predicate takes an internal file
234 /// path and returns true to include the file or false to exclude it.
235 /// </remarks>
236 public override IList<ArchiveFileInfo> GetFileInfo(
237 IUnpackStreamContext streamContext,
238 Predicate<string> fileFilter)
239 {
240 if (streamContext == null)
241 {
242 throw new ArgumentNullException("streamContext");
243 }
244
245 lock (this)
246 {
247 IList<ZipFileHeader> headers = this.GetCentralDirectory(streamContext);
248 if (headers == null)
249 {
250 throw new ZipException("Zip central directory not found.");
251 }
252
253 List<ArchiveFileInfo> files = new List<ArchiveFileInfo>(headers.Count);
254 foreach (ZipFileHeader header in headers)
255 {
256 if (!header.IsDirectory &&
257 (fileFilter == null || fileFilter(header.fileName)))
258 {
259 files.Add(header.ToZipFileInfo());
260 }
261 }
262
263 return files.AsReadOnly();
264 }
265 }
266
267 /// <summary>
268 /// Reads all the file headers from the central directory in the main archive.
269 /// </summary>
270 private IList<ZipFileHeader> GetCentralDirectory(IUnpackStreamContext streamContext)
271 {
272 Stream archiveStream = null;
273 this.currentArchiveNumber = 0;
274 try
275 {
276 List<ZipFileHeader> headers = new List<ZipFileHeader>();
277 archiveStream = this.OpenArchive(streamContext, 0);
278
279 ZipEndOfCentralDirectory eocd = this.GetEOCD(streamContext, archiveStream);
280 if (eocd == null)
281 {
282 return null;
283 }
284 else if (eocd.totalEntries == 0)
285 {
286 return headers;
287 }
288
289 headers.Capacity = (int) eocd.totalEntries;
290
291 if (eocd.dirOffset > archiveStream.Length - ZipFileHeader.CFH_FIXEDSIZE)
292 {
293 streamContext.CloseArchiveReadStream(
294 this.currentArchiveNumber, String.Empty, archiveStream);
295 archiveStream = null;
296 }
297 else
298 {
299 archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin);
300 uint sig = new BinaryReader(archiveStream).ReadUInt32();
301 if (sig != ZipFileHeader.CFHSIG)
302 {
303 streamContext.CloseArchiveReadStream(
304 this.currentArchiveNumber, String.Empty, archiveStream);
305 archiveStream = null;
306 }
307 }
308
309 if (archiveStream == null)
310 {
311 this.currentArchiveNumber = (short) (eocd.dirStartDiskNumber + 1);
312 archiveStream = streamContext.OpenArchiveReadStream(
313 this.currentArchiveNumber, String.Empty, this);
314
315 if (archiveStream == null)
316 {
317 return null;
318 }
319 }
320
321 archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin);
322
323 while (headers.Count < eocd.totalEntries)
324 {
325 ZipFileHeader header = new ZipFileHeader();
326 if (!header.Read(archiveStream, true))
327 {
328 throw new ZipException(
329 "Missing or invalid central directory file header");
330 }
331
332 headers.Add(header);
333
334 if (headers.Count < eocd.totalEntries &&
335 archiveStream.Position == archiveStream.Length)
336 {
337 streamContext.CloseArchiveReadStream(
338 this.currentArchiveNumber, String.Empty, archiveStream);
339 this.currentArchiveNumber++;
340 archiveStream = streamContext.OpenArchiveReadStream(
341 this.currentArchiveNumber, String.Empty, this);
342 if (archiveStream == null)
343 {
344 this.currentArchiveNumber = 0;
345 archiveStream = streamContext.OpenArchiveReadStream(
346 this.currentArchiveNumber, String.Empty, this);
347 }
348 }
349 }
350
351 return headers;
352 }
353 finally
354 {
355 if (archiveStream != null)
356 {
357 streamContext.CloseArchiveReadStream(
358 this.currentArchiveNumber, String.Empty, archiveStream);
359 }
360 }
361 }
362
363 /// <summary>
364 /// Locates and reads the end of central directory record near the
365 /// end of the archive.
366 /// </summary>
367 [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
368 [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "streamContext")]
369 private ZipEndOfCentralDirectory GetEOCD(
370 IUnpackStreamContext streamContext, Stream archiveStream)
371 {
372 BinaryReader reader = new BinaryReader(archiveStream);
373 long offset = archiveStream.Length
374 - ZipEndOfCentralDirectory.EOCD_RECORD_FIXEDSIZE;
375 while (offset >= 0)
376 {
377 archiveStream.Seek(offset, SeekOrigin.Begin);
378
379 uint sig = reader.ReadUInt32();
380 if (sig == ZipEndOfCentralDirectory.EOCDSIG)
381 {
382 break;
383 }
384
385 offset--;
386 }
387
388 if (offset < 0)
389 {
390 return null;
391 }
392
393 ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory();
394 archiveStream.Seek(offset, SeekOrigin.Begin);
395 if (!eocd.Read(archiveStream))
396 {
397 throw new ZipException("Invalid end of central directory record");
398 }
399
400 if (eocd.dirOffset == (long) UInt32.MaxValue)
401 {
402 string saveComment = eocd.comment;
403
404 archiveStream.Seek(
405 offset - Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE,
406 SeekOrigin.Begin);
407
408 Zip64EndOfCentralDirectoryLocator eocdl =
409 new Zip64EndOfCentralDirectoryLocator();
410 if (!eocdl.Read(archiveStream))
411 {
412 throw new ZipException("Missing or invalid end of " +
413 "central directory record locator");
414 }
415
416 if (eocdl.dirStartDiskNumber == eocdl.totalDisks - 1)
417 {
418 // ZIP64 eocd is entirely in current stream.
419 archiveStream.Seek(eocdl.dirOffset, SeekOrigin.Begin);
420 if (!eocd.Read(archiveStream))
421 {
422 throw new ZipException("Missing or invalid ZIP64 end of " +
423 "central directory record");
424 }
425 }
426 else if (streamContext == null)
427 {
428 return null;
429 }
430 else
431 {
432 // TODO: handle EOCD64 spanning archives!
433 throw new NotImplementedException("Zip implementation does not " +
434 "handle end of central directory record that spans archives.");
435 }
436
437 eocd.comment = saveComment;
438 }
439
440 return eocd;
441 }
442
443 private void ResetProgressData()
444 {
445 this.currentFileName = null;
446 this.currentFileNumber = 0;
447 this.totalFiles = 0;
448 this.currentFileBytesProcessed = 0;
449 this.currentFileTotalBytes = 0;
450 this.currentArchiveName = null;
451 this.currentArchiveNumber = 0;
452 this.totalArchives = 0;
453 this.currentArchiveBytesProcessed = 0;
454 this.currentArchiveTotalBytes = 0;
455 this.fileBytesProcessed = 0;
456 this.totalFileBytes = 0;
457 }
458
459 private void OnProgress(ArchiveProgressType progressType)
460 {
461 ArchiveProgressEventArgs e = new ArchiveProgressEventArgs(
462 progressType,
463 this.currentFileName,
464 this.currentFileNumber >= 0 ? this.currentFileNumber : 0,
465 this.totalFiles,
466 this.currentFileBytesProcessed,
467 this.currentFileTotalBytes,
468 this.currentArchiveName,
469 this.currentArchiveNumber,
470 this.totalArchives,
471 this.currentArchiveBytesProcessed,
472 this.currentArchiveTotalBytes,
473 this.fileBytesProcessed,
474 this.totalFileBytes);
475 this.OnProgress(e);
476 }
477 }
478}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs
new file mode 100644
index 00000000..50fd6156
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs
@@ -0,0 +1,60 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.Resources;
8 using System.Globalization;
9 using System.Runtime.Serialization;
10
11 /// <summary>
12 /// Exception class for zip operations.
13 /// </summary>
14 [Serializable]
15 public class ZipException : ArchiveException
16 {
17 /// <summary>
18 /// Creates a new ZipException 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 ZipException(string message, Exception innerException)
26 : base(message, innerException) { }
27
28 /// <summary>
29 /// Creates a new ZipException with a specified error message.
30 /// </summary>
31 /// <param name="message">The message that describes the error.</param>
32 public ZipException(string message)
33 : this(message, null) { }
34
35 /// <summary>
36 /// Creates a new ZipException.
37 /// </summary>
38 public ZipException()
39 : this(null, null) { }
40
41 /// <summary>
42 /// Initializes a new instance of the ZipException class with serialized data.
43 /// </summary>
44 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
45 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
46 protected ZipException(SerializationInfo info, StreamingContext context) : base(info, context)
47 {
48 }
49
50 /// <summary>
51 /// Sets the SerializationInfo with information about the exception.
52 /// </summary>
53 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
54 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
55 public override void GetObjectData(SerializationInfo info, StreamingContext context)
56 {
57 base.GetObjectData(info, context);
58 }
59 }
60}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs
new file mode 100644
index 00000000..d865bbba
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs
@@ -0,0 +1,104 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a compressed file within a zip package; provides operations for getting
11 /// the file properties and extracting the file.
12 /// </summary>
13 [Serializable]
14 public class ZipFileInfo : ArchiveFileInfo
15 {
16 private long compressedLength;
17 private ZipCompressionMethod compressionMethod;
18
19 /// <summary>
20 /// Creates a new ZipFileInfo object representing a file within a zip in a specified path.
21 /// </summary>
22 /// <param name="zipInfo">An object representing the zip archive containing the file.</param>
23 /// <param name="filePath">The path to the file within the zip archive. Usually, this is a simple file
24 /// name, but if the zip archive contains a directory structure this may include the directory.</param>
25 public ZipFileInfo(ZipInfo zipInfo, string filePath)
26 : base(zipInfo, filePath)
27 {
28 if (zipInfo == null)
29 {
30 throw new ArgumentNullException("zipInfo");
31 }
32 }
33
34 /// <summary>
35 /// Creates a new ZipFileInfo object with all parameters specified,
36 /// used internally when reading the metadata out of a zip archive.
37 /// </summary>
38 /// <param name="filePath">The internal path and name of the file in the zip archive.</param>
39 /// <param name="zipNumber">The zip archive number where the file starts.</param>
40 /// <param name="attributes">The stored attributes of the file.</param>
41 /// <param name="lastWriteTime">The stored last write time of the file.</param>
42 /// <param name="length">The uncompressed size of the file.</param>
43 /// <param name="compressedLength">The compressed size of the file.</param>
44 /// <param name="compressionMethod">Compression algorithm used for this file.</param>
45 internal ZipFileInfo(
46 string filePath,
47 int zipNumber,
48 FileAttributes attributes,
49 DateTime lastWriteTime,
50 long length,
51 long compressedLength,
52 ZipCompressionMethod compressionMethod)
53 : base(filePath, zipNumber, attributes, lastWriteTime, length)
54 {
55 this.compressedLength = compressedLength;
56 this.compressionMethod = compressionMethod;
57 }
58
59 /// <summary>
60 /// Initializes a new instance of the ZipFileInfo class with serialized data.
61 /// </summary>
62 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
63 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
64 protected ZipFileInfo(SerializationInfo info, StreamingContext context)
65 : base(info, context)
66 {
67 this.compressedLength = info.GetInt64("compressedLength");
68 }
69
70 /// <summary>
71 /// Gets the compressed size of the file in bytes.
72 /// </summary>
73 public long CompressedLength
74 {
75 get
76 {
77 return this.compressedLength;
78 }
79 }
80
81 /// <summary>
82 /// Gets the method used to compress this file.
83 /// </summary>
84 public ZipCompressionMethod CompressionMethod
85 {
86 get
87 {
88 return this.compressionMethod;
89 }
90 }
91
92 /// <summary>
93 /// Sets the SerializationInfo with information about the archive.
94 /// </summary>
95 /// <param name="info">The SerializationInfo that holds the serialized object data.</param>
96 /// <param name="context">The StreamingContext that contains contextual information
97 /// about the source or destination.</param>
98 public override void GetObjectData(SerializationInfo info, StreamingContext context)
99 {
100 base.GetObjectData(info, context);
101 info.AddValue("compressedLength", this.compressedLength);
102 }
103 }
104}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs
new file mode 100644
index 00000000..dc5e1137
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs
@@ -0,0 +1,697 @@
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.Zip
4{
5 using System;
6 using System.Globalization;
7 using System.IO;
8 using System.Text;
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11
12 [Flags]
13 internal enum ZipFileFlags : ushort
14 {
15 None = 0x0000,
16 Encrypt = 0x0001,
17 CompressOption1 = 0x0002,
18 CompressOption2 = 0x0004,
19 DataDescriptor = 0x0008,
20 StrongEncrypt = 0x0040,
21 UTF8 = 0x0800
22 }
23
24 internal enum ZipExtraFileFieldType : ushort
25 {
26 ZIP64 = 0x0001,
27 NTFS_TIMES = 0x000A,
28 NTFS_ACLS = 0x4453,
29 EXTIME = 0x5455
30 }
31
32 internal class ZipFileHeader
33 {
34 public const uint LFHSIG = 0x04034B50;
35 public const uint CFHSIG = 0x02014B50;
36
37 public const uint SPANSIG = 0x08074b50;
38 public const uint SPANSIG2 = 0x30304b50;
39
40 public const uint LFH_FIXEDSIZE = 30;
41 public const uint CFH_FIXEDSIZE = 46;
42
43 public ushort versionMadeBy;
44 public ushort versionNeeded;
45 public ZipFileFlags flags;
46 public ZipCompressionMethod compressionMethod;
47 public short lastModTime;
48 public short lastModDate;
49 public uint crc32;
50 public uint compressedSize;
51 public uint uncompressedSize;
52 public ushort diskStart;
53 public ushort internalFileAttrs;
54 public uint externalFileAttrs;
55 public uint localHeaderOffset;
56 public string fileName;
57 public ZipExtraFileField[] extraFields;
58 public string fileComment;
59 public bool zip64;
60
61 public ZipFileHeader()
62 {
63 this.versionMadeBy = 20;
64 this.versionNeeded = 20;
65 }
66
67 public ZipFileHeader(ZipFileInfo fileInfo, bool zip64)
68 : this()
69 {
70 this.flags = ZipFileFlags.None;
71 this.compressionMethod = fileInfo.CompressionMethod;
72 this.fileName = Path.Combine(fileInfo.Path, fileInfo.Name);
73 CompressionEngine.DateTimeToDosDateAndTime(
74 fileInfo.LastWriteTime, out this.lastModDate, out this.lastModTime);
75 this.zip64 = zip64;
76
77 if (this.zip64)
78 {
79 this.compressedSize = UInt32.MaxValue;
80 this.uncompressedSize = UInt32.MaxValue;
81 this.diskStart = UInt16.MaxValue;
82 this.versionMadeBy = 45;
83 this.versionNeeded = 45;
84 ZipExtraFileField field = new ZipExtraFileField();
85 field.fieldType = ZipExtraFileFieldType.ZIP64;
86 field.SetZip64Data(
87 fileInfo.CompressedLength,
88 fileInfo.Length,
89 0,
90 fileInfo.ArchiveNumber);
91 this.extraFields = new ZipExtraFileField[] { field };
92 }
93 else
94 {
95 this.compressedSize = (uint) fileInfo.CompressedLength;
96 this.uncompressedSize = (uint) fileInfo.Length;
97 this.diskStart = (ushort) fileInfo.ArchiveNumber;
98 }
99 }
100
101 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")]
102 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")]
103 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")]
104 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")]
105 public void Update(
106 long compressedSize,
107 long uncompressedSize,
108 uint crc32,
109 long localHeaderOffset,
110 int archiveNumber)
111 {
112 this.crc32 = crc32;
113
114 if (this.zip64)
115 {
116 this.compressedSize = UInt32.MaxValue;
117 this.uncompressedSize = UInt32.MaxValue;
118 this.localHeaderOffset = UInt32.MaxValue;
119 this.diskStart = UInt16.MaxValue;
120
121 if (this.extraFields != null)
122 {
123 foreach (ZipExtraFileField field in this.extraFields)
124 {
125 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
126 {
127 field.SetZip64Data(
128 compressedSize,
129 uncompressedSize,
130 localHeaderOffset,
131 archiveNumber);
132 }
133 }
134 }
135 }
136 else
137 {
138 this.compressedSize = (uint) compressedSize;
139 this.uncompressedSize = (uint) uncompressedSize;
140 this.localHeaderOffset = (uint) localHeaderOffset;
141 this.diskStart = (ushort) archiveNumber;
142 }
143 }
144
145 public bool Read(Stream stream, bool central)
146 {
147 long startPos = stream.Position;
148
149 if (stream.Length - startPos <
150 (central ? CFH_FIXEDSIZE : LFH_FIXEDSIZE))
151 {
152 return false;
153 }
154
155 BinaryReader reader = new BinaryReader(stream);
156 uint sig = reader.ReadUInt32();
157
158 if (sig == SPANSIG || sig == SPANSIG2)
159 {
160 // Spanned zip files may optionally begin with a special marker.
161 // Just ignore it and move on.
162 sig = reader.ReadUInt32();
163 }
164
165 if (sig != (central ? CFHSIG : LFHSIG))
166 {
167 return false;
168 }
169
170 this.versionMadeBy = (central ? reader.ReadUInt16() : (ushort) 0);
171 this.versionNeeded = reader.ReadUInt16();
172 this.flags = (ZipFileFlags) reader.ReadUInt16();
173 this.compressionMethod = (ZipCompressionMethod) reader.ReadUInt16();
174 this.lastModTime = reader.ReadInt16();
175 this.lastModDate = reader.ReadInt16();
176 this.crc32 = reader.ReadUInt32();
177 this.compressedSize = reader.ReadUInt32();
178 this.uncompressedSize = reader.ReadUInt32();
179
180 this.zip64 = this.uncompressedSize == UInt32.MaxValue;
181
182 int fileNameLength = reader.ReadUInt16();
183 int extraFieldLength = reader.ReadUInt16();
184 int fileCommentLength;
185
186 if (central)
187 {
188 fileCommentLength = reader.ReadUInt16();
189
190 this.diskStart = reader.ReadUInt16();
191 this.internalFileAttrs = reader.ReadUInt16();
192 this.externalFileAttrs = reader.ReadUInt32();
193 this.localHeaderOffset = reader.ReadUInt32();
194 }
195 else
196 {
197 fileCommentLength = 0;
198 this.diskStart = 0;
199 this.internalFileAttrs = 0;
200 this.externalFileAttrs = 0;
201 this.localHeaderOffset = 0;
202 }
203
204 if (stream.Length - stream.Position <
205 fileNameLength + extraFieldLength + fileCommentLength)
206 {
207 return false;
208 }
209
210 Encoding headerEncoding = ((this.flags | ZipFileFlags.UTF8) != 0 ?
211 Encoding.UTF8 : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage));
212
213 byte[] fileNameBytes = reader.ReadBytes(fileNameLength);
214 this.fileName = headerEncoding.GetString(fileNameBytes).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
215
216 List<ZipExtraFileField> fields = new List<ZipExtraFileField>();
217 while (extraFieldLength > 0)
218 {
219 ZipExtraFileField field = new ZipExtraFileField();
220 if (!field.Read(stream, ref extraFieldLength))
221 {
222 return false;
223 }
224 fields.Add(field);
225 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
226 {
227 this.zip64 = true;
228 }
229 }
230 this.extraFields = fields.ToArray();
231
232 byte[] fileCommentBytes = reader.ReadBytes(fileCommentLength);
233 this.fileComment = headerEncoding.GetString(fileCommentBytes);
234
235 return true;
236 }
237
238 public void Write(Stream stream, bool central)
239 {
240 byte[] fileNameBytes = (this.fileName != null
241 ? Encoding.UTF8.GetBytes(this.fileName) : new byte[0]);
242 byte[] fileCommentBytes = (this.fileComment != null
243 ? Encoding.UTF8.GetBytes(this.fileComment) : new byte[0]);
244 bool useUtf8 =
245 (this.fileName != null && fileNameBytes.Length > this.fileName.Length) ||
246 (this.fileComment != null && fileCommentBytes.Length > this.fileComment.Length);
247 if (useUtf8)
248 {
249 this.flags |= ZipFileFlags.UTF8;
250 }
251
252 BinaryWriter writer = new BinaryWriter(stream);
253 writer.Write(central ? CFHSIG : LFHSIG);
254 if (central)
255 {
256 writer.Write(this.versionMadeBy);
257 }
258 writer.Write(this.versionNeeded);
259 writer.Write((ushort) this.flags);
260 writer.Write((ushort) this.compressionMethod);
261 writer.Write(this.lastModTime);
262 writer.Write(this.lastModDate);
263 writer.Write(this.crc32);
264 writer.Write(this.compressedSize);
265 writer.Write(this.uncompressedSize);
266
267 ushort extraFieldLength = 0;
268 if (this.extraFields != null)
269 {
270 foreach (ZipExtraFileField field in this.extraFields)
271 {
272 if (field.data != null)
273 {
274 extraFieldLength += (ushort) (4 + field.data.Length);
275 }
276 }
277 }
278
279 writer.Write((ushort) fileNameBytes.Length);
280 writer.Write(extraFieldLength);
281
282 if (central)
283 {
284 writer.Write((ushort) fileCommentBytes.Length);
285
286 writer.Write(this.diskStart);
287 writer.Write(this.internalFileAttrs);
288 writer.Write(this.externalFileAttrs);
289 writer.Write(this.localHeaderOffset);
290 }
291
292 writer.Write(fileNameBytes);
293
294 if (this.extraFields != null)
295 {
296 foreach (ZipExtraFileField field in this.extraFields)
297 {
298 if (field.data != null)
299 {
300 field.Write(stream);
301 }
302 }
303 }
304
305 if (central)
306 {
307 writer.Write(fileCommentBytes);
308 }
309 }
310
311 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")]
312 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")]
313 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")]
314 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")]
315 public void GetZip64Fields(
316 out long compressedSize,
317 out long uncompressedSize,
318 out long localHeaderOffset,
319 out int archiveNumber,
320 out uint crc)
321 {
322 compressedSize = this.compressedSize;
323 uncompressedSize = this.uncompressedSize;
324 localHeaderOffset = this.localHeaderOffset;
325 archiveNumber = this.diskStart;
326 crc = this.crc32;
327
328 foreach (ZipExtraFileField field in this.extraFields)
329 {
330 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
331 {
332 field.GetZip64Data(
333 out compressedSize,
334 out uncompressedSize,
335 out localHeaderOffset,
336 out archiveNumber);
337 }
338 }
339 }
340
341 public ZipFileInfo ToZipFileInfo()
342 {
343 string name = this.fileName;
344
345 long compressedSizeL;
346 long uncompressedSizeL;
347 long localHeaderOffsetL;
348 int archiveNumberL;
349 uint crc;
350 this.GetZip64Fields(
351 out compressedSizeL,
352 out uncompressedSizeL,
353 out localHeaderOffsetL,
354 out archiveNumberL,
355 out crc);
356
357 DateTime dateTime;
358 CompressionEngine.DosDateAndTimeToDateTime(
359 this.lastModDate,
360 this.lastModTime,
361 out dateTime);
362 FileAttributes attrs = FileAttributes.Normal;
363 // TODO: look for attrs or times in extra fields
364
365 return new ZipFileInfo(name, archiveNumberL, attrs, dateTime,
366 uncompressedSizeL, compressedSizeL, this.compressionMethod);
367 }
368
369 public bool IsDirectory
370 {
371 get
372 {
373 return this.fileName != null &&
374 (this.fileName.EndsWith("/", StringComparison.Ordinal) ||
375 this.fileName.EndsWith("\\", StringComparison.Ordinal));
376 }
377 }
378
379 public int GetSize(bool central)
380 {
381 int size = 30;
382
383 int fileNameSize = (this.fileName != null
384 ? Encoding.UTF8.GetByteCount(this.fileName) : 0);
385 size += fileNameSize;
386
387 if (this.extraFields != null)
388 {
389 foreach (ZipExtraFileField field in this.extraFields)
390 {
391 if (field.data != null)
392 {
393 size += 4 + field.data.Length;
394 }
395 }
396 }
397
398 if (central)
399 {
400 size += 16;
401
402 int fileCommentSize = (this.fileComment != null
403 ? Encoding.UTF8.GetByteCount(this.fileComment) : 0);
404 size += fileCommentSize;
405 }
406
407 return size;
408 }
409 }
410
411 internal class ZipExtraFileField
412 {
413 public ZipExtraFileFieldType fieldType;
414 public byte[] data;
415
416 public bool Read(Stream stream, ref int bytesRemaining)
417 {
418 if (bytesRemaining < 4)
419 {
420 return false;
421 }
422
423 BinaryReader reader = new BinaryReader(stream);
424
425 this.fieldType = (ZipExtraFileFieldType) reader.ReadUInt16();
426 ushort dataSize = reader.ReadUInt16();
427 bytesRemaining -= 4;
428
429 if (bytesRemaining < dataSize)
430 {
431 return false;
432 }
433
434 this.data = reader.ReadBytes(dataSize);
435 bytesRemaining -= dataSize;
436
437 return true;
438 }
439
440 public void Write(Stream stream)
441 {
442 BinaryWriter writer = new BinaryWriter(stream);
443 writer.Write((ushort) this.fieldType);
444
445 byte[] dataBytes = (this.data != null ? this.data : new byte[0]);
446 writer.Write((ushort) dataBytes.Length);
447 writer.Write(dataBytes);
448 }
449
450 public bool GetZip64Data(
451 out long compressedSize,
452 out long uncompressedSize,
453 out long localHeaderOffset,
454 out int diskStart)
455 {
456 uncompressedSize = 0;
457 compressedSize = 0;
458 localHeaderOffset = 0;
459 diskStart = 0;
460
461 if (this.fieldType != ZipExtraFileFieldType.ZIP64 ||
462 this.data == null || this.data.Length != 28)
463 {
464 return false;
465 }
466
467 using (MemoryStream dataStream = new MemoryStream(this.data))
468 {
469 BinaryReader reader = new BinaryReader(dataStream);
470 uncompressedSize = reader.ReadInt64();
471 compressedSize = reader.ReadInt64();
472 localHeaderOffset = reader.ReadInt64();
473 diskStart = reader.ReadInt32();
474 }
475
476 return true;
477 }
478
479 public bool SetZip64Data(
480 long compressedSize,
481 long uncompressedSize,
482 long localHeaderOffset,
483 int diskStart)
484 {
485 if (this.fieldType != ZipExtraFileFieldType.ZIP64)
486 {
487 return false;
488 }
489
490 using (MemoryStream dataStream = new MemoryStream())
491 {
492 BinaryWriter writer = new BinaryWriter(dataStream);
493 writer.Write(uncompressedSize);
494 writer.Write(compressedSize);
495 writer.Write(localHeaderOffset);
496 writer.Write(diskStart);
497 this.data = dataStream.ToArray();
498 }
499
500 return true;
501 }
502 }
503
504 internal class ZipEndOfCentralDirectory
505 {
506 public const uint EOCDSIG = 0x06054B50;
507 public const uint EOCD64SIG = 0x06064B50;
508
509 public const uint EOCD_RECORD_FIXEDSIZE = 22;
510 public const uint EOCD64_RECORD_FIXEDSIZE = 56;
511
512 public ushort versionMadeBy;
513 public ushort versionNeeded;
514 public uint diskNumber;
515 public uint dirStartDiskNumber;
516 public long entriesOnDisk;
517 public long totalEntries;
518 public long dirSize;
519 public long dirOffset;
520 public string comment;
521 public bool zip64;
522
523 public ZipEndOfCentralDirectory()
524 {
525 this.versionMadeBy = 20;
526 this.versionNeeded = 20;
527 }
528
529 public bool Read(Stream stream)
530 {
531 long startPos = stream.Position;
532
533 if (stream.Length - startPos < EOCD_RECORD_FIXEDSIZE)
534 {
535 return false;
536 }
537
538 BinaryReader reader = new BinaryReader(stream);
539 uint sig = reader.ReadUInt32();
540
541 this.zip64 = false;
542 if (sig != EOCDSIG)
543 {
544 if (sig == EOCD64SIG)
545 {
546 this.zip64 = true;
547 }
548 else
549 {
550 return false;
551 }
552 }
553
554 if (this.zip64)
555 {
556 if (stream.Length - startPos < EOCD64_RECORD_FIXEDSIZE)
557 {
558 return false;
559 }
560
561 long recordSize = reader.ReadInt64();
562 this.versionMadeBy = reader.ReadUInt16();
563 this.versionNeeded = reader.ReadUInt16();
564 this.diskNumber = reader.ReadUInt32();
565 this.dirStartDiskNumber = reader.ReadUInt32();
566 this.entriesOnDisk = reader.ReadInt64();
567 this.totalEntries = reader.ReadInt64();
568 this.dirSize = reader.ReadInt64();
569 this.dirOffset = reader.ReadInt64();
570
571 // Ignore any extended zip64 eocd data.
572 long exDataSize = recordSize + 12 - EOCD64_RECORD_FIXEDSIZE;
573
574 if (stream.Length - stream.Position < exDataSize)
575 {
576 return false;
577 }
578
579 stream.Seek(exDataSize, SeekOrigin.Current);
580
581 this.comment = null;
582 }
583 else
584 {
585 this.diskNumber = reader.ReadUInt16();
586 this.dirStartDiskNumber = reader.ReadUInt16();
587 this.entriesOnDisk = reader.ReadUInt16();
588 this.totalEntries = reader.ReadUInt16();
589 this.dirSize = reader.ReadUInt32();
590 this.dirOffset = reader.ReadUInt32();
591
592 int commentLength = reader.ReadUInt16();
593
594 if (stream.Length - stream.Position < commentLength)
595 {
596 return false;
597 }
598
599 byte[] commentBytes = reader.ReadBytes(commentLength);
600 this.comment = Encoding.UTF8.GetString(commentBytes);
601 }
602
603 return true;
604 }
605
606 public void Write(Stream stream)
607 {
608 BinaryWriter writer = new BinaryWriter(stream);
609
610 if (this.zip64)
611 {
612 writer.Write(EOCD64SIG);
613 writer.Write((long) EOCD64_RECORD_FIXEDSIZE);
614 writer.Write(this.versionMadeBy);
615 writer.Write(this.versionNeeded);
616 writer.Write(this.diskNumber);
617 writer.Write(this.dirStartDiskNumber);
618 writer.Write(this.entriesOnDisk);
619 writer.Write(this.totalEntries);
620 writer.Write(this.dirSize);
621 writer.Write(this.dirOffset);
622 }
623 else
624 {
625 writer.Write(EOCDSIG);
626 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.diskNumber));
627 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.dirStartDiskNumber));
628 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.entriesOnDisk));
629 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.totalEntries));
630 writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirSize));
631 writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirOffset));
632
633 byte[] commentBytes = (this.comment != null
634 ? Encoding.UTF8.GetBytes(this.comment) : new byte[0]);
635 writer.Write((ushort) commentBytes.Length);
636 writer.Write(commentBytes);
637 }
638 }
639
640 public int GetSize(bool zip64Size)
641 {
642 if (zip64Size)
643 {
644 return 56;
645 }
646 else
647 {
648 int commentSize = (this.comment != null
649 ? Encoding.UTF8.GetByteCount(this.comment) : 0);
650 return 22 + commentSize;
651 }
652 }
653 }
654
655 internal class Zip64EndOfCentralDirectoryLocator
656 {
657 public const uint EOCDL64SIG = 0x07064B50;
658
659 public const uint EOCDL64_SIZE = 20;
660
661 public uint dirStartDiskNumber;
662 public long dirOffset;
663 public uint totalDisks;
664
665 public bool Read(Stream stream)
666 {
667 long startPos = stream.Position;
668 if (stream.Length - startPos < EOCDL64_SIZE)
669 {
670 return false;
671 }
672
673 BinaryReader reader = new BinaryReader(stream);
674 uint sig = reader.ReadUInt32();
675
676 if (sig != EOCDL64SIG)
677 {
678 return false;
679 }
680
681 this.dirStartDiskNumber = reader.ReadUInt32();
682 this.dirOffset = reader.ReadInt64();
683 this.totalDisks = reader.ReadUInt32();
684
685 return true;
686 }
687
688 public void Write(Stream stream)
689 {
690 BinaryWriter writer = new BinaryWriter(stream);
691 writer.Write(EOCDL64SIG);
692 writer.Write(this.dirStartDiskNumber);
693 writer.Write(this.dirOffset);
694 writer.Write(this.totalDisks);
695 }
696 }
697}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs
new file mode 100644
index 00000000..73f65fa0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs
@@ -0,0 +1,82 @@
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.Zip
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a zip file on disk; provides access to
11 /// file-based operations on the zip file.
12 /// </summary>
13 /// <remarks>
14 /// Generally, the methods on this class are much easier to use than the
15 /// stream-based interfaces provided by the <see cref="ZipEngine"/> class.
16 /// </remarks>
17 [Serializable]
18 public class ZipInfo : ArchiveInfo
19 {
20 /// <summary>
21 /// Creates a new CabinetInfo object representing a zip file in a specified path.
22 /// </summary>
23 /// <param name="path">The path to the zip file. When creating a zip file, this file does not
24 /// necessarily exist yet.</param>
25 public ZipInfo(string path)
26 : base(path)
27 {
28 }
29
30 /// <summary>
31 /// Initializes a new instance of the CabinetInfo class with serialized data.
32 /// </summary>
33 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
34 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
35 protected ZipInfo(SerializationInfo info, StreamingContext context)
36 : base(info, context)
37 {
38 }
39
40 /// <summary>
41 /// Creates a compression engine that does the low-level work for
42 /// this object.
43 /// </summary>
44 /// <returns>A new <see cref="ZipEngine"/> instance.</returns>
45 /// <remarks>
46 /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d
47 /// immediately after use.
48 /// </remarks>
49 protected override CompressionEngine CreateCompressionEngine()
50 {
51 return new ZipEngine();
52 }
53
54 /// <summary>
55 /// Gets information about the files contained in the archive.
56 /// </summary>
57 /// <returns>A list of <see cref="ZipFileInfo"/> objects, each
58 /// containing information about a file in the archive.</returns>
59 public new IList<ZipFileInfo> GetFiles()
60 {
61 IList<ArchiveFileInfo> files = base.GetFiles();
62 List<ZipFileInfo> zipFiles = new List<ZipFileInfo>(files.Count);
63 foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile);
64 return zipFiles.AsReadOnly();
65 }
66
67 /// <summary>
68 /// Gets information about the certain files contained in the archive file.
69 /// </summary>
70 /// <param name="searchPattern">The search string, such as
71 /// &quot;*.txt&quot;.</param>
72 /// <returns>A list of <see cref="ZipFileInfo"/> objects, each containing
73 /// information about a file in the archive.</returns>
74 public new IList<ZipFileInfo> GetFiles(string searchPattern)
75 {
76 IList<ArchiveFileInfo> files = base.GetFiles(searchPattern);
77 List<ZipFileInfo> zipFiles = new List<ZipFileInfo>(files.Count);
78 foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile);
79 return zipFiles.AsReadOnly();
80 }
81 }
82}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs
new file mode 100644
index 00000000..b9c183d3
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs
@@ -0,0 +1,489 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9 using System.Globalization;
10
11 public partial class ZipEngine
12 {
13 /// <summary>
14 /// Creates a zip archive or chain of zip archives.
15 /// </summary>
16 /// <param name="streamContext">A context interface to handle opening
17 /// and closing of archive and file streams.</param>
18 /// <param name="files">An array of file lists. Each list is
19 /// compressed into one stream in the archive.</param>
20 /// <param name="maxArchiveSize">The maximum number of bytes for one archive
21 /// before the contents are chained to the next archive, or zero for unlimited
22 /// archive size.</param>
23 /// <exception cref="ArchiveException">The archive could not be
24 /// created.</exception>
25 /// <remarks>
26 /// The stream context implementation may provide a mapping from the file
27 /// paths within the archive to the external file paths.
28 /// </remarks>
29 public override void Pack(
30 IPackStreamContext streamContext,
31 IEnumerable<string> files,
32 long maxArchiveSize)
33 {
34 if (streamContext == null)
35 {
36 throw new ArgumentNullException("streamContext");
37 }
38
39 if (files == null)
40 {
41 throw new ArgumentNullException("files");
42 }
43
44 lock (this)
45 {
46 Stream archiveStream = null;
47 try
48 {
49 this.ResetProgressData();
50 this.totalArchives = 1;
51
52 object forceZip64Value = streamContext.GetOption("forceZip64", null);
53 bool forceZip64 = Convert.ToBoolean(
54 forceZip64Value, CultureInfo.InvariantCulture);
55
56 // Count the total number of files and bytes to be compressed.
57 foreach (string file in files)
58 {
59 FileAttributes attributes;
60 DateTime lastWriteTime;
61 Stream fileStream = streamContext.OpenFileReadStream(
62 file,
63 out attributes,
64 out lastWriteTime);
65 if (fileStream != null)
66 {
67 this.totalFileBytes += fileStream.Length;
68 this.totalFiles++;
69 streamContext.CloseFileReadStream(file, fileStream);
70 }
71 }
72
73 List<ZipFileHeader> fileHeaders = new List<ZipFileHeader>();
74 this.currentFileNumber = -1;
75
76 if (this.currentArchiveName == null)
77 {
78 this.mainArchiveName = streamContext.GetArchiveName(0);
79 this.currentArchiveName = this.mainArchiveName;
80
81 if (String.IsNullOrEmpty(this.currentArchiveName))
82 {
83 throw new FileNotFoundException("No name provided for archive.");
84 }
85 }
86
87 this.OnProgress(ArchiveProgressType.StartArchive);
88
89 // Compress files one by one, saving header info for each.
90 foreach (string file in files)
91 {
92 ZipFileHeader fileHeader = this.PackOneFile(
93 streamContext,
94 file,
95 maxArchiveSize,
96 forceZip64,
97 ref archiveStream);
98
99 if (fileHeader != null)
100 {
101 fileHeaders.Add(fileHeader);
102 }
103
104 this.currentArchiveTotalBytes = (archiveStream != null ?
105 archiveStream.Position : 0);
106 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
107 }
108
109 bool zip64 = forceZip64 || this.totalFiles > UInt16.MaxValue;
110
111 // Write the central directory composed of all the file headers.
112 uint centralDirStartArchiveNumber = 0;
113 long centralDirStartPosition = 0;
114 long centralDirSize = 0;
115 for (int i = 0; i < fileHeaders.Count; i++)
116 {
117 ZipFileHeader fileHeader = fileHeaders[i];
118
119 int headerSize = fileHeader.GetSize(true);
120 centralDirSize += headerSize;
121
122 this.CheckArchiveWriteStream(
123 streamContext,
124 maxArchiveSize,
125 headerSize,
126 ref archiveStream);
127
128 if (i == 0)
129 {
130 centralDirStartArchiveNumber = (uint) this.currentArchiveNumber;
131 centralDirStartPosition = archiveStream.Position;
132 }
133
134 fileHeader.Write(archiveStream, true);
135 if (fileHeader.zip64)
136 {
137 zip64 = true;
138 }
139 }
140
141 this.currentArchiveTotalBytes =
142 (archiveStream != null ? archiveStream.Position : 0);
143 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
144
145 ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory();
146 eocd.dirStartDiskNumber = centralDirStartArchiveNumber;
147 eocd.entriesOnDisk = fileHeaders.Count;
148 eocd.totalEntries = fileHeaders.Count;
149 eocd.dirSize = centralDirSize;
150 eocd.dirOffset = centralDirStartPosition;
151 eocd.comment = this.comment;
152
153 Zip64EndOfCentralDirectoryLocator eocdl =
154 new Zip64EndOfCentralDirectoryLocator();
155
156 int maxFooterSize = eocd.GetSize(false);
157 if (archiveStream != null && (zip64 || archiveStream.Position >
158 ((long) UInt32.MaxValue) - eocd.GetSize(false)))
159 {
160 maxFooterSize += eocd.GetSize(true) + (int)
161 Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE;
162 zip64 = true;
163 }
164
165 this.CheckArchiveWriteStream(
166 streamContext,
167 maxArchiveSize,
168 maxFooterSize,
169 ref archiveStream);
170 eocd.diskNumber = (uint) this.currentArchiveNumber;
171
172 if (zip64)
173 {
174 eocd.versionMadeBy = 45;
175 eocd.versionNeeded = 45;
176 eocd.zip64 = true;
177 eocdl.dirOffset = archiveStream.Position;
178 eocdl.dirStartDiskNumber = (uint) this.currentArchiveNumber;
179 eocdl.totalDisks = (uint) this.currentArchiveNumber + 1;
180 eocd.Write(archiveStream);
181 eocdl.Write(archiveStream);
182
183 eocd.dirOffset = UInt32.MaxValue;
184 eocd.dirStartDiskNumber = UInt16.MaxValue;
185 }
186
187 eocd.zip64 = false;
188 eocd.Write(archiveStream);
189
190 this.currentArchiveTotalBytes = archiveStream.Position;
191 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
192 }
193 finally
194 {
195 if (archiveStream != null)
196 {
197 streamContext.CloseArchiveWriteStream(
198 this.currentArchiveNumber, this.mainArchiveName, archiveStream);
199 this.OnProgress(ArchiveProgressType.FinishArchive);
200 }
201 }
202 }
203 }
204
205 /// <summary>
206 /// Moves to the next archive in the sequence if necessary.
207 /// </summary>
208 private void CheckArchiveWriteStream(
209 IPackStreamContext streamContext,
210 long maxArchiveSize,
211 long requiredSize,
212 ref Stream archiveStream)
213 {
214 if (archiveStream != null &&
215 archiveStream.Length > 0 && maxArchiveSize > 0)
216 {
217 long sizeRemaining = maxArchiveSize - archiveStream.Length;
218 if (sizeRemaining < requiredSize)
219 {
220 string nextArchiveName = streamContext.GetArchiveName(
221 this.currentArchiveNumber + 1);
222
223 if (String.IsNullOrEmpty(nextArchiveName))
224 {
225 throw new FileNotFoundException("No name provided for archive #" +
226 this.currentArchiveNumber + 1);
227 }
228
229 this.currentArchiveTotalBytes = archiveStream.Position;
230 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
231
232 streamContext.CloseArchiveWriteStream(
233 this.currentArchiveNumber,
234 nextArchiveName,
235 archiveStream);
236 archiveStream = null;
237
238 this.OnProgress(ArchiveProgressType.FinishArchive);
239
240 this.currentArchiveNumber++;
241 this.totalArchives++;
242 this.currentArchiveBytesProcessed = 0;
243 this.currentArchiveTotalBytes = 0;
244 }
245 }
246
247 if (archiveStream == null)
248 {
249 if (this.currentArchiveNumber > 0)
250 {
251 this.OnProgress(ArchiveProgressType.StartArchive);
252 }
253
254 archiveStream = streamContext.OpenArchiveWriteStream(
255 this.currentArchiveNumber, this.mainArchiveName, true, this);
256
257 if (archiveStream == null)
258 {
259 throw new FileNotFoundException("Stream not provided for archive #" +
260 this.currentArchiveNumber);
261 }
262 }
263 }
264
265 /// <summary>
266 /// Adds one file to a zip archive in the process of being created.
267 /// </summary>
268 private ZipFileHeader PackOneFile(
269 IPackStreamContext streamContext,
270 string file,
271 long maxArchiveSize,
272 bool forceZip64,
273 ref Stream archiveStream)
274 {
275 Stream fileStream = null;
276 int headerArchiveNumber = 0;
277 try
278 {
279 // TODO: call GetOption to get compression method for the specific file
280 ZipCompressionMethod compressionMethod = ZipCompressionMethod.Deflate;
281 if (this.CompressionLevel == WixToolset.Dtf.Compression.CompressionLevel.None)
282 {
283 compressionMethod = ZipCompressionMethod.Store;
284 }
285
286 Converter<Stream, Stream> compressionStreamCreator;
287 if (!ZipEngine.compressionStreamCreators.TryGetValue(
288 compressionMethod, out compressionStreamCreator))
289 {
290 return null;
291 }
292
293 FileAttributes attributes;
294 DateTime lastWriteTime;
295 fileStream = streamContext.OpenFileReadStream(
296 file, out attributes, out lastWriteTime);
297 if (fileStream == null)
298 {
299 return null;
300 }
301
302 this.currentFileName = file;
303 this.currentFileNumber++;
304
305 this.currentFileTotalBytes = fileStream.Length;
306 this.currentFileBytesProcessed = 0;
307 this.OnProgress(ArchiveProgressType.StartFile);
308
309 ZipFileInfo fileInfo = new ZipFileInfo(
310 file,
311 this.currentArchiveNumber,
312 attributes,
313 lastWriteTime,
314 fileStream.Length,
315 0,
316 compressionMethod);
317
318 bool zip64 = forceZip64 || fileStream.Length >= (long) UInt32.MaxValue;
319 ZipFileHeader fileHeader = new ZipFileHeader(fileInfo, zip64);
320
321 this.CheckArchiveWriteStream(
322 streamContext,
323 maxArchiveSize,
324 fileHeader.GetSize(false),
325 ref archiveStream);
326
327 long headerPosition = archiveStream.Position;
328 fileHeader.Write(archiveStream, false);
329 headerArchiveNumber = this.currentArchiveNumber;
330
331 uint crc;
332 long bytesWritten = this.PackFileBytes(
333 streamContext,
334 fileStream,
335 maxArchiveSize,
336 compressionStreamCreator,
337 ref archiveStream,
338 out crc);
339
340 fileHeader.Update(
341 bytesWritten,
342 fileStream.Length,
343 crc,
344 headerPosition,
345 headerArchiveNumber);
346
347 streamContext.CloseFileReadStream(file, fileStream);
348 fileStream = null;
349
350 // Go back and rewrite the updated file header.
351 if (this.currentArchiveNumber == headerArchiveNumber)
352 {
353 long fileEndPosition = archiveStream.Position;
354 archiveStream.Seek(headerPosition, SeekOrigin.Begin);
355 fileHeader.Write(archiveStream, false);
356 archiveStream.Seek(fileEndPosition, SeekOrigin.Begin);
357 }
358 else
359 {
360 // The file spanned archives, so temporarily reopen
361 // the archive where it started.
362 string headerArchiveName = streamContext.GetArchiveName(
363 headerArchiveNumber + 1);
364 Stream headerStream = null;
365 try
366 {
367 headerStream = streamContext.OpenArchiveWriteStream(
368 headerArchiveNumber, headerArchiveName, false, this);
369 headerStream.Seek(headerPosition, SeekOrigin.Begin);
370 fileHeader.Write(headerStream, false);
371 }
372 finally
373 {
374 if (headerStream != null)
375 {
376 streamContext.CloseArchiveWriteStream(
377 headerArchiveNumber, headerArchiveName, headerStream);
378 }
379 }
380 }
381
382 this.OnProgress(ArchiveProgressType.FinishFile);
383
384 return fileHeader;
385 }
386 finally
387 {
388 if (fileStream != null)
389 {
390 streamContext.CloseFileReadStream(
391 this.currentFileName, fileStream);
392 }
393 }
394 }
395
396 /// <summary>
397 /// Writes compressed bytes of one file to the archive,
398 /// keeping track of the CRC and number of bytes written.
399 /// </summary>
400 private long PackFileBytes(
401 IPackStreamContext streamContext,
402 Stream fileStream,
403 long maxArchiveSize,
404 Converter<Stream, Stream> compressionStreamCreator,
405 ref Stream archiveStream,
406 out uint crc)
407 {
408 long writeStartPosition = archiveStream.Position;
409 long bytesWritten = 0;
410 CrcStream fileCrcStream = new CrcStream(fileStream);
411
412 ConcatStream concatStream = new ConcatStream(
413 delegate(ConcatStream s)
414 {
415 Stream sourceStream = s.Source;
416 bytesWritten += sourceStream.Position - writeStartPosition;
417
418 this.CheckArchiveWriteStream(
419 streamContext,
420 maxArchiveSize,
421 1,
422 ref sourceStream);
423
424 writeStartPosition = sourceStream.Position;
425 s.Source = sourceStream;
426 });
427
428 concatStream.Source = archiveStream;
429
430 if (maxArchiveSize > 0)
431 {
432 concatStream.SetLength(maxArchiveSize);
433 }
434
435 Stream compressionStream = compressionStreamCreator(concatStream);
436
437 try
438 {
439 byte[] buf = new byte[4096];
440 long bytesRemaining = fileStream.Length;
441 int counter = 0;
442 while (bytesRemaining > 0)
443 {
444 int count = (int) Math.Min(
445 bytesRemaining, (long) buf.Length);
446
447 count = fileCrcStream.Read(buf, 0, count);
448 if (count <= 0)
449 {
450 throw new ZipException(
451 "Failed to read file: " + this.currentFileName);
452 }
453
454 compressionStream.Write(buf, 0, count);
455 bytesRemaining -= count;
456
457 this.fileBytesProcessed += count;
458 this.currentFileBytesProcessed += count;
459 this.currentArchiveTotalBytes = concatStream.Source.Position;
460 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
461
462 if (++counter % 16 == 0) // Report every 64K
463 {
464 this.OnProgress(ArchiveProgressType.PartialFile);
465 }
466 }
467
468 if (compressionStream is DeflateStream)
469 {
470 compressionStream.Close();
471 }
472 else
473 {
474 compressionStream.Flush();
475 }
476 }
477 finally
478 {
479 archiveStream = concatStream.Source;
480 }
481
482 bytesWritten += archiveStream.Position - writeStartPosition;
483
484 crc = fileCrcStream.Crc;
485
486 return bytesWritten;
487 }
488 }
489}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs
new file mode 100644
index 00000000..681c0e46
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs
@@ -0,0 +1,336 @@
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.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9
10 public partial class ZipEngine
11 {
12 /// <summary>
13 /// Extracts files from a zip archive or archive chain.
14 /// </summary>
15 /// <param name="streamContext">A context interface to handle opening
16 /// and closing of archive and file streams.</param>
17 /// <param name="fileFilter">An optional predicate that can determine
18 /// which files to process.</param>
19 /// <exception cref="ArchiveException">The archive provided
20 /// by the stream context is not valid.</exception>
21 /// <remarks>
22 /// The <paramref name="fileFilter"/> predicate takes an internal file
23 /// path and returns true to include the file or false to exclude it.
24 /// </remarks>
25 public override void Unpack(
26 IUnpackStreamContext streamContext,
27 Predicate<string> fileFilter)
28 {
29 if (streamContext == null)
30 {
31 throw new ArgumentNullException("streamContext");
32 }
33
34 lock (this)
35 {
36 IList<ZipFileHeader> allHeaders = this.GetCentralDirectory(streamContext);
37 if (allHeaders == null)
38 {
39 throw new ZipException("Zip central directory not found.");
40 }
41
42 IList<ZipFileHeader> headers = new List<ZipFileHeader>(allHeaders.Count);
43 foreach (ZipFileHeader header in allHeaders)
44 {
45 if (!header.IsDirectory &&
46 (fileFilter == null || fileFilter(header.fileName)))
47 {
48 headers.Add(header);
49 }
50 }
51
52 this.ResetProgressData();
53
54 // Count the total number of files and bytes to be compressed.
55 this.totalFiles = headers.Count;
56 foreach (ZipFileHeader header in headers)
57 {
58 long compressedSize;
59 long uncompressedSize;
60 long localHeaderOffset;
61 int archiveNumber;
62 uint crc;
63 header.GetZip64Fields(
64 out compressedSize,
65 out uncompressedSize,
66 out localHeaderOffset,
67 out archiveNumber,
68 out crc);
69
70 this.totalFileBytes += uncompressedSize;
71 if (archiveNumber >= this.totalArchives)
72 {
73 this.totalArchives = (short) (archiveNumber + 1);
74 }
75 }
76
77 this.currentArchiveNumber = -1;
78 this.currentFileNumber = -1;
79 Stream archiveStream = null;
80 try
81 {
82 foreach (ZipFileHeader header in headers)
83 {
84 this.currentFileNumber++;
85 this.UnpackOneFile(streamContext, header, ref archiveStream);
86 }
87 }
88 finally
89 {
90 if (archiveStream != null)
91 {
92 streamContext.CloseArchiveReadStream(
93 0, String.Empty, archiveStream);
94 this.currentArchiveNumber--;
95 this.OnProgress(ArchiveProgressType.FinishArchive);
96 }
97 }
98 }
99 }
100
101 /// <summary>
102 /// Unpacks a single file from an archive or archive chain.
103 /// </summary>
104 private void UnpackOneFile(
105 IUnpackStreamContext streamContext,
106 ZipFileHeader header,
107 ref Stream archiveStream)
108 {
109 ZipFileInfo fileInfo = null;
110 Stream fileStream = null;
111 try
112 {
113 Converter<Stream, Stream> compressionStreamCreator;
114 if (!ZipEngine.decompressionStreamCreators.TryGetValue(
115 header.compressionMethod, out compressionStreamCreator))
116 {
117 // Silently skip files of an unsupported compression method.
118 return;
119 }
120
121 long compressedSize;
122 long uncompressedSize;
123 long localHeaderOffset;
124 int archiveNumber;
125 uint crc;
126 header.GetZip64Fields(
127 out compressedSize,
128 out uncompressedSize,
129 out localHeaderOffset,
130 out archiveNumber,
131 out crc);
132
133 if (this.currentArchiveNumber != archiveNumber + 1)
134 {
135 if (archiveStream != null)
136 {
137 streamContext.CloseArchiveReadStream(
138 this.currentArchiveNumber,
139 String.Empty,
140 archiveStream);
141 archiveStream = null;
142
143 this.OnProgress(ArchiveProgressType.FinishArchive);
144 this.currentArchiveName = null;
145 }
146
147 this.currentArchiveNumber = (short) (archiveNumber + 1);
148 this.currentArchiveBytesProcessed = 0;
149 this.currentArchiveTotalBytes = 0;
150
151 archiveStream = this.OpenArchive(
152 streamContext, this.currentArchiveNumber);
153
154 FileStream archiveFileStream = archiveStream as FileStream;
155 this.currentArchiveName = (archiveFileStream != null ?
156 Path.GetFileName(archiveFileStream.Name) : null);
157
158 this.currentArchiveTotalBytes = archiveStream.Length;
159 this.currentArchiveNumber--;
160 this.OnProgress(ArchiveProgressType.StartArchive);
161 this.currentArchiveNumber++;
162 }
163
164 archiveStream.Seek(localHeaderOffset, SeekOrigin.Begin);
165
166 ZipFileHeader localHeader = new ZipFileHeader();
167 if (!localHeader.Read(archiveStream, false) ||
168 !ZipEngine.AreFilePathsEqual(localHeader.fileName, header.fileName))
169 {
170 string msg = "Could not read file: " + header.fileName;
171 throw new ZipException(msg);
172 }
173
174 fileInfo = header.ToZipFileInfo();
175
176 fileStream = streamContext.OpenFileWriteStream(
177 fileInfo.FullName,
178 fileInfo.Length,
179 fileInfo.LastWriteTime);
180
181 if (fileStream != null)
182 {
183 this.currentFileName = header.fileName;
184 this.currentFileBytesProcessed = 0;
185 this.currentFileTotalBytes = fileInfo.Length;
186 this.currentArchiveNumber--;
187 this.OnProgress(ArchiveProgressType.StartFile);
188 this.currentArchiveNumber++;
189
190 this.UnpackFileBytes(
191 streamContext,
192 fileInfo.FullName,
193 fileInfo.CompressedLength,
194 fileInfo.Length,
195 header.crc32,
196 fileStream,
197 compressionStreamCreator,
198 ref archiveStream);
199 }
200 }
201 finally
202 {
203 if (fileStream != null)
204 {
205 streamContext.CloseFileWriteStream(
206 fileInfo.FullName,
207 fileStream,
208 fileInfo.Attributes,
209 fileInfo.LastWriteTime);
210
211 this.currentArchiveNumber--;
212 this.OnProgress(ArchiveProgressType.FinishFile);
213 this.currentArchiveNumber++;
214 }
215 }
216 }
217
218 /// <summary>
219 /// Compares two internal file paths while ignoring case and slash differences.
220 /// </summary>
221 /// <param name="path1">The first path to compare.</param>
222 /// <param name="path2">The second path to compare.</param>
223 /// <returns>True if the paths are equivalent.</returns>
224 private static bool AreFilePathsEqual(string path1, string path2)
225 {
226 path1 = path1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
227 path2 = path2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
228 return String.Compare(path1, path2, StringComparison.OrdinalIgnoreCase) == 0;
229 }
230
231 private Stream OpenArchive(IUnpackStreamContext streamContext, int archiveNumber)
232 {
233 Stream archiveStream = streamContext.OpenArchiveReadStream(
234 archiveNumber, String.Empty, this);
235 if (archiveStream == null && archiveNumber != 0)
236 {
237 archiveStream = streamContext.OpenArchiveReadStream(
238 0, String.Empty, this);
239 }
240
241 if (archiveStream == null)
242 {
243 throw new FileNotFoundException("Archive stream not provided.");
244 }
245
246 return archiveStream;
247 }
248
249 /// <summary>
250 /// Decompresses bytes for one file from an archive or archive chain,
251 /// checking the crc at the end.
252 /// </summary>
253 private void UnpackFileBytes(
254 IUnpackStreamContext streamContext,
255 string fileName,
256 long compressedSize,
257 long uncompressedSize,
258 uint crc,
259 Stream fileStream,
260 Converter<Stream, Stream> compressionStreamCreator,
261 ref Stream archiveStream)
262 {
263 CrcStream crcStream = new CrcStream(fileStream);
264
265 ConcatStream concatStream = new ConcatStream(
266 delegate(ConcatStream s)
267 {
268 this.currentArchiveBytesProcessed = s.Source.Position;
269 streamContext.CloseArchiveReadStream(
270 this.currentArchiveNumber,
271 String.Empty,
272 s.Source);
273
274 this.currentArchiveNumber--;
275 this.OnProgress(ArchiveProgressType.FinishArchive);
276 this.currentArchiveNumber += 2;
277 this.currentArchiveName = null;
278 this.currentArchiveBytesProcessed = 0;
279 this.currentArchiveTotalBytes = 0;
280
281 s.Source = this.OpenArchive(streamContext, this.currentArchiveNumber);
282
283 FileStream archiveFileStream = s.Source as FileStream;
284 this.currentArchiveName = (archiveFileStream != null ?
285 Path.GetFileName(archiveFileStream.Name) : null);
286
287 this.currentArchiveTotalBytes = s.Source.Length;
288 this.currentArchiveNumber--;
289 this.OnProgress(ArchiveProgressType.StartArchive);
290 this.currentArchiveNumber++;
291 });
292
293 concatStream.Source = archiveStream;
294 concatStream.SetLength(compressedSize);
295
296 Stream decompressionStream = compressionStreamCreator(concatStream);
297
298 try
299 {
300 byte[] buf = new byte[4096];
301 long bytesRemaining = uncompressedSize;
302 int counter = 0;
303 while (bytesRemaining > 0)
304 {
305 int count = (int) Math.Min(buf.Length, bytesRemaining);
306 count = decompressionStream.Read(buf, 0, count);
307 crcStream.Write(buf, 0, count);
308 bytesRemaining -= count;
309
310 this.fileBytesProcessed += count;
311 this.currentFileBytesProcessed += count;
312 this.currentArchiveBytesProcessed = concatStream.Source.Position;
313
314 if (++counter % 16 == 0) // Report every 64K
315 {
316 this.currentArchiveNumber--;
317 this.OnProgress(ArchiveProgressType.PartialFile);
318 this.currentArchiveNumber++;
319 }
320 }
321 }
322 finally
323 {
324 archiveStream = concatStream.Source;
325 }
326
327 crcStream.Flush();
328
329 if (crcStream.Crc != crc)
330 {
331 throw new ZipException("CRC check failed for file: " + fileName);
332 }
333 }
334 }
335}
336
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>