aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs1169
1 files changed, 1169 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}