main
cs 791 lines 35.7 KB
Raw
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
3 namespace WixToolset.Core.WindowsInstaller.Bind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Text.RegularExpressions;
10 using WixToolset.Core.Native.Msi;
11 using WixToolset.Data;
12 using WixToolset.Data.Symbols;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Data.WindowsInstaller.Rows;
15 using WixToolset.Extensibility.Services;
16
17 /// <summary>
18 /// Include transforms in a patch.
19 /// </summary>
20 internal class CreatePatchSubStoragesCommand
21 {
22 private static readonly string[] PatchUninstallBreakingTables = new[]
23 {
24 "AppId",
25 "BindImage",
26 "Class",
27 "Complus",
28 "CreateFolder",
29 "DuplicateFile",
30 "Environment",
31 "Extension",
32 "Font",
33 "IniFile",
34 "IsolatedComponent",
35 "LockPermissions",
36 "MIME",
37 "MoveFile",
38 "MsiLockPermissionsEx",
39 "MsiServiceConfig",
40 "MsiServiceConfigFailureActions",
41 "ODBCAttribute",
42 "ODBCDataSource",
43 "ODBCDriver",
44 "ODBCSourceAttribute",
45 "ODBCTranslator",
46 "ProgId",
47 "PublishComponent",
48 "RemoveIniFile",
49 "SelfReg",
50 "ServiceControl",
51 "ServiceInstall",
52 "TypeLib",
53 "Verb",
54 };
55
56 private readonly TableDefinitionCollection tableDefinitions;
57
58 public CreatePatchSubStoragesCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable<PatchTransform> transforms)
59 {
60 this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
61 this.Messaging = messaging;
62 this.BackendHelper = backendHelper;
63 this.Intermediate = intermediate;
64 this.Transforms = transforms;
65 }
66
67 private IMessaging Messaging { get; }
68
69 private IBackendHelper BackendHelper { get; }
70
71 private Intermediate Intermediate { get; }
72
73 private IEnumerable<PatchTransform> Transforms { get; }
74
75 public IEnumerable<SubStorage> SubStorages { get; private set; }
76
77 public IEnumerable<SubStorage> Execute()
78 {
79 var subStorages = new List<SubStorage>();
80
81 if (this.Transforms == null || !this.Transforms.Any())
82 {
83 this.Messaging.Write(ErrorMessages.PatchWithoutTransforms());
84 return subStorages;
85 }
86
87 var summaryInfo = this.ExtractPatchSummaryInfo();
88
89 var section = this.Intermediate.Sections.First();
90
91 var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList();
92
93 // Get the patch id from the WixPatchId symbol.
94 var patchSymbol = symbols.OfType<WixPatchSymbol>().FirstOrDefault();
95
96 if (String.IsNullOrEmpty(patchSymbol.Id?.Id))
97 {
98 this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp());
99 return subStorages;
100 }
101
102 if (String.IsNullOrEmpty(patchSymbol.ClientPatchId))
103 {
104 this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp());
105 return subStorages;
106 }
107
108 // enumerate patch.Media to map diskId to Media row
109 var patchMediaByDiskId = symbols.OfType<MediaSymbol>().ToDictionary(t => t.DiskId);
110
111 if (patchMediaByDiskId.Count == 0)
112 {
113 this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp());
114 return subStorages;
115 }
116
117 // populate MSP summary information
118 var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol);
119
120 // enumerate transforms
121 var productCodes = new SortedSet<string>();
122 var transformNames = new List<string>();
123 var validTransform = new List<Tuple<string, WindowsInstallerData>>();
124
125 var baselineSymbolsById = symbols.OfType<WixPatchBaselineSymbol>().ToDictionary(t => t.Id.Id);
126
127 foreach (var mainTransform in this.Transforms)
128 {
129 // Validate the transform doesn't break any patch specific rules.
130 this.Validate(mainTransform);
131
132 // ensure consistent File.Sequence within each Media
133 var baselineSymbol = baselineSymbolsById[mainTransform.Baseline];
134 var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId];
135
136 // Ensure that files are sequenced after the last file in any transform.
137 if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable))
138 {
139 foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
140 {
141 if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence)
142 {
143 // The Binder will pre-increment the sequence.
144 mediaSymbol.LastSequence = transformMediaRow.LastSequence;
145 }
146 }
147 }
148
149 // Use the Media/@DiskId if greater than the last sequence for backward compatibility.
150 if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId)
151 {
152 mediaSymbol.LastSequence = mediaSymbol.DiskId;
153 }
154
155 // Ignore media table in the transform.
156 mainTransform.Transform.Tables.Remove("Media");
157 mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
158
159 var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode);
160
161 productCode = productCode.ToUpperInvariant();
162 productCodes.Add(productCode);
163 validTransform.Add(Tuple.Create(productCode, mainTransform.Transform));
164
165 // Attach the main and paired transforms to the patch object.
166 var baseTransformName = mainTransform.Baseline;
167 var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture);
168
169 if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName)
170 {
171 var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length);
172
173 this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName));
174
175 baseTransformName = trimmedTransformName;
176 }
177
178 var transformName = baseTransformName + countSuffix;
179 subStorages.Add(new SubStorage(transformName, mainTransform.Transform));
180 transformNames.Add(":" + transformName);
181
182 var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName;
183 subStorages.Add(new SubStorage(pairedTransformName, pairedTransform));
184 transformNames.Add(":" + pairedTransformName);
185 }
186
187 if (validTransform.Count == 0)
188 {
189 this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms());
190 return subStorages;
191 }
192
193 // Validate that a patch authored as removable is actually removable
194 if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1")
195 {
196 var uninstallable = true;
197
198 foreach (var entry in validTransform)
199 {
200 uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2);
201 }
202
203 if (!uninstallable)
204 {
205 this.Messaging.Write(ErrorMessages.PatchNotRemovable());
206 return subStorages;
207 }
208 }
209
210 // Finish filling tables with transform-dependent data.
211 productCodes = FinalizePatchProductCodes(symbols, productCodes);
212
213 // Semicolon delimited list of the product codes that can accept the patch.
214 summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
215 {
216 PropertyId = SummaryInformationType.PatchProductCodes,
217 Value = String.Join(";", productCodes)
218 });
219
220 // Semicolon delimited list of transform substorage names in the order they are applied.
221 summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
222 {
223 PropertyId = SummaryInformationType.TransformNames,
224 Value = String.Join(";", transformNames)
225 });
226
227 // Put the summary information that was extracted back in now that it is updated.
228 foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId))
229 {
230 section.AddSymbol(readSummaryInfo);
231 }
232
233 this.SubStorages = subStorages;
234
235 return subStorages;
236 }
237
238 private Dictionary<SummaryInformationType, SummaryInformationSymbol> ExtractPatchSummaryInfo()
239 {
240 var result = new Dictionary<SummaryInformationType, SummaryInformationSymbol>();
241
242 foreach (var section in this.Intermediate.Sections)
243 {
244 // Remove all summary information from the symbols and remember those that
245 // are not calculated or reserved.
246 foreach (var patchSummaryInfo in section.Symbols.OfType<SummaryInformationSymbol>().ToList())
247 {
248 section.RemoveSymbol(patchSummaryInfo);
249
250 if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes &&
251 patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode &&
252 patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement &&
253 patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 &&
254 patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 &&
255 patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16)
256 {
257 result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo);
258 }
259 }
260 }
261
262 return result;
263 }
264
265 private Dictionary<string, MsiPatchMetadataSymbol> PopulateSummaryInformation(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, List<IntermediateSymbol> symbols, WixPatchSymbol patchSymbol)
266 {
267 // PID_CODEPAGE
268 if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage))
269 {
270 // Set the code page by default to the same code page for the
271 // string pool in the database.
272 AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers);
273 }
274
275 // GUID patch code for the patch.
276 AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers);
277
278 // Indicates the minimum Windows Installer version that is required to install the patch.
279 AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers);
280
281 if (!summaryInfo.ContainsKey(SummaryInformationType.Security))
282 {
283 AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced;
284 }
285
286 // Use authored comments or default to display name.
287 MsiPatchMetadataSymbol commentsSymbol = null;
288
289 var metadataSymbols = symbols.OfType<MsiPatchMetadataSymbol>().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property);
290
291 if (!summaryInfo.ContainsKey(SummaryInformationType.Title) &&
292 metadataSymbols.TryGetValue("DisplayName", out var displayName))
293 {
294 AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers);
295
296 // Default comments to use display name as-is.
297 commentsSymbol = displayName;
298 }
299
300 // TODO: This code below seems unnecessary given the codepage is set at the top of this method.
301 //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) &&
302 // metadataValues.TryGetValue("CodePage", out var codepage))
303 //{
304 // AddSummaryInformation(SummaryInformationType.Codepage, codepage);
305 //}
306
307 if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) &&
308 metadataSymbols.TryGetValue("Description", out var description))
309 {
310 AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers);
311 }
312
313 if (!summaryInfo.ContainsKey(SummaryInformationType.Author) &&
314 metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer))
315 {
316 AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers);
317 }
318
319 // Special metadata marshalled through the build.
320 //var wixMetadataValues = symbols.OfType<WixPatchMetadataSymbol>().ToDictionary(t => t.Id.Id, t => t.Value);
321
322 //if (wixMetadataValues.TryGetValue("Comments", out var wixComments))
323 if (metadataSymbols.TryGetValue("Comments", out var wixComments))
324 {
325 commentsSymbol = wixComments;
326 }
327
328 // Write the package comments to summary info.
329 if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) &&
330 commentsSymbol != null)
331 {
332 AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers);
333 }
334
335 return metadataSymbols;
336
337 void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber)
338 {
339 summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber)
340 {
341 PropertyId = type,
342 Value = value
343 });
344 }
345 }
346
347 /// <summary>
348 /// Ensure transform is uninstallable.
349 /// </summary>
350 /// <param name="productCode">Product code in transform.</param>
351 /// <param name="transform">Transform generated by torch.</param>
352 /// <returns>True if the transform is uninstallable</returns>
353 private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform)
354 {
355 var success = true;
356
357 foreach (var tableName in PatchUninstallBreakingTables)
358 {
359 if (transform.TryGetTable(tableName, out var table))
360 {
361 foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add))
362 {
363 success = false;
364
365 var primaryKey = row.GetPrimaryKey('/') ?? String.Empty;
366
367 this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey));
368 }
369 }
370 }
371
372 return success;
373 }
374
375 private void Validate(PatchTransform patchTransform)
376 {
377 var transformPath = patchTransform.Baseline;
378 var transform = patchTransform.Transform;
379
380 // Changing the ProdocutCode in a patch transform is not recommended.
381 if (transform.TryGetTable("Property", out var propertyTable))
382 {
383 foreach (var row in propertyTable.Rows)
384 {
385 // Only interested in modified rows; fast check.
386 if (RowOperation.Modify == row.Operation &&
387 "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal))
388 {
389 this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended());
390 }
391 }
392 }
393
394 // If there is nothing in the component table we can return early because the remaining checks are component based.
395 if (!transform.TryGetTable("Component", out var componentTable))
396 {
397 return;
398 }
399
400 // Index Feature table row operations
401 var featureOps = new Dictionary<string, RowOperation>();
402 if (transform.TryGetTable("Feature", out var featureTable))
403 {
404 foreach (var row in featureTable.Rows)
405 {
406 featureOps[row.FieldAsString(0)] = row.Operation;
407 }
408 }
409
410 // Index Component table and check for keypath modifications
411 var componentKeyPath = new Dictionary<string, string>();
412 var deletedComponent = new Dictionary<string, Row>();
413 foreach (var row in componentTable.Rows)
414 {
415 var id = row.FieldAsString(0);
416 var keypath = row.FieldAsString(5) ?? String.Empty;
417
418 componentKeyPath.Add(id, keypath);
419
420 if (RowOperation.Delete == row.Operation)
421 {
422 deletedComponent.Add(id, row);
423 }
424 else if (RowOperation.Modify == row.Operation)
425 {
426 if (row.Fields[1].Modified)
427 {
428 // Changing the guid of a component is equal to deleting the old one and adding a new one.
429 deletedComponent.Add(id, row);
430 }
431
432 // If the keypath is modified its an error
433 if (row.Fields[5].Modified)
434 {
435 this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath));
436 }
437 }
438 }
439
440 // Verify changes in the file table
441 if (transform.TryGetTable("File", out var fileTable))
442 {
443 var componentWithChangedKeyPath = new Dictionary<string, string>();
444 foreach (FileRow row in fileTable.Rows)
445 {
446 if (RowOperation.None == row.Operation)
447 {
448 continue;
449 }
450
451 var fileId = row.File;
452 var componentId = row.Component;
453
454 // If this file is the keypath of a component
455 if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal))
456 {
457 if (row.Fields[2].Modified)
458 {
459 // You can't change the filename of a file that is the keypath of a component.
460 this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath));
461 }
462
463 if (!componentWithChangedKeyPath.ContainsKey(componentId))
464 {
465 componentWithChangedKeyPath.Add(componentId, fileId);
466 }
467 }
468
469 if (RowOperation.Delete == row.Operation)
470 {
471 // If the file is removed from a component that is not deleted.
472 if (!deletedComponent.ContainsKey(componentId))
473 {
474 var foundRemoveFileEntry = false;
475 var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true);
476
477 if (transform.TryGetTable("RemoveFile", out var removeFileTable))
478 {
479 foreach (var removeFileRow in removeFileTable.Rows)
480 {
481 if (RowOperation.Delete == removeFileRow.Operation)
482 {
483 continue;
484 }
485
486 if (componentId == removeFileRow.FieldAsString(1))
487 {
488 // Check if there is a RemoveFile entry for this file
489 if (null != removeFileRow[2])
490 {
491 var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true);
492
493 // Convert the MSI format for a wildcard string to Regex format.
494 removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\.");
495
496 var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
497 if (regex.IsMatch(filename))
498 {
499 foundRemoveFileEntry = true;
500 break;
501 }
502 }
503 }
504 }
505 }
506
507 if (!foundRemoveFileEntry)
508 {
509 this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId));
510 }
511 }
512 }
513 }
514 }
515
516 var featureComponentsTable = transform.Tables["FeatureComponents"];
517
518 if (0 < deletedComponent.Count)
519 {
520 // Index FeatureComponents table.
521 var featureComponents = new Dictionary<string, List<string>>();
522
523 if (null != featureComponentsTable)
524 {
525 foreach (var row in featureComponentsTable.Rows)
526 {
527 var componentId = row.FieldAsString(1);
528
529 if (!featureComponents.TryGetValue(componentId, out var features))
530 {
531 features = new List<string>();
532 featureComponents.Add(componentId, features);
533 }
534
535 features.Add(row.FieldAsString(0));
536 }
537 }
538
539 // Check to make sure if a component was deleted, the feature was too.
540 foreach (var entry in deletedComponent)
541 {
542 if (featureComponents.TryGetValue(entry.Key, out var features))
543 {
544 foreach (var featureId in features)
545 {
546 if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete)
547 {
548 // The feature was not deleted.
549 this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath));
550 }
551 }
552 }
553 }
554 }
555
556 // Warn if new components are added to existing features
557 if (null != featureComponentsTable)
558 {
559 foreach (var row in featureComponentsTable.Rows)
560 {
561 if (RowOperation.Add == row.Operation)
562 {
563 // Check if the feature is in the Feature table
564 var feature_ = row.FieldAsString(0);
565 var component_ = row.FieldAsString(1);
566
567 // Features may not be present if not referenced
568 if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_])
569 {
570 this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath));
571 }
572 }
573 }
574 }
575 }
576
577 /// <summary>
578 /// Create the #transform for the given main transform.
579 /// </summary>
580 private WindowsInstallerData BuildPairedTransform(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, Dictionary<string, MsiPatchMetadataSymbol> patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode)
581 {
582 productCode = null;
583
584 var pairedTransform = new WindowsInstallerData(null)
585 {
586 Type = OutputType.Transform,
587 Codepage = mainTransform.Codepage
588 };
589
590 // lookup productVersion property to correct summaryInformation
591 var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1);
592
593 var mainSummaryTable = mainTransform.Tables["_SummaryInformation"];
594 var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0));
595
596 var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture);
597
598 if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags))
599 {
600 var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers);
601 mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags;
602 mainSummaryRow[1] = baselineValidationFlags;
603 }
604
605 // copy summary information from core transform
606 var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
607
608 foreach (var mainSummaryRow in mainSummaryTable.Rows)
609 {
610 var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0);
611 var value = mainSummaryRow.FieldAsString(1);
612 switch (type)
613 {
614 case SummaryInformationType.TransformProductCodes:
615 var propertyData = value.Split(';');
616 var oldProductVersion = propertyData[0].Substring(38);
617 var upgradeCode = propertyData[2];
618 productCode = propertyData[0].Substring(0, 38);
619
620 if (newProductVersion == null)
621 {
622 newProductVersion = oldProductVersion;
623 }
624
625 // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade'
626 mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
627 value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
628 break;
629 case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML.
630 value = baselineValidationFlags;
631 mainSummaryRow[1] = value;
632 break;
633 }
634
635 var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers);
636 pairedSummaryRow[0] = mainSummaryRow[0];
637 pairedSummaryRow[1] = value;
638 }
639
640 if (productCode == null)
641 {
642 this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo());
643 return null;
644 }
645
646 // Copy File table
647 if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count)
648 {
649 var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
650
651 foreach (var mainFileRow in mainFileTable.Rows.Cast<FileRow>())
652 {
653 // Set File.Sequence to non null to satisfy transform bind and suppress any
654 // change to File.Sequence to avoid bloat.
655 mainFileRow.Sequence = 1;
656 mainFileRow.Fields[7].Modified = false;
657
658 // Override authored media to the media provided in the patch.
659 mainFileRow.DiskId = mediaSymbol.DiskId;
660
661 // Delete's don't need rows in the paired transform.
662 if (mainFileRow.Operation == RowOperation.Delete)
663 {
664 continue;
665 }
666
667 var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers);
668 pairedFileRow.Operation = RowOperation.Modify;
669 mainFileRow.CopyTo(pairedFileRow);
670
671 // Force modified File rows to appear in the transform.
672 switch (mainFileRow.Operation)
673 {
674 case RowOperation.Modify:
675 case RowOperation.Add:
676 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
677 pairedFileRow.Fields[6].Modified = true;
678 pairedFileRow.Operation = mainFileRow.Operation;
679 break;
680 default:
681 pairedFileRow.Fields[6].Modified = false;
682 break;
683 }
684 }
685 }
686
687 // Add Media row to pairedTransform
688 var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
689 var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers);
690 pairedMediaRow.Operation = RowOperation.Add;
691 pairedMediaRow.DiskId = mediaSymbol.DiskId;
692 pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0;
693 pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt;
694 pairedMediaRow.Cabinet = mediaSymbol.Cabinet;
695 pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel;
696 pairedMediaRow.Source = mediaSymbol.Source;
697
698 // Add PatchPackage for this Media
699 var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
700 pairedPackageTable.Operation = TableOperation.Add;
701 var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers);
702 pairedPackageRow.Operation = RowOperation.Add;
703 pairedPackageRow[0] = patchIdSymbol.Id.Id;
704 pairedPackageRow[1] = mediaSymbol.DiskId;
705
706 // Add the property to the patch transform's Property table.
707 var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]);
708 pairedPropertyTable.Operation = TableOperation.Add;
709
710 // Add property to both identify client patches and whether those patches are removable or not
711 patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol);
712
713 var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers);
714 pairedPropertyRow.Operation = RowOperation.Add;
715 pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval");
716 pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0";
717
718 // Add this patch code GUID to the patch transform to identify
719 // which patches are installed, including in multi-patch
720 // installations.
721 pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
722 pairedPropertyRow.Operation = RowOperation.Add;
723 pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode");
724 pairedPropertyRow[1] = patchIdSymbol.Id.Id;
725
726 // Add PATCHNEWPACKAGECODE to apply to admin layouts.
727 pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
728 pairedPropertyRow.Operation = RowOperation.Add;
729 pairedPropertyRow[0] = "PATCHNEWPACKAGECODE";
730 pairedPropertyRow[1] = patchIdSymbol.Id.Id;
731
732 // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts.
733 if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol))
734 {
735 pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers);
736 pairedPropertyRow.Operation = RowOperation.Add;
737 pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT";
738 pairedPropertyRow[1] = subjectSymbol.Value;
739 }
740
741 if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol))
742 {
743 pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers);
744 pairedPropertyRow.Operation = RowOperation.Add;
745 pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS";
746 pairedPropertyRow[1] = commentsSymbol.Value;
747 }
748
749 return pairedTransform;
750 }
751
752 private static SortedSet<string> FinalizePatchProductCodes(List<IntermediateSymbol> symbols, SortedSet<string> productCodes)
753 {
754 var patchTargetSymbols = symbols.OfType<WixPatchTargetSymbol>().ToList();
755
756 if (patchTargetSymbols.Any())
757 {
758 var targets = new SortedSet<string>();
759 var replace = true;
760 foreach (var wixPatchTargetRow in patchTargetSymbols)
761 {
762 var target = wixPatchTargetRow.ProductCode.ToUpperInvariant();
763 if (target == "*")
764 {
765 replace = false;
766 }
767 else
768 {
769 targets.Add(target);
770 }
771 }
772
773 // Replace the target ProductCodes with the authored list.
774 if (replace)
775 {
776 productCodes = targets;
777 }
778 else
779 {
780 // Copy the authored target ProductCodes into the list.
781 foreach (var target in targets)
782 {
783 productCodes.Add(target);
784 }
785 }
786 }
787
788 return productCodes;
789 }
790 }
791 }