1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
|
// 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.
namespace WixToolset.Dtf.Compression
{
using System;
using System.IO;
using System.Collections.Generic;
/// <summary>
/// Provides a basic implementation of the archive pack and unpack stream context
/// interfaces, based on a list of archive files, a default directory, and an
/// optional mapping from internal to external file paths.
/// </summary>
/// <remarks>
/// This class can also handle creating or extracting chained archive packages.
/// </remarks>
public class ArchiveFileStreamContext
: IPackStreamContext, IUnpackStreamContext
{
private IList<string> archiveFiles;
private string directory;
private IDictionary<string, string> files;
private bool extractOnlyNewerFiles;
private bool enableOffsetOpen;
#region Constructors
/// <summary>
/// Creates a new ArchiveFileStreamContext with a archive file and
/// no default directory or file mapping.
/// </summary>
/// <param name="archiveFile">The path to a archive file that will be
/// created or extracted.</param>
public ArchiveFileStreamContext(string archiveFile)
: this(archiveFile, null, null)
{
}
/// <summary>
/// Creates a new ArchiveFileStreamContext with a archive file, default
/// directory and mapping from internal to external file paths.
/// </summary>
/// <param name="archiveFile">The path to a archive file that will be
/// created or extracted.</param>
/// <param name="directory">The default root directory where files will be
/// located, optional.</param>
/// <param name="files">A mapping from internal file paths to external file
/// paths, optional.</param>
/// <remarks>
/// If the mapping is not null and a file is not included in the mapping,
/// the file will be skipped.
/// <para>If the external path in the mapping is a simple file name or
/// relative file path, it will be concatenated onto the default directory,
/// if one was specified.</para>
/// <para>For more about how the default directory and files mapping are
/// used, see <see cref="OpenFileReadStream"/> and
/// <see cref="OpenFileWriteStream"/>.</para>
/// </remarks>
public ArchiveFileStreamContext(
string archiveFile,
string directory,
IDictionary<string, string> files)
: this(new string[] { archiveFile }, directory, files)
{
if (archiveFile == null)
{
throw new ArgumentNullException("archiveFile");
}
}
/// <summary>
/// Creates a new ArchiveFileStreamContext with a list of archive files,
/// a default directory and a mapping from internal to external file paths.
/// </summary>
/// <param name="archiveFiles">A list of paths to archive files that will be
/// created or extracted.</param>
/// <param name="directory">The default root directory where files will be
/// located, optional.</param>
/// <param name="files">A mapping from internal file paths to external file
/// paths, optional.</param>
/// <remarks>
/// When creating chained archives, the <paramref name="archiveFiles"/> list
/// should include at least enough archives to handle the entire set of
/// input files, based on the maximum archive size that is passed to the
/// <see cref="CompressionEngine"/>.<see
/// cref="CompressionEngine.Pack(IPackStreamContext,IEnumerable<string>,long)"/>.
/// <para>If the mapping is not null and a file is not included in the mapping,
/// the file will be skipped.</para>
/// <para>If the external path in the mapping is a simple file name or
/// relative file path, it will be concatenated onto the default directory,
/// if one was specified.</para>
/// <para>For more about how the default directory and files mapping are used,
/// see <see cref="OpenFileReadStream"/> and
/// <see cref="OpenFileWriteStream"/>.</para>
/// </remarks>
public ArchiveFileStreamContext(
IList<string> archiveFiles,
string directory,
IDictionary<string, string> files)
{
if (archiveFiles == null || archiveFiles.Count == 0)
{
throw new ArgumentNullException("archiveFiles");
}
this.archiveFiles = archiveFiles;
this.directory = directory;
this.files = files;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the list of archive files that are created or extracted.
/// </summary>
/// <value>The list of archive files that are created or extracted.</value>
public IList<string> ArchiveFiles
{
get
{
return this.archiveFiles;
}
}
/// <summary>
/// Gets or sets the default root directory where files are located.
/// </summary>
/// <value>The default root directory where files are located.</value>
/// <remarks>
/// For details about how the default directory is used,
/// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>.
/// </remarks>
public string Directory
{
get
{
return this.directory;
}
}
/// <summary>
/// Gets or sets the mapping from internal file paths to external file paths.
/// </summary>
/// <value>A mapping from internal file paths to external file paths.</value>
/// <remarks>
/// For details about how the files mapping is used,
/// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>.
/// </remarks>
public IDictionary<string, string> Files
{
get
{
return this.files;
}
}
/// <summary>
/// Gets or sets a flag that can prevent extracted files from overwriting
/// newer files that already exist.
/// </summary>
/// <value>True to prevent overwriting newer files that already exist
/// during extraction; false to always extract from the archive regardless
/// of existing files.</value>
public bool ExtractOnlyNewerFiles
{
get
{
return this.extractOnlyNewerFiles;
}
set
{
this.extractOnlyNewerFiles = value;
}
}
/// <summary>
/// Gets or sets a flag that enables creating or extracting an archive
/// at an offset within an existing file. (This is typically used to open
/// archive-based self-extracting packages.)
/// </summary>
/// <value>True to search an existing package file for an archive offset
/// or the end of the file;/ false to always create or open a plain
/// archive file.</value>
public bool EnableOffsetOpen
{
get
{
return this.enableOffsetOpen;
}
set
{
this.enableOffsetOpen = value;
}
}
#endregion
#region IPackStreamContext Members
/// <summary>
/// Gets the name of the archive with a specified number.
/// </summary>
/// <param name="archiveNumber">The 0-based index of the archive within
/// the chain.</param>
/// <returns>The name of the requested archive. May be an empty string
/// for non-chained archives, but may never be null.</returns>
/// <remarks>This method returns the file name of the archive from the
/// <see cref="archiveFiles"/> list with the specified index, or an empty
/// string if the archive number is outside the bounds of the list. The
/// file name should not include any directory path.</remarks>
public virtual string GetArchiveName(int archiveNumber)
{
if (archiveNumber < this.archiveFiles.Count)
{
return Path.GetFileName(this.archiveFiles[archiveNumber]);
}
return String.Empty;
}
/// <summary>
/// Opens a stream for writing an archive.
/// </summary>
/// <param name="archiveNumber">The 0-based index of the archive within
/// the chain.</param>
/// <param name="archiveName">The name of the archive that was returned
/// by <see cref="GetArchiveName"/>.</param>
/// <param name="truncate">True if the stream should be truncated when
/// opened (if it already exists); false if an existing stream is being
/// re-opened for writing additional data.</param>
/// <param name="compressionEngine">Instance of the compression engine
/// doing the operations.</param>
/// <returns>A writable Stream where the compressed archive bytes will be
/// written, or null to cancel the archive creation.</returns>
/// <remarks>
/// This method opens the file from the <see cref="ArchiveFiles"/> list
/// with the specified index. If the archive number is outside the bounds
/// of the list, this method returns null.
/// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method
/// will seek to the start of any existing archive in the file, or to the
/// end of the file if the existing file is not an archive.</para>
/// </remarks>
public virtual Stream OpenArchiveWriteStream(
int archiveNumber,
string archiveName,
bool truncate,
CompressionEngine compressionEngine)
{
if (archiveNumber >= this.archiveFiles.Count)
{
return null;
}
if (String.IsNullOrEmpty(archiveName))
{
throw new ArgumentNullException("archiveName");
}
// All archives must be in the same directory,
// so always use the directory from the first archive.
string archiveFile = Path.Combine(
Path.GetDirectoryName(this.archiveFiles[0]), archiveName);
Stream stream = File.Open(
archiveFile,
(truncate ? FileMode.OpenOrCreate : FileMode.Open),
FileAccess.ReadWrite);
if (this.enableOffsetOpen)
{
long offset = compressionEngine.FindArchiveOffset(
new DuplicateStream(stream));
// If this is not an archive file, append the archive to it.
if (offset < 0)
{
offset = stream.Length;
}
if (offset > 0)
{
stream = new OffsetStream(stream, offset);
}
stream.Seek(0, SeekOrigin.Begin);
}
if (truncate)
{
// Truncate the stream, in case a larger old archive starts here.
stream.SetLength(0);
}
return stream;
}
/// <summary>
/// Closes a stream where an archive package was written.
/// </summary>
/// <param name="archiveNumber">The 0-based index of the archive within
/// the chain.</param>
/// <param name="archiveName">The name of the archive that was previously
/// returned by <see cref="GetArchiveName"/>.</param>
/// <param name="stream">A stream that was previously returned by
/// <see cref="OpenArchiveWriteStream"/> and is now ready to be closed.</param>
public virtual void CloseArchiveWriteStream(
int archiveNumber,
string archiveName,
Stream stream)
{
if (stream != null)
{
stream.Close();
FileStream fileStream = stream as FileStream;
if (fileStream != null)
{
string streamFile = fileStream.Name;
if (!String.IsNullOrEmpty(archiveName) &&
archiveName != Path.GetFileName(streamFile))
{
string archiveFile = Path.Combine(
Path.GetDirectoryName(this.archiveFiles[0]), archiveName);
if (File.Exists(archiveFile))
{
File.Delete(archiveFile);
}
File.Move(streamFile, archiveFile);
}
}
}
}
/// <summary>
/// Opens a stream to read a file that is to be included in an archive.
/// </summary>
/// <param name="path">The path of the file within the archive.</param>
/// <param name="attributes">The returned attributes of the opened file,
/// to be stored in the archive.</param>
/// <param name="lastWriteTime">The returned last-modified time of the
/// opened file, to be stored in the archive.</param>
/// <returns>A readable Stream where the file bytes will be read from
/// before they are compressed, or null to skip inclusion of the file and
/// continue to the next file.</returns>
/// <remarks>
/// This method opens a file using the following logic:
/// <list>
/// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping
/// are both null, the path is treated as relative to the current directory,
/// and that file is opened.</item>
/// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/>
/// mapping is null, the path is treated as relative to that directory, and
/// that file is opened.</item>
/// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/>
/// mapping is not null, the path parameter is used as a key into the mapping,
/// and the resulting value is the file path that is opened, relative to the
/// current directory (or it may be an absolute path). If no mapping exists,
/// the file is skipped.</item>
/// <item>If both the <see cref="Directory"/> and the <see cref="Files"/>
/// mapping are specified, the path parameter is used as a key into the
/// mapping, and the resulting value is the file path that is opened, relative
/// to the specified directory (or it may be an absolute path). If no mapping
/// exists, the file is skipped.</item>
/// </list>
/// </remarks>
public virtual Stream OpenFileReadStream(
string path, out FileAttributes attributes, out DateTime lastWriteTime)
{
string filePath = this.TranslateFilePath(path);
if (filePath == null)
{
attributes = FileAttributes.Normal;
lastWriteTime = DateTime.Now;
return null;
}
attributes = File.GetAttributes(filePath);
lastWriteTime = File.GetLastWriteTime(filePath);
return File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
/// <summary>
/// Closes a stream that has been used to read a file.
/// </summary>
/// <param name="path">The path of the file within the archive; the same as
/// the path provided when the stream was opened.</param>
/// <param name="stream">A stream that was previously returned by
/// <see cref="OpenFileReadStream"/> and is now ready to be closed.</param>
public virtual void CloseFileReadStream(string path, Stream stream)
{
if (stream != null)
{
stream.Close();
}
}
/// <summary>
/// Gets extended parameter information specific to the compression format
/// being used.
/// </summary>
/// <param name="optionName">Name of the option being requested.</param>
/// <param name="parameters">Parameters for the option; for per-file options,
/// the first parameter is typically the internal file path.</param>
/// <returns>Option value, or null to use the default behavior.</returns>
/// <remarks>
/// This implementation does not handle any options. Subclasses may override
/// this method to allow for non-default behavior.
/// </remarks>
public virtual object GetOption(string optionName, object[] parameters)
{
return null;
}
#endregion
#region IUnpackStreamContext Members
/// <summary>
/// Opens the archive stream for reading.
/// </summary>
/// <param name="archiveNumber">The zero-based index of the archive to
/// open.</param>
/// <param name="archiveName">The name of the archive being opened.</param>
/// <param name="compressionEngine">Instance of the compression engine
/// doing the operations.</param>
/// <returns>A stream from which archive bytes are read, or null to cancel
/// extraction of the archive.</returns>
/// <remarks>
/// This method opens the file from the <see cref="ArchiveFiles"/> list with
/// the specified index. If the archive number is outside the bounds of the
/// list, this method returns null.
/// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method will
/// seek to the start of any existing archive in the file, or to the end of
/// the file if the existing file is not an archive.</para>
/// </remarks>
public virtual Stream OpenArchiveReadStream(
int archiveNumber, string archiveName, CompressionEngine compressionEngine)
{
if (archiveNumber >= this.archiveFiles.Count)
{
return null;
}
string archiveFile = this.archiveFiles[archiveNumber];
Stream stream = File.Open(
archiveFile, FileMode.Open, FileAccess.Read, FileShare.Read);
if (this.enableOffsetOpen)
{
long offset = compressionEngine.FindArchiveOffset(
new DuplicateStream(stream));
if (offset > 0)
{
stream = new OffsetStream(stream, offset);
}
else
{
stream.Seek(0, SeekOrigin.Begin);
}
}
return stream;
}
/// <summary>
/// Closes a stream where an archive was read.
/// </summary>
/// <param name="archiveNumber">The archive number of the stream
/// to close.</param>
/// <param name="archiveName">The name of the archive being closed.</param>
/// <param name="stream">The stream that was previously returned by
/// <see cref="OpenArchiveReadStream"/> and is now ready to be closed.</param>
public virtual void CloseArchiveReadStream(
int archiveNumber, string archiveName, Stream stream)
{
if (stream != null)
{
stream.Close();
}
}
/// <summary>
/// Opens a stream for writing extracted file bytes.
/// </summary>
/// <param name="path">The path of the file within the archive.</param>
/// <param name="fileSize">The uncompressed size of the file to be
/// extracted.</param>
/// <param name="lastWriteTime">The last write time of the file to be
/// extracted.</param>
/// <returns>A stream where extracted file bytes are to be written, or null
/// to skip extraction of the file and continue to the next file.</returns>
/// <remarks>
/// This method opens a file using the following logic:
/// <list>
/// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping
/// are both null, the path is treated as relative to the current directory,
/// and that file is opened.</item>
/// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/>
/// mapping is null, the path is treated as relative to that directory, and
/// that file is opened.</item>
/// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/>
/// mapping is not null, the path parameter is used as a key into the mapping,
/// and the resulting value is the file path that is opened, relative to the
/// current directory (or it may be an absolute path). If no mapping exists,
/// the file is skipped.</item>
/// <item>If both the <see cref="Directory"/> and the <see cref="Files"/>
/// mapping are specified, the path parameter is used as a key into the
/// mapping, and the resulting value is the file path that is opened,
/// relative to the specified directory (or it may be an absolute path).
/// If no mapping exists, the file is skipped.</item>
/// </list>
/// <para>If the <see cref="ExtractOnlyNewerFiles"/> flag is set, the file
/// is skipped if a file currently exists in the same path with an equal
/// or newer write time.</para>
/// </remarks>
public virtual Stream OpenFileWriteStream(
string path,
long fileSize,
DateTime lastWriteTime)
{
string filePath = this.TranslateFilePath(path);
if (filePath == null)
{
return null;
}
FileInfo file = new FileInfo(filePath);
if (file.Exists)
{
if (this.extractOnlyNewerFiles && lastWriteTime != DateTime.MinValue)
{
if (file.LastWriteTime >= lastWriteTime)
{
return null;
}
}
// Clear attributes that will prevent overwriting the file.
// (The final attributes will be set after the file is unpacked.)
FileAttributes attributesToClear =
FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System;
if ((file.Attributes & attributesToClear) != 0)
{
file.Attributes &= ~attributesToClear;
}
}
if (!file.Directory.Exists)
{
file.Directory.Create();
}
return File.Open(
filePath, FileMode.Create, FileAccess.Write, FileShare.None);
}
/// <summary>
/// Closes a stream where an extracted file was written.
/// </summary>
/// <param name="path">The path of the file within the archive.</param>
/// <param name="stream">The stream that was previously returned by
/// <see cref="OpenFileWriteStream"/> and is now ready to be closed.</param>
/// <param name="attributes">The attributes of the extracted file.</param>
/// <param name="lastWriteTime">The last write time of the file.</param>
/// <remarks>
/// After closing the extracted file stream, this method applies the date
/// and attributes to that file.
/// </remarks>
public virtual void CloseFileWriteStream(
string path,
Stream stream,
FileAttributes attributes,
DateTime lastWriteTime)
{
if (stream != null)
{
stream.Close();
}
string filePath = this.TranslateFilePath(path);
if (filePath != null)
{
FileInfo file = new FileInfo(filePath);
if (lastWriteTime != DateTime.MinValue)
{
try
{
file.LastWriteTime = lastWriteTime;
}
catch (ArgumentException)
{
}
catch (IOException)
{
}
}
try
{
file.Attributes = attributes;
}
catch (IOException)
{
}
}
}
#endregion
#region Private utility methods
/// <summary>
/// Translates an internal file path to an external file path using the
/// <see cref="Directory"/> and the <see cref="Files"/> mapping, according to
/// rules documented in <see cref="OpenFileReadStream"/> and
/// <see cref="OpenFileWriteStream"/>.
/// </summary>
/// <param name="path">The path of the file with the archive.</param>
/// <returns>The external path of the file, or null if there is no
/// valid translation.</returns>
private string TranslateFilePath(string path)
{
string filePath;
if (this.files != null)
{
filePath = this.files[path];
}
else
{
this.ValidateArchivePath(path);
filePath = path;
}
if (filePath != null)
{
if (this.directory != null)
{
filePath = Path.Combine(this.directory, filePath);
}
}
return filePath;
}
private void ValidateArchivePath(string filePath)
{
string basePath = Path.GetFullPath(String.IsNullOrEmpty(this.directory) ? Environment.CurrentDirectory : this.directory);
string path = Path.GetFullPath(Path.Combine(basePath, filePath));
if (!path.StartsWith(basePath, StringComparison.InvariantCultureIgnoreCase))
{
throw new InvalidDataException("Archive cannot contain files with absolute or traversal paths.");
}
}
#endregion
}
}
|