diff options
Diffstat (limited to '')
-rw-r--r-- | src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs | 478 |
1 files changed, 478 insertions, 0 deletions
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 | |||
3 | namespace 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 | } | ||