aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs489
1 files changed, 489 insertions, 0 deletions
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}