aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs478
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
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}