aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller.Package
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs1169
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs1073
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs259
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs154
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj23
5 files changed, 2678 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs
new file mode 100644
index 00000000..276732b7
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs
@@ -0,0 +1,1169 @@
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.WindowsInstaller.Package
4{
5using System;
6using System.IO;
7using System.Text;
8using System.Collections;
9using System.Collections.Generic;
10using System.Diagnostics.CodeAnalysis;
11using System.Globalization;
12using System.Text.RegularExpressions;
13using WixToolset.Dtf.Compression;
14using WixToolset.Dtf.Compression.Cab;
15
16/// <summary>
17/// Handles status messages generated when operations are performed on an
18/// <see cref="InstallPackage"/> or <see cref="PatchPackage"/>.
19/// </summary>
20/// <example>
21/// <c>installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
22/// </example>
23public delegate void InstallPackageMessageHandler(string format, params object[] args);
24
25/// <summary>
26/// Provides access to powerful build, maintenance, and analysis operations on an
27/// installation package (.MSI or .MSM).
28/// </summary>
29public class InstallPackage : Database
30{
31 private string cabName;
32 private string cabMsg;
33
34 /// <summary>
35 /// Creates a new InstallPackage object. The file source directory and working
36 /// directory are the same as the location as the package file.
37 /// </summary>
38 /// <param name="packagePath">Path to the install package to be created or opened</param>
39 /// <param name="openMode">Open mode for the database</param>
40 public InstallPackage(string packagePath, DatabaseOpenMode openMode)
41 : this(packagePath, openMode, null, null)
42 {
43 }
44 /// <summary>
45 /// Creates a new InstallPackage object, specifying an alternate file source
46 /// directory and/or working directory.
47 /// </summary>
48 /// <param name="packagePath">Path to the install package to be created or opened</param>
49 /// <param name="openMode">Open mode for the database</param>
50 /// <param name="sourceDir">Location to obtain source files and cabinets when extracting
51 /// or updating files in the working directory. This is often the location of an original
52 /// copy of the package that is not meant to be modified. If this parameter is null, it
53 /// defaults to the directory of <paramref name="packagePath"/>.</param>
54 /// <param name="workingDir">Location where files will be extracted to/updated from. Also
55 /// the location where a temporary folder is created during some operations. If this
56 /// parameter is null, it defaults to the directory of <paramref name="packagePath"/>.</param>
57 /// <remarks>If the source location is different than the working directory, then
58 /// no files will be modified at the source location.
59 /// </remarks>
60 public InstallPackage(string packagePath, DatabaseOpenMode openMode,
61 string sourceDir, string workingDir) : base(packagePath, openMode)
62 {
63 this.sourceDir = (sourceDir != null ? sourceDir : Path.GetDirectoryName(packagePath));
64 this.workingDir = (workingDir != null ? workingDir : Path.GetDirectoryName(packagePath));
65 this.compressionLevel = CompressionLevel.Normal;
66
67 this.DeleteOnClose(this.TempDirectory);
68 }
69
70 /// <summary>
71 /// Handle this event to receive status messages when operations are performed
72 /// on the install package.
73 /// </summary>
74 /// <example>
75 /// <c>installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
76 /// </example>
77 public event InstallPackageMessageHandler Message;
78
79 /// <summary>
80 /// Sends a message to the <see cref="Message"/> event-handler.
81 /// </summary>
82 /// <param name="format">Message string, containing 0 or more format items</param>
83 /// <param name="args">Items to be formatted</param>
84 protected void LogMessage(string format, params object[] args)
85 {
86 if(this.Message != null)
87 {
88 this.Message(format, args);
89 }
90 }
91
92 /// <summary>
93 /// Gets or sets the location to obtain source files and cabinets when
94 /// extracting or updating files in the working directory. This is often
95 /// the location of an original copy of the package that is not meant
96 /// to be modified.
97 /// </summary>
98 public string SourceDirectory
99 {
100 get { return this.sourceDir; }
101 set { this.sourceDir = value; }
102 }
103 private string sourceDir;
104
105 /// <summary>
106 /// Gets or sets the location where files will be extracted to/updated from. Also
107 /// the location where a temporary folder is created during some operations.
108 /// </summary>
109 public string WorkingDirectory
110 {
111 get { return this.workingDir; }
112 set { this.workingDir = value; }
113 }
114 private string workingDir;
115
116 private const string TEMP_DIR_NAME = "WITEMP";
117
118 private string TempDirectory
119 {
120 get { return Path.Combine(this.WorkingDirectory, TEMP_DIR_NAME); }
121 }
122
123 /// <summary>
124 /// Gets the list of file keys that have the specified long file name.
125 /// </summary>
126 /// <param name="longFileName">File name to search for (case-insensitive)</param>
127 /// <returns>Array of file keys, or a 0-length array if none are found</returns>
128 [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
129 public string[] FindFiles(string longFileName)
130 {
131 longFileName = longFileName.ToLowerInvariant();
132 ArrayList fileList = new ArrayList();
133 foreach(KeyValuePair<string, InstallPath> entry in this.Files)
134 {
135 if(((InstallPath) entry.Value).TargetName.ToLowerInvariant()
136 == longFileName)
137 {
138 fileList.Add(entry.Key);
139 }
140 }
141 return (string[]) fileList.ToArray(typeof(string));
142 }
143
144 /// <summary>
145 /// Gets the list of file keys whose long file names match a specified
146 /// regular-expression search pattern.
147 /// </summary>
148 /// <param name="pattern">Regular expression search pattern</param>
149 /// <returns>Array of file keys, or a 0-length array if none are found</returns>
150 public string[] FindFiles(Regex pattern)
151 {
152 ArrayList fileList = new ArrayList();
153 foreach (KeyValuePair<string, InstallPath> entry in this.Files)
154 {
155 if(pattern.IsMatch(((InstallPath) entry.Value).TargetName))
156 {
157 fileList.Add(entry.Key);
158 }
159 }
160 return (string[]) fileList.ToArray(typeof(string));
161 }
162
163 /// <summary>
164 /// Extracts all files to the <see cref="WorkingDirectory"/>. The files are extracted
165 /// to the relative directory matching their <see cref="InstallPath.SourcePath"/>.
166 /// </summary>
167 /// <remarks>If any files have the uncompressed attribute, they will be copied
168 /// from the <see cref="SourceDirectory"/>.</remarks>
169 public void ExtractFiles()
170 {
171 this.ExtractFiles(null);
172 }
173 /// <summary>
174 /// Extracts a specified list of files to the <see cref="WorkingDirectory"/>. The files
175 /// are extracted to the relative directory matching their <see cref="InstallPath.SourcePath"/>.
176 /// </summary>
177 /// <param name="fileKeys">List of file key strings to extract</param>
178 /// <remarks>If any files have the uncompressed attribute, they will be copied
179 /// from the <see cref="SourceDirectory"/>.</remarks>
180 public void ExtractFiles(ICollection<string> fileKeys)
181 {
182 this.ProcessFilesByMediaDisk(fileKeys,
183 new ProcessFilesOnOneMediaDiskHandler(this.ExtractFilesOnOneMediaDisk));
184 }
185
186 private bool IsMergeModule()
187 {
188 return this.CountRows("Media", "`LastSequence` >= 0") == 0 &&
189 this.CountRows("_Streams", "`Name` = 'MergeModule.CABinet'") != 0;
190 }
191
192 private delegate void ProcessFilesOnOneMediaDiskHandler(string mediaCab,
193 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap);
194
195 private void ProcessFilesByMediaDisk(ICollection<string> fileKeys,
196 ProcessFilesOnOneMediaDiskHandler diskHandler)
197 {
198 if(this.IsMergeModule())
199 {
200 InstallPathMap files = new InstallPathMap();
201 foreach(string fileKey in this.Files.Keys)
202 {
203 if(fileKeys == null || fileKeys.Contains(fileKey))
204 {
205 files[fileKey] = this.Files[fileKey];
206 }
207 }
208 diskHandler("#MergeModule.CABinet", files, new InstallPathMap());
209 }
210 else
211 {
212 bool defaultCompressed = ((this.SummaryInfo.WordCount & 0x2) != 0);
213
214 View fileView = null, mediaView = null;
215 Record fileRec = null;
216 try
217 {
218 fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
219 "FROM `File` ORDER BY `Sequence`");
220 mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " +
221 "FROM `Media` ORDER BY `DiskId`");
222 fileView.Execute();
223 mediaView.Execute();
224
225 int currentMediaDiskId = -1;
226 int currentMediaMaxSequence = -1;
227 string currentMediaCab = null;
228 InstallPathMap compressedFileMap = new InstallPathMap();
229 InstallPathMap uncompressedFileMap = new InstallPathMap();
230
231 while((fileRec = fileView.Fetch()) != null)
232 {
233 string fileKey = (string) fileRec[1];
234
235 if(fileKeys == null || fileKeys.Contains(fileKey))
236 {
237 int fileAttributes = fileRec.GetInteger(2);
238 int fileSequence = fileRec.GetInteger(3);
239
240 InstallPath fileInstallPath = this.Files[fileKey];
241 if(fileInstallPath == null)
242 {
243 this.LogMessage("Could not get install path for source file: {0}", fileKey);
244 throw new InstallerException("Could not get install path for source file: " + fileKey);
245 }
246
247 if(fileSequence > currentMediaMaxSequence)
248 {
249 if(currentMediaDiskId != -1)
250 {
251 diskHandler(currentMediaCab,
252 compressedFileMap, uncompressedFileMap);
253 compressedFileMap.Clear();
254 uncompressedFileMap.Clear();
255 }
256
257 while(fileSequence > currentMediaMaxSequence)
258 {
259 Record mediaRec = mediaView.Fetch();
260 if(mediaRec == null)
261 {
262 currentMediaDiskId = -1;
263 break;
264 }
265 using(mediaRec)
266 {
267 currentMediaDiskId = mediaRec.GetInteger(1);
268 currentMediaMaxSequence = mediaRec.GetInteger(2);
269 currentMediaCab = (string) mediaRec[3];
270 }
271 }
272 if(fileSequence > currentMediaMaxSequence) break;
273 }
274
275 if((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed) != 0)
276 {
277 compressedFileMap[fileKey] = fileInstallPath;
278 }
279 else if ((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed) != 0)
280 {
281 // Non-compressed files are located
282 // in the same directory as the MSI, without any path.
283 uncompressedFileMap[fileKey] = new InstallPath(fileInstallPath.SourceName);
284 }
285 else if(defaultCompressed)
286 {
287 compressedFileMap[fileKey] = fileInstallPath;
288 }
289 else
290 {
291 uncompressedFileMap[fileKey] = fileInstallPath;
292 }
293 }
294 fileRec.Close();
295 fileRec = null;
296 }
297 if(currentMediaDiskId != -1)
298 {
299 diskHandler(currentMediaCab,
300 compressedFileMap, uncompressedFileMap);
301 }
302 }
303 finally
304 {
305 if (fileRec != null) fileRec.Close();
306 if (fileView != null) fileView.Close();
307 if (mediaView != null) mediaView.Close();
308 }
309 }
310 }
311
312 [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
313 private void ExtractFilesOnOneMediaDisk(string mediaCab,
314 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
315 {
316 if(compressedFileMap.Count > 0)
317 {
318 string cabFile = null;
319 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
320 {
321 mediaCab = mediaCab.Substring(1);
322
323 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
324 "WHERE `Name` = '{0}'", mediaCab))
325 {
326 streamView.Execute();
327 Record streamRec = streamView.Fetch();
328 if(streamRec == null)
329 {
330 this.LogMessage("Stream not found: {0}", mediaCab);
331 throw new InstallerException("Stream not found: " + mediaCab);
332 }
333 using(streamRec)
334 {
335 this.LogMessage("extract cab {0}", mediaCab);
336 Directory.CreateDirectory(this.TempDirectory);
337 cabFile = Path.Combine(this.TempDirectory,
338 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
339 streamRec.GetStream("Data", cabFile);
340 }
341 }
342 }
343 else
344 {
345 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
346 }
347
348 this.cabName = mediaCab;
349 this.cabMsg = "extract {0}\\{1} {2}";
350 new CabInfo(cabFile).UnpackFileSet(compressedFileMap.SourcePaths, this.WorkingDirectory,
351 this.CabinetProgress);
352 ClearReadOnlyAttribute(this.WorkingDirectory, compressedFileMap.Values);
353 }
354 foreach(InstallPath fileInstallPath in uncompressedFileMap.Values)
355 {
356 string sourcePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath);
357 string extractPath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
358 if(Path.GetFullPath(sourcePath).ToLowerInvariant() !=
359 Path.GetFullPath(extractPath).ToLowerInvariant())
360 {
361 if(!File.Exists(sourcePath))
362 {
363 this.LogMessage("Error: Uncompressed file not found: {0}", sourcePath);
364 throw new FileNotFoundException("Uncompressed file not found.", sourcePath);
365 }
366 else
367 {
368 this.LogMessage("copy {0} {1}", sourcePath, extractPath);
369 Directory.CreateDirectory(Path.GetDirectoryName(extractPath));
370 File.Copy(sourcePath, extractPath, true);
371 }
372 }
373 else
374 {
375 if(!File.Exists(extractPath))
376 {
377 this.LogMessage("Error: Uncompressed file not found: {0}", extractPath);
378 throw new FileNotFoundException("Uncompressed file not found.", extractPath);
379 }
380 }
381 }
382 }
383
384 private void CabinetProgress(object sender, ArchiveProgressEventArgs e)
385 {
386 switch(e.ProgressType)
387 {
388 case ArchiveProgressType.StartFile:
389 {
390 string filePath = e.CurrentFileName;
391 if(this.filePathMap != null)
392 {
393 InstallPath fileInstallPath = this.Files[e.CurrentFileName];
394 if(fileInstallPath != null)
395 {
396 filePath = fileInstallPath.SourcePath;
397 }
398 }
399 this.LogMessage(this.cabMsg, this.cabName, e.CurrentFileName,
400 Path.Combine(this.WorkingDirectory, filePath));
401 }
402 break;
403 }
404 }
405
406 /// <summary>
407 /// Updates the install package with new files from the <see cref="WorkingDirectory"/>. The
408 /// files must be in the relative directory matching their <see cref="InstallPath.SourcePath"/>.
409 /// This method re-compresses and packages the files if necessary, and also updates the
410 /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart*
411 /// </summary>
412 /// <remarks>
413 /// The cabinet compression level used during re-cabbing can be configured with the
414 /// <see cref="CompressionLevel"/> property.
415 /// </remarks>
416 public void UpdateFiles()
417 {
418 this.UpdateFiles(null);
419 }
420 /// <summary>
421 /// Updates the install package with new files from the <see cref="WorkingDirectory"/>. The
422 /// files must be in the relative directory matching their <see cref="InstallPath.SourcePath"/>.
423 /// This method re-compresses and packages the files if necessary, and also updates the
424 /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart?.
425 /// </summary>
426 /// <param name="fileKeys">List of file key strings to update</param>
427 /// <remarks>
428 /// This method does not change the media structure of the package, so it may require extracting
429 /// and re-compressing a large cabinet just to update one file.
430 /// <p>The cabinet compression level used during re-cabbing can be configured with the
431 /// <see cref="CompressionLevel"/> property.</p>
432 /// </remarks>
433 public void UpdateFiles(ICollection<string> fileKeys)
434 {
435 this.ProcessFilesByMediaDisk(fileKeys,
436 new ProcessFilesOnOneMediaDiskHandler(this.UpdateFilesOnOneMediaDisk));
437 }
438
439 private void UpdateFilesOnOneMediaDisk(string mediaCab,
440 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
441 {
442 if(compressedFileMap.Count > 0)
443 {
444 string cabFile = null;
445 bool cabFileIsTemp = false;
446 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
447 {
448 cabFileIsTemp = true;
449 mediaCab = mediaCab.Substring(1);
450
451 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
452 "WHERE `Name` = '{0}'", mediaCab))
453 {
454 streamView.Execute();
455 Record streamRec = streamView.Fetch();
456 if(streamRec == null)
457 {
458 this.LogMessage("Stream not found: {0}", mediaCab);
459 throw new InstallerException("Stream not found: " + mediaCab);
460 }
461 using(streamRec)
462 {
463 this.LogMessage("extract cab {0}", mediaCab);
464 Directory.CreateDirectory(this.TempDirectory);
465 cabFile = Path.Combine(this.TempDirectory,
466 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
467 streamRec.GetStream("Data", cabFile);
468 }
469 }
470 }
471 else
472 {
473 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
474 }
475
476 CabInfo cab = new CabInfo(cabFile);
477 ArrayList fileKeyList = new ArrayList();
478 foreach (CabFileInfo fileInCab in cab.GetFiles())
479 {
480 string fileKey = fileInCab.Name;
481 if(this.Files[fileKey] != null)
482 {
483 fileKeyList.Add(fileKey);
484 }
485 }
486 string[] fileKeys = (string[]) fileKeyList.ToArray(typeof(string));
487
488 Directory.CreateDirectory(this.TempDirectory);
489
490 ArrayList remainingFileKeys = new ArrayList(fileKeys);
491 foreach(string fileKey in fileKeys)
492 {
493 InstallPath fileInstallPath = compressedFileMap[fileKey];
494 if(fileInstallPath != null)
495 {
496 UpdateFileStats(fileKey, fileInstallPath);
497
498 string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
499 this.LogMessage("copy {0} {1}", filePath, fileKey);
500 File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey), true);
501 remainingFileKeys.Remove(fileKey);
502 }
503 }
504
505 if(remainingFileKeys.Count > 0)
506 {
507 this.cabName = mediaCab;
508 this.cabMsg = "extract {0}\\{1}";
509 string[] remainingFileKeysArray = (string[]) remainingFileKeys.ToArray(typeof(string));
510 cab.UnpackFiles(remainingFileKeysArray, this.TempDirectory, remainingFileKeysArray,
511 this.CabinetProgress);
512 }
513
514 ClearReadOnlyAttribute(this.TempDirectory, fileKeys);
515
516 if(!cabFileIsTemp)
517 {
518 cab = new CabInfo(Path.Combine(this.WorkingDirectory, mediaCab));
519 }
520 this.cabName = mediaCab;
521 this.cabMsg = "compress {0}\\{1}";
522 cab.PackFiles(this.TempDirectory, fileKeys, fileKeys,
523 this.CompressionLevel, this.CabinetProgress);
524
525 if(cabFileIsTemp)
526 {
527 using (Record streamRec = new Record(1))
528 {
529 streamRec.SetStream(1, cabFile);
530 this.Execute(String.Format(
531 "UPDATE `_Streams` SET `Data` = ? WHERE `Name` = '{0}'", mediaCab),
532 streamRec);
533 }
534 }
535 }
536
537 foreach (KeyValuePair<string, InstallPath> entry in uncompressedFileMap)
538 {
539 UpdateFileStats((string) entry.Key, (InstallPath) entry.Value);
540 }
541 }
542
543 private void UpdateFileStats(string fileKey, InstallPath fileInstallPath)
544 {
545 string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
546 if(!File.Exists(filePath))
547 {
548 this.LogMessage("Updated source file not found: {0}", filePath);
549 throw new FileNotFoundException("Updated source file not found: " + filePath);
550 }
551
552 this.LogMessage("updatestats {0}", fileKey);
553
554 string version = Installer.GetFileVersion(filePath);
555 string language = Installer.GetFileLanguage(filePath);
556 long size = new FileInfo(filePath).Length;
557
558 this.Execute("UPDATE `File` SET `Version` = '{0}', `Language` = '{1}', " +
559 "`FileSize` = {2} WHERE `File` = '{3}'", version, language, size, fileKey);
560
561 if ((version == null || version.Length == 0) && this.Tables.Contains("MsiFileHash"))
562 {
563 int[] hash = new int[4];
564 Installer.GetFileHash(filePath, hash);
565 this.Execute("DELETE FROM `MsiFileHash` WHERE `File_` = '{0}'", fileKey);
566 this.Execute("INSERT INTO `MsiFileHash` (`File_`, `Options`, `HashPart1`, `HashPart2`, " +
567 "`HashPart3`, `HashPart4`) VALUES ('" + fileKey + "', 0, {0}, {1}, {2}, {3})",
568 hash[0], hash[1], hash[2], hash[3]);
569 }
570 }
571
572 /// <summary>
573 /// Consolidates a package by combining and re-compressing all files into a single
574 /// internal or external cabinet.
575 /// </summary>
576 /// <param name="mediaCabinet"></param>
577 /// <remarks>If an installation package was built from many merge modules, this
578 /// method can somewhat decrease package size, complexity, and installation time.
579 /// <p>This method will also convert a package with all or mostly uncompressed
580 /// files into a package where all files are compressed.</p>
581 /// <p>If the package contains any not-yet-applied binary file patches (for
582 /// example, a package generated by a call to <see cref="ApplyPatch"/>) then
583 /// this method will apply the patches before compressing the updated files.</p>
584 /// <p>This method edits the database summary information and the File, Media
585 /// and Patch tables as necessary to maintain a valid installation package.</p>
586 /// <p>The cabinet compression level used during re-cabbing can be configured with the
587 /// <see cref="CompressionLevel"/> property.</p>
588 /// </remarks>
589 public void Consolidate(string mediaCabinet)
590 {
591 this.LogMessage("Consolidating package");
592
593 Directory.CreateDirectory(this.TempDirectory);
594
595 this.LogMessage("Extracting/preparing files");
596 this.ProcessFilesByMediaDisk(null,
597 new ProcessFilesOnOneMediaDiskHandler(this.PrepareOneMediaDiskForConsolidation));
598
599 this.LogMessage("Applying any file patches");
600 ApplyFilePatchesForConsolidation();
601
602 this.LogMessage("Clearing PatchPackage, Patch, MsiPatchHeaders tables");
603 if (this.Tables.Contains("PatchPackage"))
604 {
605 this.Execute("DELETE FROM `PatchPackage` WHERE `PatchId` <> ''");
606 }
607 if (this.Tables.Contains("Patch"))
608 {
609 this.Execute("DELETE FROM `Patch` WHERE `File_` <> ''");
610 }
611 if (this.Tables.Contains("MsiPatchHeaders"))
612 {
613 this.Execute("DELETE FROM `MsiPatchHeaders` WHERE `StreamRef` <> ''");
614 }
615
616 this.LogMessage("Resequencing files");
617 ArrayList files = new ArrayList();
618 using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
619 "FROM `File` ORDER BY `Sequence`"))
620 {
621 fileView.Execute();
622
623 foreach (Record fileRec in fileView) using(fileRec)
624 {
625 files.Add(fileRec[1]);
626 int fileAttributes = fileRec.GetInteger(2);
627 fileAttributes &= ~(int) (WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed
628 | WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed | WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded);
629 fileRec[2] = fileAttributes;
630 fileRec[3] = files.Count;
631 fileView.Update(fileRec);
632 }
633 }
634
635 bool internalCab = false;
636 if(mediaCabinet.StartsWith("#", StringComparison.Ordinal))
637 {
638 internalCab = true;
639 mediaCabinet = mediaCabinet.Substring(1);
640 }
641
642 this.LogMessage("Cabbing files");
643 string[] fileKeys = (string[]) files.ToArray(typeof(string));
644 string cabPath = Path.Combine(internalCab ? this.TempDirectory
645 : this.WorkingDirectory, mediaCabinet);
646 this.cabName = mediaCabinet;
647 this.cabMsg = "compress {0}\\{1}";
648 new CabInfo(cabPath).PackFiles(this.TempDirectory, fileKeys,
649 fileKeys, this.CompressionLevel, this.CabinetProgress);
650
651 this.DeleteEmbeddedCabs();
652
653 if(internalCab)
654 {
655 this.LogMessage("Inserting cab stream into MSI");
656 Record cabRec = new Record(1);
657 cabRec.SetStream(1, cabPath);
658 this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)", cabRec);
659 }
660
661 this.LogMessage("Inserting cab media record into MSI");
662 this.Execute("DELETE FROM `Media` WHERE `DiskId` <> 0");
663 this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) " +
664 "VALUES (1, " + files.Count + ", '" + (internalCab ? "#" : "") + mediaCabinet + "')");
665
666
667 this.LogMessage("Setting compressed flag on package summary info");
668 this.SummaryInfo.WordCount = this.SummaryInfo.WordCount | 2;
669 this.SummaryInfo.Persist();
670 }
671
672 private void DeleteEmbeddedCabs()
673 {
674 using (View view = this.OpenView("SELECT `Cabinet` FROM `Media` WHERE `Cabinet` <> ''"))
675 {
676 view.Execute();
677
678 foreach (Record rec in view) using(rec)
679 {
680 string cab = rec.GetString(1);
681 if(cab.StartsWith("#", StringComparison.Ordinal))
682 {
683 cab = cab.Substring(1);
684 this.LogMessage("Deleting embedded cab stream: {0}", cab);
685 this.Execute("DELETE FROM `_Streams` WHERE `Name` = '{0}'", cab);
686 }
687 }
688 }
689 }
690
691 private void PrepareOneMediaDiskForConsolidation(string mediaCab,
692 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
693 {
694 if(compressedFileMap.Count > 0)
695 {
696 string cabFile = null;
697 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
698 {
699 mediaCab = mediaCab.Substring(1);
700
701 using (View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
702 "WHERE `Name` = '{0}'", mediaCab))
703 {
704 streamView.Execute();
705 Record streamRec = streamView.Fetch();
706 if(streamRec == null)
707 {
708 this.LogMessage("Stream not found: {0}", mediaCab);
709 throw new InstallerException("Stream not found: " + mediaCab);
710 }
711 using(streamRec)
712 {
713 this.LogMessage("extract cab {0}", mediaCab);
714 cabFile = Path.Combine(this.TempDirectory,
715 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
716 streamRec.GetStream("Data", cabFile);
717 }
718 }
719 }
720 else
721 {
722 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
723 }
724 string[] fileKeys = new string[compressedFileMap.Keys.Count];
725 compressedFileMap.Keys.CopyTo(fileKeys, 0);
726 this.cabName = mediaCab;
727 this.cabMsg = "extract {0}\\{1}";
728 new CabInfo(cabFile).UnpackFiles(fileKeys, this.TempDirectory, fileKeys,
729 this.CabinetProgress);
730 ClearReadOnlyAttribute(this.TempDirectory, fileKeys);
731 }
732 foreach (KeyValuePair<string, InstallPath> entry in uncompressedFileMap)
733 {
734 string fileKey = (string) entry.Key;
735 InstallPath fileInstallPath = (InstallPath) entry.Value;
736
737 string filePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath);
738 this.LogMessage("copy {0} {1}", filePath, fileKey);
739 File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey));
740 }
741 }
742
743 private void ClearReadOnlyAttribute(string baseDirectory, IEnumerable filePaths)
744 {
745 foreach(object filePath in filePaths)
746 {
747 string fullFilePath = Path.Combine(baseDirectory, filePath.ToString());
748 if (File.Exists(fullFilePath))
749 {
750 System.IO.FileAttributes fileAttributes = File.GetAttributes(fullFilePath);
751 if ((fileAttributes & System.IO.FileAttributes.ReadOnly) != 0)
752 {
753 fileAttributes &= ~System.IO.FileAttributes.ReadOnly;
754 File.SetAttributes(fullFilePath, fileAttributes);
755 }
756 }
757 }
758 }
759
760 private void ApplyFilePatchesForConsolidation()
761 {
762 if(this.Tables.Contains("Patch"))
763 {
764 using(View patchView = this.OpenView("SELECT `File_`, `Sequence` " +
765 "FROM `Patch` ORDER BY `Sequence`"))
766 {
767 patchView.Execute();
768 Hashtable extractedPatchCabs = new Hashtable();
769
770 foreach (Record patchRec in patchView) using(patchRec)
771 {
772 string fileKey = (string) patchRec[1];
773 int sequence = patchRec.GetInteger(2);
774 this.LogMessage("patch {0}", fileKey);
775
776 string tempPatchFile = Path.Combine(this.TempDirectory, fileKey + ".pat");
777 ExtractFilePatch(fileKey, sequence, tempPatchFile, extractedPatchCabs);
778 string filePath = Path.Combine(this.TempDirectory, fileKey);
779 string oldFilePath = filePath + ".old";
780 if(File.Exists(oldFilePath)) File.Delete(oldFilePath);
781 File.Move(filePath, oldFilePath);
782 Type.GetType("WixToolset.Dtf.WindowsInstaller.FilePatch")
783 .GetMethod("ApplyPatchToFile",
784 new Type[] { typeof(string), typeof(string), typeof(string) })
785 .Invoke(null, new object[] { tempPatchFile, oldFilePath, filePath });
786 }
787 }
788 }
789 }
790
791 private void ExtractFilePatch(string fileKey, int sequence, string extractPath,
792 IDictionary extractedCabs)
793 {
794 string mediaCab = null;
795 using(View mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " +
796 "FROM `Media` ORDER BY `DiskId`"))
797 {
798 mediaView.Execute();
799
800 foreach (Record mediaRec in mediaView) using(mediaRec)
801 {
802 int mediaMaxSequence = mediaRec.GetInteger(2);
803 if(mediaMaxSequence >= sequence)
804 {
805 mediaCab = mediaRec.GetString(3);
806 break;
807 }
808 }
809 }
810
811 if(mediaCab == null || mediaCab.Length == 0)
812 {
813 this.LogMessage("Could not find cabinet for file patch: {0}", fileKey);
814 throw new InstallerException("Could not find cabinet for file patch: " + fileKey);
815 }
816
817 if(!mediaCab.StartsWith("#", StringComparison.Ordinal))
818 {
819 this.LogMessage("Error: Patch cabinet {0} must be embedded", mediaCab);
820 throw new InstallerException("Patch cabinet " + mediaCab + " must be embedded.");
821 }
822 mediaCab = mediaCab.Substring(1);
823
824 string cabFile = (string) extractedCabs[mediaCab];
825 if(cabFile == null)
826 {
827 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
828 "WHERE `Name` = '{0}'", mediaCab))
829 {
830 streamView.Execute();
831 Record streamRec = streamView.Fetch();
832 if(streamRec == null)
833 {
834 this.LogMessage("Stream not found: {0}", mediaCab);
835 throw new InstallerException("Stream not found: " + mediaCab);
836 }
837 using(streamRec)
838 {
839 this.LogMessage("extract cab {0}", mediaCab);
840 Directory.CreateDirectory(this.TempDirectory);
841 cabFile = Path.Combine(this.TempDirectory,
842 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
843 streamRec.GetStream("Data", cabFile);
844 }
845 }
846 extractedCabs[mediaCab] = cabFile;
847 }
848
849 this.LogMessage("extract patch {0}\\{1}", mediaCab, fileKey);
850 new CabInfo(cabFile).UnpackFile(fileKey, extractPath);
851 }
852
853 /// <summary>
854 /// Rebuilds the cached directory structure information accessed by the
855 /// <see cref="Directories"/> and <see cref="Files"/> properties. This
856 /// should be done after modifying the File, Component, or Directory
857 /// tables, or else the cached information may no longer be accurate.
858 /// </summary>
859 public void UpdateDirectories()
860 {
861 this.dirPathMap = null;
862 this.filePathMap = InstallPathMap.BuildFilePathMap(this,
863 InstallPathMap.BuildComponentPathMap(this, this.Directories), false);
864 }
865
866 /// <summary>
867 /// Gets a mapping from Directory keys to source/target paths.
868 /// </summary>
869 /// <remarks>
870 /// If the Directory table is modified, this mapping
871 /// will be outdated until you call <see cref="UpdateDirectories"/>.
872 /// </remarks>
873 public InstallPathMap Directories
874 {
875 get
876 {
877 if(this.dirPathMap == null)
878 {
879 this.dirPathMap = InstallPathMap.BuildDirectoryPathMap(this, false);
880 }
881 return this.dirPathMap;
882 }
883 }
884 private InstallPathMap dirPathMap;
885
886 /// <summary>
887 /// Gets a mapping from File keys to source/target paths.
888 /// </summary>
889 /// <remarks>
890 /// If the File, Component, or Directory tables are modified, this mapping
891 /// may be outdated until you call <see cref="UpdateDirectories"/>.
892 /// </remarks>
893 public InstallPathMap Files
894 {
895 get
896 {
897 if(this.filePathMap == null)
898 {
899 this.filePathMap = InstallPathMap.BuildFilePathMap(this,
900 InstallPathMap.BuildComponentPathMap(this, this.Directories), false);
901 }
902 return this.filePathMap;
903 }
904 }
905 private InstallPathMap filePathMap;
906
907 /// <summary>
908 /// Gets or sets the compression level used by <see cref="UpdateFiles()"/>
909 /// and <see cref="Consolidate"/>.
910 /// </summary>
911 /// <remarks>
912 /// If the Directory table is modified, this mapping will be outdated
913 /// until you close and reopen the install package.
914 /// </remarks>
915 public CompressionLevel CompressionLevel
916 {
917 get { return this.compressionLevel; }
918 set { this.compressionLevel = value; }
919 }
920 private CompressionLevel compressionLevel;
921
922 /// <summary>
923 /// Applies a patch package to the database, resulting in an installation package that
924 /// has the patch built-in.
925 /// </summary>
926 /// <param name="patchPackage">The patch package to be applied</param>
927 /// <param name="transform">Optional name of the specific transform to apply.
928 /// This parameter is usually left null, which causes the patch to be searched for
929 /// a transform that is valid to apply to this database.</param>
930 /// <remarks>
931 /// If the patch contains any binary file patches, they will not immediately be applied
932 /// to the target files, though they will at installation time.
933 /// <p>After calling this method you can use <see cref="Consolidate"/> to apply
934 /// the file patches immediately and also discard any outdated files from the package.</p>
935 /// </remarks>
936 public void ApplyPatch(PatchPackage patchPackage, string transform)
937 {
938 if(patchPackage == null) throw new ArgumentNullException("patchPackage");
939
940 this.LogMessage("Applying patch file {0} to database {1}",
941 patchPackage.FilePath, this.FilePath);
942
943 if(transform == null)
944 {
945 this.LogMessage("No transform specified; searching for valid patch transform");
946 string[] validTransforms = patchPackage.GetValidTransforms(this);
947 if(validTransforms.Length == 0)
948 {
949 this.LogMessage("No valid patch transform was found");
950 throw new InvalidOperationException("No valid patch transform was found.");
951 }
952 transform = validTransforms[0];
953 }
954 this.LogMessage("Patch transform = {0}", transform);
955
956 string patchPrefix = Path.GetFileNameWithoutExtension(patchPackage.FilePath) + "_";
957
958 string specialTransform = "#" + transform;
959 Directory.CreateDirectory(this.TempDirectory);
960 this.LogMessage("Extracting substorage {0}", transform);
961 string transformFile = Path.Combine(this.TempDirectory,
962 patchPrefix + Path.GetFileNameWithoutExtension(transform) + ".mst");
963 patchPackage.ExtractTransform(transform, transformFile);
964 this.LogMessage("Extracting substorage {0}", specialTransform);
965 string specialTransformFile = Path.Combine(this.TempDirectory,
966 patchPrefix + Path.GetFileNameWithoutExtension(specialTransform) + ".mst");
967 patchPackage.ExtractTransform(specialTransform, specialTransformFile);
968
969 if (this.Tables.Contains("Patch") && !this.Tables["Patch"].Columns.Contains("_StreamRef"))
970 {
971 if(this.CountRows("Patch") > 0)
972 {
973 this.LogMessage("Warning: non-empty Patch table exists without StreamRef_ column; " +
974 "patch transform may fail");
975 }
976 else
977 {
978 this.Execute("DROP TABLE `Patch`");
979 this.Execute("CREATE TABLE `Patch` (`File_` CHAR(72) NOT NULL, " +
980 "`Sequence` INTEGER NOT NULL, `PatchSize` LONG NOT NULL, " +
981 "`Attributes` INTEGER NOT NULL, `Header` OBJECT, `StreamRef_` CHAR(72) " +
982 "PRIMARY KEY `File_`, `Sequence`)");
983 }
984 }
985
986 this.LogMessage("Applying transform {0} to database", transform);
987 this.ApplyTransform(transformFile);
988 this.LogMessage("Applying transform {0} to database", specialTransform);
989 this.ApplyTransform(specialTransformFile);
990
991 if (this.Tables.Contains("MsiPatchHeaders") && this.CountRows("MsiPatchHeaders") > 0 &&
992 (!this.Tables.Contains("Patch") || this.CountRows("Patch", "`StreamRef_` <> ''") == 0))
993 {
994 this.LogMessage("Error: patch transform failed because of missing Patch.StreamRef_ column");
995 throw new InstallerException("Patch transform failed because of missing Patch.StreamRef_ column");
996 }
997
998 IList<int> mediaIds = this.ExecuteIntegerQuery("SELECT `Media_` FROM `PatchPackage` " +
999 "WHERE `PatchId` = '{0}'", patchPackage.PatchCode);
1000 if (mediaIds.Count == 0)
1001 {
1002 this.LogMessage("Warning: PatchPackage Media record not found -- " +
1003 "skipping inclusion of patch cabinet");
1004 }
1005 else
1006 {
1007 int patchMediaDiskId = mediaIds[0];
1008 IList<string> patchCabinets = this.ExecuteStringQuery("SELECT `Cabinet` FROM `Media` " +
1009 "WHERE `DiskId` = {0}", patchMediaDiskId);
1010 if(patchCabinets.Count == 0)
1011 {
1012 this.LogMessage("Patch cabinet record not found");
1013 throw new InstallerException("Patch cabinet record not found.");
1014 }
1015 string patchCabinet = patchCabinets[0];
1016 this.LogMessage("Patch cabinet = {0}", patchCabinet);
1017 if(!patchCabinet.StartsWith("#", StringComparison.Ordinal))
1018 {
1019 this.LogMessage("Error: Patch cabinet must be embedded");
1020 throw new InstallerException("Patch cabinet must be embedded.");
1021 }
1022 patchCabinet = patchCabinet.Substring(1);
1023
1024 string renamePatchCabinet = patchPrefix + patchCabinet;
1025
1026 const int HIGH_DISKID = 30000; // Must not collide with other patch media DiskIDs
1027 int renamePatchMediaDiskId = HIGH_DISKID;
1028 while (this.CountRows("Media", "`DiskId` = " + renamePatchMediaDiskId) > 0) renamePatchMediaDiskId++;
1029
1030 // Since the patch cab is now embedded in the MSI, it shouldn't have a separate disk prompt/source
1031 this.LogMessage("Renaming the patch media record");
1032 int lastSeq = Convert.ToInt32(this.ExecuteScalar("SELECT `LastSequence` FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId));
1033 this.Execute("DELETE FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId);
1034 this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES ({0}, '{1}', '#{2}')",
1035 renamePatchMediaDiskId, lastSeq, renamePatchCabinet);
1036 this.Execute("UPDATE `PatchPackage` SET `Media_` = {0} WHERE `PatchId` = '{1}'", renamePatchMediaDiskId, patchPackage.PatchCode);
1037
1038 this.LogMessage("Copying patch cabinet: {0}", patchCabinet);
1039 string patchCabFile = Path.Combine(this.TempDirectory,
1040 Path.GetFileNameWithoutExtension(patchCabinet) + ".cab");
1041 using(View streamView = patchPackage.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
1042 "WHERE `Name` = '{0}'", patchCabinet))
1043 {
1044 streamView.Execute();
1045 Record streamRec = streamView.Fetch();
1046 if(streamRec == null)
1047 {
1048 this.LogMessage("Error: Patch cabinet not found");
1049 throw new InstallerException("Patch cabinet not found.");
1050 }
1051 using(streamRec)
1052 {
1053 streamRec.GetStream(2, patchCabFile);
1054 }
1055 }
1056 using(Record patchCabRec = new Record(2))
1057 {
1058 patchCabRec[1] = patchCabinet;
1059 patchCabRec.SetStream(2, patchCabFile);
1060 this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES (?, ?)", patchCabRec);
1061 }
1062
1063 this.LogMessage("Ensuring PatchFiles action exists in InstallExecuteSequence table");
1064 if (this.Tables.Contains("InstallExecuteSequence"))
1065 {
1066 if(this.CountRows("InstallExecuteSequence", "`Action` = 'PatchFiles'") == 0)
1067 {
1068 IList<int> installFilesSeqList = this.ExecuteIntegerQuery("SELECT `Sequence` " +
1069 "FROM `InstallExecuteSequence` WHERE `Action` = 'InstallFiles'");
1070 short installFilesSeq = (short) (installFilesSeqList.Count != 0 ?
1071 installFilesSeqList[0] : 0);
1072 this.Execute("INSERT INTO `InstallExecuteSequence` (`Action`, `Sequence`) " +
1073 "VALUES ('PatchFiles', {0})", installFilesSeq + 1);
1074 }
1075 }
1076
1077 // Patch-added files need to be marked always-compressed
1078 this.LogMessage("Adjusting attributes of patch-added files");
1079 using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
1080 "FROM `File` ORDER BY `Sequence`"))
1081 {
1082 fileView.Execute();
1083
1084 foreach (Record fileRec in fileView) using(fileRec)
1085 {
1086 int fileAttributes = fileRec.GetInteger(2);
1087 if ((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded) != 0)
1088 {
1089 fileAttributes = (fileAttributes | (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed)
1090 & ~(int) WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed
1091 & ~(int) WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded;
1092 fileRec[2] = fileAttributes;
1093 fileView.Update(fileRec);
1094 }
1095 }
1096 }
1097 }
1098
1099 this.LogMessage("Applying new summary info from patch package");
1100 this.SummaryInfo.RevisionNumber = this.Property["PATCHNEWPACKAGECODE"];
1101 this.SummaryInfo.Subject = this.Property["PATCHNEWSUMMARYSUBJECT"];
1102 this.SummaryInfo.Comments = this.Property["PATCHNEWSUMMARYCOMMENTS"];
1103 this.SummaryInfo.Persist();
1104 this.Property["PATCHNEWPACKAGECODE" ] = null;
1105 this.Property["PATCHNEWSUMMARYSUBJECT" ] = null;
1106 this.Property["PATCHNEWSUMMARYCOMMENTS"] = null;
1107
1108 this.LogMessage("Patch application finished");
1109 }
1110
1111 /// <summary>
1112 /// Accessor for getting and setting properties of the InstallPackage database.
1113 /// </summary>
1114 public InstallPackageProperties Property
1115 {
1116 get
1117 {
1118 if(this.properties == null)
1119 {
1120 this.properties = new InstallPackageProperties(this);
1121 }
1122 return this.properties;
1123 }
1124 }
1125 private InstallPackageProperties properties = null;
1126}
1127
1128/// <summary>
1129/// Accessor for getting and setting properties of the <see cref="InstallPackage"/> database.
1130/// </summary>
1131public class InstallPackageProperties
1132{
1133 internal InstallPackageProperties(InstallPackage installPackage)
1134 {
1135 this.installPackage = installPackage;
1136 }
1137 private InstallPackage installPackage;
1138
1139 /// <summary>
1140 /// Gets or sets a property in the database. When getting a property
1141 /// that does not exist in the database, an empty string is returned.
1142 /// To remove a property from the database, set it to an empty string.
1143 /// </summary>
1144 /// <remarks>
1145 /// This has the same results as direct SQL queries on the Property table; it's only
1146 /// meant to be a more convenient way of access.
1147 /// </remarks>
1148 public string this[string name]
1149 {
1150 get
1151 {
1152 IList<string> values = installPackage.ExecuteStringQuery(
1153 "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", name);
1154 return (values.Count != 0 ? values[0] : "");
1155 }
1156 set
1157 {
1158 Record propRec = new Record(name, (value != null ? value : ""));
1159 installPackage.Execute("DELETE FROM `Property` WHERE `Property` = ?", propRec);
1160 if(value != null && value.Length != 0)
1161 {
1162 installPackage.Execute("INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)",
1163 propRec);
1164 }
1165 }
1166 }
1167}
1168
1169}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs
new file mode 100644
index 00000000..e3ba81b5
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs
@@ -0,0 +1,1073 @@
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.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Globalization;
10
11 /// <summary>
12 /// Represents the installation path of a file or directory from an installer product database.
13 /// </summary>
14 public class InstallPath
15 {
16 /// <summary>
17 /// Creates a new InstallPath, specifying a filename.
18 /// </summary>
19 /// <param name="name">The name of the file or directory. Not a full path.</param>
20 public InstallPath(string name) : this(name, false) { }
21
22 /// <summary>
23 /// Creates a new InstallPath, parsing out either the short or long filename.
24 /// </summary>
25 /// <param name="name">The name of the file or directory, in short|long syntax for a filename
26 /// or targetshort|targetlong:sourceshort|sourcelong syntax for a directory.</param>
27 /// <param name="useShortNames">true to parse the short part of the combined filename; false to parse the long part</param>
28 public InstallPath(string name, bool useShortNames)
29 {
30 if(name == null)
31 {
32 throw new ArgumentNullException();
33 }
34 this.parentPath = null;
35 ParseName(name, useShortNames);
36 }
37
38 private void ParseName(string name, bool useShortNames)
39 {
40 string[] parse = name.Split(new char[] { ':' }, 3);
41 if(parse.Length == 3)
42 {
43 // Syntax was targetshort:sourceshort|targetlong:sourcelong.
44 // Chnage it to targetshort|targetlong:sourceshort|sourcelong.
45 parse = name.Split(new char[] { ':', '|' }, 4);
46 if(parse.Length == 4)
47 parse = new string[] { parse[0] + '|' + parse[2], parse[1] + '|' + parse[3] };
48 else
49 parse = new string[] { parse[0] + '|' + parse[1], parse[1] + '|' + parse[2] };
50 }
51 string targetName = parse[0];
52 string sourceName = (parse.Length == 2 ? parse[1] : parse[0]);
53 parse = targetName.Split(new char[] { '|' }, 2);
54 if(parse.Length == 2) targetName = (useShortNames ? parse[0] : parse[1]);
55 parse = sourceName.Split(new char[] { '|' }, 2);
56 if(parse.Length == 2) sourceName = (useShortNames ? parse[0] : parse[1]);
57
58 this.SourceName = sourceName;
59 this.TargetName = targetName;
60 }
61
62 /// <summary>
63 /// Gets the path of the parent directory.
64 /// </summary>
65 public InstallPath ParentPath
66 {
67 get
68 {
69 return parentPath;
70 }
71 }
72 internal void SetParentPath(InstallPath value)
73 {
74 parentPath = value;
75 ResetSourcePath();
76 ResetTargetPath();
77 }
78 private InstallPath parentPath;
79
80 /// <summary>
81 /// Gets the set of child paths if this InstallPath object represents a a directory.
82 /// </summary>
83 public InstallPathCollection ChildPaths
84 {
85 get
86 {
87 if(childPaths == null)
88 {
89 childPaths = new InstallPathCollection(this);
90 }
91 return childPaths;
92 }
93 }
94 private InstallPathCollection childPaths;
95
96 /// <summary>
97 /// Gets or sets the source name of the InstallPath.
98 /// </summary>
99 public string SourceName
100 {
101 get
102 {
103 return sourceName;
104 }
105 set
106 {
107 if(value == null)
108 {
109 throw new ArgumentNullException();
110 }
111 sourceName = value;
112 ResetSourcePath();
113 }
114 }
115 private string sourceName;
116
117 /// <summary>
118 /// Gets or sets the target name of the install path.
119 /// </summary>
120 public string TargetName
121 {
122 get
123 {
124 return targetName;
125 }
126 set
127 {
128 if(value == null)
129 {
130 throw new ArgumentNullException();
131 }
132 targetName = value;
133 ResetTargetPath();
134 }
135 }
136 private string targetName;
137
138 /// <summary>
139 /// Gets the full source path.
140 /// </summary>
141 public string SourcePath
142 {
143 get
144 {
145 if(sourcePath == null)
146 {
147 if(parentPath != null)
148 {
149 sourcePath = (sourceName.Equals(".") ? parentPath.SourcePath
150 : Path.Combine(parentPath.SourcePath, sourceName));
151 }
152 else
153 {
154 sourcePath = sourceName;
155 }
156 }
157 return sourcePath;
158 }
159 set
160 {
161 ResetSourcePath();
162 sourcePath = value;
163 }
164 }
165 private string sourcePath;
166
167 /// <summary>
168 /// Gets the full target path.
169 /// </summary>
170 public string TargetPath
171 {
172 get
173 {
174 if(targetPath == null)
175 {
176 if(parentPath != null)
177 {
178 targetPath = (targetName.Equals(".") ? parentPath.TargetPath
179 : Path.Combine(parentPath.TargetPath, targetName));
180 }
181 else
182 {
183 targetPath = targetName;
184 }
185 }
186 return targetPath;
187 }
188 set
189 {
190 ResetTargetPath();
191 targetPath = value;
192 }
193 }
194 private string targetPath;
195
196 private void ResetSourcePath()
197 {
198 if(sourcePath != null)
199 {
200 sourcePath = null;
201 if(childPaths != null)
202 {
203 foreach(InstallPath ip in childPaths)
204 {
205 ip.ResetSourcePath();
206 }
207 }
208 }
209 }
210
211 private void ResetTargetPath()
212 {
213 if(targetPath != null)
214 {
215 targetPath = null;
216 if(childPaths != null)
217 {
218 foreach(InstallPath ip in childPaths)
219 {
220 ip.ResetTargetPath();
221 }
222 }
223 }
224 }
225
226 /// <summary>
227 /// Gets the full source path.
228 /// </summary>
229 /// <returns><see cref="SourcePath"/></returns>
230 public override String ToString()
231 {
232 return SourcePath;
233 }
234 }
235
236 /// <summary>
237 /// Represents a collection of InstallPaths that are the child paths of the same parent directory.
238 /// </summary>
239 public class InstallPathCollection : IList<InstallPath>
240 {
241 private InstallPath parentPath;
242 private List<InstallPath> items;
243
244 internal InstallPathCollection(InstallPath parentPath)
245 {
246 this.parentPath = parentPath;
247 this.items = new List<InstallPath>();
248 }
249
250 /// <summary>
251 /// Gets or sets the element at the specified index.
252 /// </summary>
253 public InstallPath this[int index]
254 {
255 get
256 {
257 return this.items[index];
258 }
259 set
260 {
261 this.OnSet(this.items[index], value);
262 this.items[index] = value;
263 }
264 }
265
266 /// <summary>
267 /// Adds a new child path to the collection.
268 /// </summary>
269 /// <param name="item">The InstallPath to add.</param>
270 public void Add(InstallPath item)
271 {
272 this.OnInsert(item);
273 this.items.Add(item);
274 }
275
276 /// <summary>
277 /// Removes a child path to the collection.
278 /// </summary>
279 /// <param name="item">The InstallPath to remove.</param>
280 public bool Remove(InstallPath item)
281 {
282 int index = this.items.IndexOf(item);
283 if (index >= 0)
284 {
285 this.OnRemove(item);
286 this.items.RemoveAt(index);
287 return true;
288 }
289 else
290 {
291 return false;
292 }
293 }
294
295 /// <summary>
296 /// Gets the index of a child path in the collection.
297 /// </summary>
298 /// <param name="item">The InstallPath to search for.</param>
299 /// <returns>The index of the item, or -1 if not found.</returns>
300 public int IndexOf(InstallPath item)
301 {
302 return this.items.IndexOf(item);
303 }
304
305 /// <summary>
306 /// Inserts a child path into the collection.
307 /// </summary>
308 /// <param name="index">The insertion index.</param>
309 /// <param name="item">The InstallPath to insert.</param>
310 public void Insert(int index, InstallPath item)
311 {
312 this.OnInsert(item);
313 this.items.Insert(index, item);
314 }
315
316 /// <summary>
317 /// Tests if the collection contains a child path.
318 /// </summary>
319 /// <param name="item">The InstallPath to search for.</param>
320 /// <returns>true if the item is found; false otherwise</returns>
321 public bool Contains(InstallPath item)
322 {
323 return this.items.Contains(item);
324 }
325
326 /// <summary>
327 /// Copies the collection into an array.
328 /// </summary>
329 /// <param name="array">The array to copy into.</param>
330 /// <param name="index">The starting index in the destination array.</param>
331 public void CopyTo(InstallPath[] array, int index)
332 {
333 this.items.CopyTo(array, index);
334 }
335
336 private void OnInsert(InstallPath item)
337 {
338 if (item.ParentPath != null)
339 {
340 item.ParentPath.ChildPaths.Remove(item);
341 }
342
343 item.SetParentPath(this.parentPath);
344 }
345
346 private void OnRemove(InstallPath item)
347 {
348 item.SetParentPath(null);
349 }
350
351 private void OnSet(InstallPath oldItem, InstallPath newItem)
352 {
353 this.OnRemove(oldItem);
354 this.OnInsert(newItem);
355 }
356
357 /// <summary>
358 /// Removes an item from the collection.
359 /// </summary>
360 /// <param name="index">The index of the item to remove.</param>
361 public void RemoveAt(int index)
362 {
363 this.OnRemove(this[index]);
364 this.items.RemoveAt(index);
365 }
366
367 /// <summary>
368 /// Removes all items from the collection.
369 /// </summary>
370 public void Clear()
371 {
372 foreach (InstallPath item in this)
373 {
374 this.OnRemove(item);
375 }
376
377 this.items.Clear();
378 }
379
380 /// <summary>
381 /// Gets the number of items in the collection.
382 /// </summary>
383 public int Count
384 {
385 get
386 {
387 return this.items.Count;
388 }
389 }
390
391 bool ICollection<InstallPath>.IsReadOnly
392 {
393 get
394 {
395 return false;
396 }
397 }
398
399 /// <summary>
400 /// Gets an enumerator over all items in the collection.
401 /// </summary>
402 /// <returns>An enumerator for the collection.</returns>
403 public IEnumerator<InstallPath> GetEnumerator()
404 {
405 return this.items.GetEnumerator();
406 }
407
408 IEnumerator IEnumerable.GetEnumerator()
409 {
410 return ((IEnumerable<InstallPath>) this).GetEnumerator();
411 }
412 }
413
414 /// <summary>
415 /// Represents a mapping of install paths for all directories, components, or files in
416 /// an installation database.
417 /// </summary>
418 public class InstallPathMap : IDictionary<string, InstallPath>
419 {
420 /// <summary>
421 /// Builds a mapping from File keys to installation paths.
422 /// </summary>
423 /// <param name="db">Installation database.</param>
424 /// <param name="componentPathMap">Component mapping returned by <see cref="BuildComponentPathMap"/>.</param>
425 /// <param name="useShortNames">true to use short file names; false to use long names</param>
426 /// <returns>An InstallPathMap with the described mapping.</returns>
427 public static InstallPathMap BuildFilePathMap(Database db, InstallPathMap componentPathMap,
428 bool useShortNames)
429 {
430 if(db == null)
431 {
432 throw new ArgumentNullException("db");
433 }
434
435 if(componentPathMap == null)
436 {
437 componentPathMap = BuildComponentPathMap(db, BuildDirectoryPathMap(db, useShortNames));
438 }
439
440 InstallPathMap filePathMap = new InstallPathMap();
441
442 using (View fileView = db.OpenView("SELECT `File`, `Component_`, `FileName` FROM `File`"))
443 {
444 fileView.Execute();
445
446 foreach (Record fileRec in fileView)
447 {
448 using (fileRec)
449 {
450 string file = (string) fileRec[1];
451 string comp = (string) fileRec[2];
452 string fileName = (string) fileRec[3];
453
454 InstallPath compPath = (InstallPath) componentPathMap[comp];
455 if(compPath != null)
456 {
457 InstallPath filePath = new InstallPath(fileName, useShortNames);
458 compPath.ChildPaths.Add(filePath);
459 filePathMap[file] = filePath;
460 }
461 }
462 }
463 }
464
465 return filePathMap;
466 }
467
468 /// <summary>
469 /// Builds a mapping from Component keys to installation paths.
470 /// </summary>
471 /// <param name="db">Installation database.</param>
472 /// <param name="directoryPathMap">Directory mapping returned by
473 /// <see cref="BuildDirectoryPathMap(Database,bool)"/>.</param>
474 /// <returns>An InstallPathMap with the described mapping.</returns>
475 public static InstallPathMap BuildComponentPathMap(Database db, InstallPathMap directoryPathMap)
476 {
477 if(db == null)
478 {
479 throw new ArgumentNullException("db");
480 }
481
482 InstallPathMap compPathMap = new InstallPathMap();
483
484 using (View compView = db.OpenView("SELECT `Component`, `Directory_` FROM `Component`"))
485 {
486 compView.Execute();
487
488 foreach (Record compRec in compView)
489 {
490 using (compRec)
491 {
492 string comp = (string) compRec[1];
493 InstallPath dirPath = (InstallPath) directoryPathMap[(string) compRec[2]];
494
495 if (dirPath != null)
496 {
497 compPathMap[comp] = dirPath;
498 }
499 }
500 }
501 }
502
503 return compPathMap;
504 }
505
506 /// <summary>
507 /// Builds a mapping from Directory keys to installation paths.
508 /// </summary>
509 /// <param name="db">Installation database.</param>
510 /// <param name="useShortNames">true to use short directory names; false to use long names</param>
511 /// <returns>An InstallPathMap with the described mapping.</returns>
512 public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames)
513 {
514 return BuildDirectoryPathMap(db, useShortNames, null, null);
515 }
516
517 /// <summary>
518 /// Builds a mapping of Directory keys to directory paths, specifying root directories
519 /// for the source and target paths.
520 /// </summary>
521 /// <param name="db">Database containing the Directory table.</param>
522 /// <param name="useShortNames">true to use short directory names; false to use long names</param>
523 /// <param name="sourceRootDir">The root directory path of all source paths, or null to leave them relative.</param>
524 /// <param name="targetRootDir">The root directory path of all source paths, or null to leave them relative.</param>
525 /// <returns>An InstallPathMap with the described mapping.</returns>
526 public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames,
527 string sourceRootDir, string targetRootDir)
528 {
529 if(db == null)
530 {
531 throw new ArgumentNullException("db");
532 }
533
534 if(sourceRootDir == null) sourceRootDir = "";
535 if(targetRootDir == null) targetRootDir = "";
536
537 InstallPathMap dirMap = new InstallPathMap();
538 IDictionary dirTreeMap = new Hashtable();
539
540 using (View dirView = db.OpenView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"))
541 {
542 dirView.Execute();
543
544 foreach (Record dirRec in dirView) using (dirRec)
545 {
546 string key = (string) dirRec[1];
547 string parentKey = (string) dirRec[2];
548 InstallPath dir = new InstallPath((string) dirRec[3], useShortNames);
549
550 dirMap[key] = dir;
551
552 InstallPathMap siblingDirs = (InstallPathMap) dirTreeMap[parentKey];
553 if (siblingDirs == null)
554 {
555 siblingDirs = new InstallPathMap();
556 dirTreeMap[parentKey] = siblingDirs;
557 }
558 siblingDirs.Add(key, dir);
559 }
560 }
561
562 foreach (KeyValuePair<string, InstallPath> entry in (InstallPathMap) dirTreeMap[""])
563 {
564 string key = (string) entry.Key;
565 InstallPath dir = (InstallPath) entry.Value;
566 LinkSubdirectories(key, dir, dirTreeMap);
567 }
568
569 InstallPath targetDirPath = (InstallPath) dirMap["TARGETDIR"];
570 if(targetDirPath != null)
571 {
572 targetDirPath.SourcePath = sourceRootDir;
573 targetDirPath.TargetPath = targetRootDir;
574 }
575
576 return dirMap;
577 }
578
579 private static void LinkSubdirectories(string key, InstallPath dir, IDictionary dirTreeMap)
580 {
581 InstallPathMap subDirs = (InstallPathMap) dirTreeMap[key];
582 if(subDirs != null)
583 {
584 foreach (KeyValuePair<string, InstallPath> entry in subDirs)
585 {
586 string subKey = (string) entry.Key;
587 InstallPath subDir = (InstallPath) entry.Value;
588 dir.ChildPaths.Add(subDir);
589 LinkSubdirectories(subKey, subDir, dirTreeMap);
590 }
591 }
592 }
593
594 private Dictionary<string, InstallPath> items;
595
596 /// <summary>
597 /// Creates a new empty InstallPathMap.
598 /// </summary>
599 public InstallPathMap()
600 {
601 this.items = new Dictionary<string,InstallPath>(StringComparer.Ordinal);
602 }
603
604 /// <summary>
605 /// Gets a mapping from keys to source paths.
606 /// </summary>
607 public IDictionary<string, string> SourcePaths
608 {
609 get
610 {
611 return new SourcePathMap(this);
612 }
613 }
614
615 /// <summary>
616 /// Gets a mapping from keys to target paths.
617 /// </summary>
618 public IDictionary<string, string> TargetPaths
619 {
620 get
621 {
622 return new TargetPathMap(this);
623 }
624 }
625
626 /// <summary>
627 /// Gets or sets an install path for a direcotry, component, or file key.
628 /// </summary>
629 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
630 /// either the Directory, Component, or File table.</param>
631 /// <remarks>
632 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
633 /// </remarks>
634 public InstallPath this[string key]
635 {
636 get
637 {
638 InstallPath value = null;
639 this.items.TryGetValue(key, out value);
640 return value;
641 }
642 set
643 {
644 this.items[key] = value;
645 }
646 }
647
648 /// <summary>
649 /// Gets the collection of keys in the InstallPathMap. Depending on the type of InstallPathMap,
650 /// they are all directory, component, or file key strings.
651 /// </summary>
652 public ICollection<string> Keys
653 {
654 get
655 {
656 return this.items.Keys;
657 }
658 }
659
660 /// <summary>
661 /// Gets the collection of InstallPath values in the InstallPathMap.
662 /// </summary>
663 public ICollection<InstallPath> Values
664 {
665 get
666 {
667 return this.items.Values;
668 }
669 }
670
671 /// <summary>
672 /// Sets an install path for a direcotry, component, or file key.
673 /// </summary>
674 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
675 /// either the Directory, Component, or File table.</param>
676 /// <param name="installPath">The install path of the key item.</param>
677 /// <remarks>
678 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
679 /// </remarks>
680 public void Add(string key, InstallPath installPath)
681 {
682 this.items.Add(key, installPath);
683 }
684
685 /// <summary>
686 /// Removes an install path from the map.
687 /// </summary>
688 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
689 /// either the Directory, Component, or File table.</param>
690 /// <returns>true if the item was removed, false if it did not exist</returns>
691 /// <remarks>
692 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
693 /// </remarks>
694 public bool Remove(string key)
695 {
696 return this.items.Remove(key);
697 }
698
699 /// <summary>
700 /// Tests whether a direcotry, component, or file key exists in the map.
701 /// </summary>
702 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
703 /// either the Directory, Component, or File table.</param>
704 /// <returns>true if the key is found; false otherwise</returns>
705 public bool ContainsKey(string key)
706 {
707 return this.items.ContainsKey(key);
708 }
709
710 /*
711 public override string ToString()
712 {
713 System.Text.StringBuilder buf = new System.Text.StringBuilder();
714 foreach(KeyValuePair<string, InstallPath> entry in this)
715 {
716 buf.AppendFormat("{0}={1}", entry.Key, entry.Value);
717 buf.Append("\n");
718 }
719 return buf.ToString();
720 }
721 */
722
723 /// <summary>
724 /// Attempts to get a value from the dictionary.
725 /// </summary>
726 /// <param name="key">The key to lookup.</param>
727 /// <param name="value">Receives the value, or null if they key was not found.</param>
728 /// <returns>True if the value was found, else false.</returns>
729 public bool TryGetValue(string key, out InstallPath value)
730 {
731 return this.items.TryGetValue(key, out value);
732 }
733
734 void ICollection<KeyValuePair<string, InstallPath>>.Add(KeyValuePair<string, InstallPath> item)
735 {
736 ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Add(item);
737 }
738
739 /// <summary>
740 /// Removes all entries from the dictionary.
741 /// </summary>
742 public void Clear()
743 {
744 this.items.Clear();
745 }
746
747 bool ICollection<KeyValuePair<string, InstallPath>>.Contains(KeyValuePair<string, InstallPath> item)
748 {
749 return ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Contains(item);
750 }
751
752 void ICollection<KeyValuePair<string, InstallPath>>.CopyTo(KeyValuePair<string, InstallPath>[] array, int arrayIndex)
753 {
754 ((ICollection<KeyValuePair<string, InstallPath>>) this.items).CopyTo(array, arrayIndex);
755 }
756
757 /// <summary>
758 /// Gets the number of entries in the dictionary.
759 /// </summary>
760 public int Count
761 {
762 get
763 {
764 return this.items.Count;
765 }
766 }
767
768 bool ICollection<KeyValuePair<string, InstallPath>>.IsReadOnly
769 {
770 get
771 {
772 return false;
773 }
774 }
775
776 bool ICollection<KeyValuePair<string, InstallPath>>.Remove(KeyValuePair<string, InstallPath> item)
777 {
778 return ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Remove(item);
779 }
780
781 /// <summary>
782 /// Gets an enumerator over all entries in the dictionary.
783 /// </summary>
784 /// <returns>An enumerator for the dictionary.</returns>
785 public IEnumerator<KeyValuePair<string, InstallPath>> GetEnumerator()
786 {
787 return this.items.GetEnumerator();
788 }
789
790 IEnumerator IEnumerable.GetEnumerator()
791 {
792 return this.items.GetEnumerator();
793 }
794 }
795
796 internal class SourcePathMap : IDictionary<string, string>
797 {
798 private const string RO_MSG =
799 "The SourcePathMap collection is read-only. " +
800 "Modify the InstallPathMap instead.";
801
802 private InstallPathMap map;
803
804 internal SourcePathMap(InstallPathMap map)
805 {
806 this.map = map;
807 }
808
809 public void Add(string key, string value)
810 {
811 throw new InvalidOperationException(RO_MSG);
812 }
813
814 public bool ContainsKey(string key)
815 {
816 return this.map.ContainsKey(key);
817 }
818
819 public ICollection<string> Keys
820 {
821 get
822 {
823 return this.map.Keys;
824 }
825 }
826
827 public bool Remove(string key)
828 {
829 throw new InvalidOperationException(RO_MSG);
830 }
831
832 public bool TryGetValue(string key, out string value)
833 {
834 InstallPath installPath;
835 if (this.map.TryGetValue(key, out installPath))
836 {
837 value = installPath.SourcePath;
838 return true;
839 }
840 else
841 {
842 value = null;
843 return false;
844 }
845 }
846
847 public ICollection<string> Values
848 {
849 get
850 {
851 List<string> values = new List<string>(this.Count);
852 foreach (KeyValuePair<string, InstallPath> entry in this.map)
853 {
854 values.Add(entry.Value.SourcePath);
855 }
856 return values;
857 }
858 }
859
860 public string this[string key]
861 {
862 get
863 {
864 string value = null;
865 this.TryGetValue(key, out value);
866 return value;
867 }
868 set
869 {
870 throw new InvalidOperationException(RO_MSG);
871 }
872 }
873
874 public void Add(KeyValuePair<string, string> item)
875 {
876 throw new InvalidOperationException(RO_MSG);
877 }
878
879 public void Clear()
880 {
881 throw new InvalidOperationException(RO_MSG);
882 }
883
884 public bool Contains(KeyValuePair<string, string> item)
885 {
886 string value = this[item.Key];
887 return value == item.Value;
888 }
889
890 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
891 {
892 foreach (KeyValuePair<string, string> entry in this)
893 {
894 array[arrayIndex] = entry;
895 arrayIndex++;
896 }
897 }
898
899 public int Count
900 {
901 get
902 {
903 return this.map.Count;
904 }
905 }
906
907 public bool IsReadOnly
908 {
909 get
910 {
911 return true;
912 }
913 }
914
915 public bool Remove(KeyValuePair<string, string> item)
916 {
917 throw new InvalidOperationException(RO_MSG);
918 }
919
920 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
921 {
922 foreach (KeyValuePair<string, InstallPath> entry in this.map)
923 {
924 yield return new KeyValuePair<string, string>(
925 entry.Key, entry.Value.SourcePath);
926 }
927 }
928
929 IEnumerator IEnumerable.GetEnumerator()
930 {
931 return this.GetEnumerator();
932 }
933 }
934
935 internal class TargetPathMap : IDictionary<string, string>
936 {
937 private const string RO_MSG =
938 "The TargetPathMap collection is read-only. " +
939 "Modify the InstallPathMap instead.";
940
941 private InstallPathMap map;
942
943 internal TargetPathMap(InstallPathMap map)
944 {
945 this.map = map;
946 }
947
948 public void Add(string key, string value)
949 {
950 throw new InvalidOperationException(RO_MSG);
951 }
952
953 public bool ContainsKey(string key)
954 {
955 return this.map.ContainsKey(key);
956 }
957
958 public ICollection<string> Keys
959 {
960 get
961 {
962 return this.map.Keys;
963 }
964 }
965
966 public bool Remove(string key)
967 {
968 throw new InvalidOperationException(RO_MSG);
969 }
970
971 public bool TryGetValue(string key, out string value)
972 {
973 InstallPath installPath;
974 if (this.map.TryGetValue(key, out installPath))
975 {
976 value = installPath.TargetPath;
977 return true;
978 }
979 else
980 {
981 value = null;
982 return false;
983 }
984 }
985
986 public ICollection<string> Values
987 {
988 get
989 {
990 List<string> values = new List<string>(this.Count);
991 foreach (KeyValuePair<string, InstallPath> entry in this.map)
992 {
993 values.Add(entry.Value.TargetPath);
994 }
995 return values;
996 }
997 }
998
999 public string this[string key]
1000 {
1001 get
1002 {
1003 string value = null;
1004 this.TryGetValue(key, out value);
1005 return value;
1006 }
1007 set
1008 {
1009 throw new InvalidOperationException(RO_MSG);
1010 }
1011 }
1012
1013 public void Add(KeyValuePair<string, string> item)
1014 {
1015 throw new InvalidOperationException(RO_MSG);
1016 }
1017
1018 public void Clear()
1019 {
1020 throw new InvalidOperationException(RO_MSG);
1021 }
1022
1023 public bool Contains(KeyValuePair<string, string> item)
1024 {
1025 string value = this[item.Key];
1026 return value == item.Value;
1027 }
1028
1029 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
1030 {
1031 foreach (KeyValuePair<string, string> entry in this)
1032 {
1033 array[arrayIndex] = entry;
1034 arrayIndex++;
1035 }
1036 }
1037
1038 public int Count
1039 {
1040 get
1041 {
1042 return this.map.Count;
1043 }
1044 }
1045
1046 public bool IsReadOnly
1047 {
1048 get
1049 {
1050 return true;
1051 }
1052 }
1053
1054 public bool Remove(KeyValuePair<string, string> item)
1055 {
1056 throw new InvalidOperationException(RO_MSG);
1057 }
1058
1059 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
1060 {
1061 foreach (KeyValuePair<string, InstallPath> entry in this.map)
1062 {
1063 yield return new KeyValuePair<string, string>(
1064 entry.Key, entry.Value.TargetPath);
1065 }
1066 }
1067
1068 IEnumerator IEnumerable.GetEnumerator()
1069 {
1070 return this.GetEnumerator();
1071 }
1072 }
1073}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs
new file mode 100644
index 00000000..54bd2b93
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs
@@ -0,0 +1,259 @@
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.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11
12 /// <summary>
13 /// Provides access to convenient properties and operations on a patch package (.MSP).
14 /// </summary>
15 public class PatchPackage : Database
16 {
17 /// <summary>
18 /// Creates a new patch package object; opening the patch database in read-only mode.
19 /// </summary>
20 /// <param name="packagePath">Path to the patch package (.MSP)</param>
21 /// <remarks>The PatchPackage object only opens the patch database in read-only mode, because
22 /// transforms (sub-storages) cannot be read if the database is open in read-write mode.</remarks>
23 public PatchPackage(string packagePath)
24 : base(packagePath, (DatabaseOpenMode) ((int) DatabaseOpenMode.ReadOnly | 32))
25 // TODO: figure out what to do about DatabaseOpenMode.Patch
26 {
27 }
28
29 /// <summary>
30 /// Handle this event to receive status messages when operations are performed on the patch package.
31 /// </summary>
32 /// <example>
33 /// <c>patchPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
34 /// </example>
35 public event InstallPackageMessageHandler Message;
36
37 /// <summary>
38 /// Sends a message to the <see cref="Message"/> event-handler.
39 /// </summary>
40 /// <param name="format">Message string, containing 0 or more format items</param>
41 /// <param name="args">Items to be formatted</param>
42 protected void LogMessage(string format, params object[] args)
43 {
44 if(this.Message != null)
45 {
46 this.Message(format, args);
47 }
48 }
49
50 /// <summary>
51 /// Gets the patch code (GUID) of the patch package.
52 /// </summary>
53 /// <remarks>
54 /// The patch code is stored in the RevisionNumber field of the patch summary information.
55 /// </remarks>
56 public string PatchCode
57 {
58 get
59 {
60 string guids = this.SummaryInfo.RevisionNumber;
61 return guids.Substring(0, guids.IndexOf('}') + 1);
62 }
63 }
64
65 /// <summary>
66 /// Gets the list of patch codes that are replaced by this patch package.
67 /// </summary>
68 /// <returns>Array of replaced patch codes (GUIDs)</returns>
69 /// <remarks>
70 /// The list of replaced patch codes is stored in the RevisionNumber field of the patch summary information.
71 /// </remarks>
72 public string[] GetReplacedPatchCodes()
73 {
74 ArrayList patchCodeList = new ArrayList();
75 string guids = this.SummaryInfo.RevisionNumber;
76 int thisGuid = guids.IndexOf('}') + 1;
77 int nextGuid = guids.IndexOf('}', thisGuid) + 1;
78 while(nextGuid > 0)
79 {
80 patchCodeList.Add(guids.Substring(thisGuid, (nextGuid - thisGuid)));
81 thisGuid = nextGuid;
82 nextGuid = guids.IndexOf('}', thisGuid) + 1;
83 }
84 return (string[]) patchCodeList.ToArray(typeof(string));
85 }
86
87 /// <summary>
88 /// Gets the list of product codes of products targeted by this patch package.
89 /// </summary>
90 /// <returns>Array of product codes (GUIDs)</returns>
91 /// <remarks>
92 /// The list of target product codes is stored in the Template field of the patch summary information.
93 /// </remarks>
94 public string[] GetTargetProductCodes()
95 {
96 string productList = this.SummaryInfo.Template;
97 return productList.Split(';');
98 }
99
100 /// <summary>
101 /// Gets the names of the transforms included in the patch package.
102 /// </summary>
103 /// <returns>Array of transform names</returns>
104 /// <remarks>
105 /// The returned list does not include the &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;
106 /// <p>The list of transform names is stored in the LastSavedBy field of the patch summary information.</p>
107 /// </remarks>
108 public string[] GetTransforms()
109 {
110 return this.GetTransforms(false);
111 }
112 /// <summary>
113 /// Gets the names of the transforms included in the patch package.
114 /// </summary>
115 /// <param name="includeSpecialTransforms">Specifies whether to include the
116 /// &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;</param>
117 /// <returns>Array of transform names</returns>
118 /// <remarks>
119 /// The list of transform names is stored in the LastSavedBy field of the patch summary information.
120 /// </remarks>
121 public string[] GetTransforms(bool includeSpecialTransforms)
122 {
123 ArrayList transformArray = new ArrayList();
124 string transformList = this.SummaryInfo.LastSavedBy;
125 foreach(string transform in transformList.Split(';', ':'))
126 {
127 if(transform.Length != 0 && (includeSpecialTransforms || !transform.StartsWith("#", StringComparison.Ordinal)))
128 {
129 transformArray.Add(transform);
130 }
131 }
132 return (string[]) transformArray.ToArray(typeof(string));
133 }
134
135 /// <summary>
136 /// Gets information about the transforms included in the patch package.
137 /// </summary>
138 /// <returns>Array containing information about each transform</returns>
139 /// <remarks>
140 /// The returned info does not include the &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;
141 /// </remarks>
142 public TransformInfo[] GetTransformsInfo()
143 {
144 return this.GetTransformsInfo(false);
145 }
146
147 /// <summary>
148 /// Gets information about the transforms included in the patch package.
149 /// </summary>
150 /// <param name="includeSpecialTransforms">Specifies whether to include the
151 /// &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;</param>
152 /// <returns>Array containing information about each transform</returns>
153 public TransformInfo[] GetTransformsInfo(bool includeSpecialTransforms)
154 {
155 string[] transforms = this.GetTransforms(includeSpecialTransforms);
156 ArrayList transformInfoArray = new ArrayList(transforms.Length);
157 foreach(string transform in transforms)
158 {
159 transformInfoArray.Add(this.GetTransformInfo(transform));
160 }
161 return (TransformInfo[]) transformInfoArray.ToArray(typeof(TransformInfo));
162 }
163
164 /// <summary>
165 /// Gets information about a transforms included in the patch package.
166 /// </summary>
167 /// <param name="transform">Name of the transform to extract; this may optionally be a
168 /// special transform prefixed by &quot;#&quot;</param>
169 /// <returns>Information about the transform</returns>
170 public TransformInfo GetTransformInfo(string transform)
171 {
172 string tempTransformFile = null;
173 try
174 {
175 tempTransformFile = Path.GetTempFileName();
176 this.ExtractTransform(transform, tempTransformFile);
177 using(SummaryInfo transformSummInfo = new SummaryInfo(tempTransformFile, false))
178 {
179 return new TransformInfo(transform, transformSummInfo);
180 }
181 }
182 finally
183 {
184 if(tempTransformFile != null && File.Exists(tempTransformFile))
185 {
186 File.Delete(tempTransformFile);
187 }
188 }
189 }
190
191 /// <summary>
192 /// Analyzes the transforms included in the patch package to find the ones that
193 /// are applicable to an install package.
194 /// </summary>
195 /// <param name="installPackage">The install package to validate the transforms against</param>
196 /// <returns>Array of valid transform names</returns>
197 /// <remarks>
198 /// The returned list does not include the &quot;patch special transforms&quot; that
199 /// are prefixed with &quot;#&quot; If a transform is valid, then its corresponding
200 /// special transform is assumed to be valid as well.
201 /// </remarks>
202 public string[] GetValidTransforms(InstallPackage installPackage)
203 {
204 ArrayList transformArray = new ArrayList();
205 string transformList = this.SummaryInfo.LastSavedBy;
206 foreach(string transform in transformList.Split(';', ':'))
207 {
208 if(transform.Length != 0 && !transform.StartsWith("#", StringComparison.Ordinal))
209 {
210 this.LogMessage("Checking validity of transform {0}", transform);
211 string tempTransformFile = null;
212 try
213 {
214 tempTransformFile = Path.GetTempFileName();
215 this.ExtractTransform(transform, tempTransformFile);
216 if(installPackage.IsTransformValid(tempTransformFile))
217 {
218 this.LogMessage("Found valid transform: {0}", transform);
219 transformArray.Add(transform);
220 }
221 }
222 finally
223 {
224 if(tempTransformFile != null && File.Exists(tempTransformFile))
225 {
226 try { File.Delete(tempTransformFile); }
227 catch(IOException) { }
228 }
229 }
230 }
231 }
232 return (string[]) transformArray.ToArray(typeof(string));
233 }
234
235 /// <summary>
236 /// Extracts a transform (.MST) from a patch package.
237 /// </summary>
238 /// <param name="transform">Name of the transform to extract; this may optionally be a
239 /// special transform prefixed by &quot;#&quot;</param>
240 /// <param name="extractFile">Location where the transform will be extracted</param>
241 public void ExtractTransform(string transform, string extractFile)
242 {
243 using(View stgView = this.OpenView("SELECT `Name`, `Data` FROM `_Storages` WHERE `Name` = '{0}'", transform))
244 {
245 stgView.Execute();
246 Record stgRec = stgView.Fetch();
247 if(stgRec == null)
248 {
249 this.LogMessage("Transform not found: {0}", transform);
250 throw new InstallerException("Transform not found: " + transform);
251 }
252 using(stgRec)
253 {
254 stgRec.GetStream("Data", extractFile);
255 }
256 }
257 }
258 }
259}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs
new file mode 100644
index 00000000..0bf3d3f9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs
@@ -0,0 +1,154 @@
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.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Globalization;
8
9 /// <summary>
10 /// Contains properties of a transform package (.MST).
11 /// </summary>
12 public class TransformInfo
13 {
14 /// <summary>
15 /// Reads transform information from a transform package.
16 /// </summary>
17 /// <param name="mstFile">Path to a transform package (.MST file).</param>
18 public TransformInfo(string mstFile)
19 {
20 this.name = Path.GetFileName(mstFile);
21 using (SummaryInfo transformSummInfo = new SummaryInfo(mstFile, false))
22 {
23 this.DecodeSummaryInfo(transformSummInfo);
24 }
25 }
26
27 /// <summary>
28 /// Reads transform information from the summary information of a transform package.
29 /// </summary>
30 /// <param name="name">Filename of the transform (optional).</param>
31 /// <param name="transformSummaryInfo">Handle to the summary information of a transform package (.MST file).</param>
32 public TransformInfo(string name, SummaryInfo transformSummaryInfo)
33 {
34 this.name = name;
35 this.DecodeSummaryInfo(transformSummaryInfo);
36 }
37
38 private void DecodeSummaryInfo(SummaryInfo transformSummaryInfo)
39 {
40 try
41 {
42 string[] rev = transformSummaryInfo.RevisionNumber.Split(new char[] { ';' }, 3);
43 this.targetProductCode = rev[0].Substring(0, 38);
44 this.targetProductVersion = rev[0].Substring(38);
45 this.upgradeProductCode = rev[1].Substring(0, 38);
46 this.upgradeProductVersion = rev[1].Substring(38);
47 this.upgradeCode = rev[2];
48
49 string[] templ = transformSummaryInfo.Template.Split(new Char[] { ';' }, 2);
50 this.targetPlatform = templ[0];
51 this.targetLanguage = 0;
52 if (templ.Length >= 2 && templ[1].Length > 0)
53 {
54 this.targetLanguage = Int32.Parse(templ[1], CultureInfo.InvariantCulture.NumberFormat);
55 }
56
57 this.validateFlags = (TransformValidations) transformSummaryInfo.CharacterCount;
58 }
59 catch (Exception ex)
60 {
61 throw new InstallerException("Invalid transform summary info", ex);
62 }
63 }
64
65 /// <summary>
66 /// Gets the filename of the transform.
67 /// </summary>
68 public string Name
69 {
70 get { return this.name; }
71 }
72 private string name;
73
74 /// <summary>
75 /// Gets the target product code of the transform.
76 /// </summary>
77 public string TargetProductCode
78 {
79 get { return this.targetProductCode; }
80 }
81 private string targetProductCode;
82
83 /// <summary>
84 /// Gets the target product version of the transform.
85 /// </summary>
86 public string TargetProductVersion
87 {
88 get { return this.targetProductVersion; }
89 }
90 private string targetProductVersion;
91
92 /// <summary>
93 /// Gets the upgrade product code of the transform.
94 /// </summary>
95 public string UpgradeProductCode
96 {
97 get { return this.upgradeProductCode; }
98 }
99 private string upgradeProductCode;
100
101 /// <summary>
102 /// Gets the upgrade product version of the transform.
103 /// </summary>
104 public string UpgradeProductVersion
105 {
106 get { return this.upgradeProductVersion; }
107 }
108 private string upgradeProductVersion;
109
110 /// <summary>
111 /// Gets the upgrade code of the transform.
112 /// </summary>
113 public string UpgradeCode
114 {
115 get { return this.upgradeCode; }
116 }
117 private string upgradeCode;
118
119 /// <summary>
120 /// Gets the target platform of the transform.
121 /// </summary>
122 public string TargetPlatform
123 {
124 get { return this.targetPlatform; }
125 }
126 private string targetPlatform;
127
128 /// <summary>
129 /// Gets the target language of the transform, or 0 if the transform is language-neutral.
130 /// </summary>
131 public int TargetLanguage
132 {
133 get { return this.targetLanguage; }
134 }
135 private int targetLanguage;
136
137 /// <summary>
138 /// Gets the validation flags specified when the transform was generated.
139 /// </summary>
140 public TransformValidations Validations
141 {
142 get { return this.validateFlags; }
143 }
144 private TransformValidations validateFlags;
145
146 /// <summary>
147 /// Returns the name of the transform.
148 /// </summary>
149 public override string ToString()
150 {
151 return (this.Name != null ? this.Name : "MST");
152 }
153 }
154}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj
new file mode 100644
index 00000000..28ded687
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj
@@ -0,0 +1,23 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- 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. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.WindowsInstaller</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.WindowsInstaller.Package</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Extended managed libraries for Windows Installer</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
15 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
16 <ProjectReference Include="..\WixToolset.Dtf.Compression.Cab\WixToolset.Dtf.Compression.Cab.csproj" />
17 </ItemGroup>
18
19 <ItemGroup>
20 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
21 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
22 </ItemGroup>
23</Project>