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