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