aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2020-01-24 15:27:20 -0800
committerRob Mensching <rob@firegiant.com>2020-02-05 16:15:47 -0800
commit6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d (patch)
treec717333cd10d5592e59dfb898b391275bba1f389 /src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
parent6e2e67ab55c75f4655397588c0dcc64f50d22f92 (diff)
downloadwix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.gz
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.bz2
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.zip
Start on new patch infrastructure
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs1322
1 files changed, 1322 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
new file mode 100644
index 00000000..aa5ca20a
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
@@ -0,0 +1,1322 @@
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.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.WindowsInstaller;
11 using WixToolset.Core.WindowsInstaller.Msi;
12 using WixToolset.Data;
13 using WixToolset.Data.Tuples;
14 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Data.WindowsInstaller.Rows;
16 using WixToolset.Extensibility.Services;
17
18 /// <summary>
19 /// Include transforms in a patch.
20 /// </summary>
21 internal class AttachPatchTransformsCommand
22 {
23 private static readonly string[] PatchUninstallBreakingTables = new[]
24 {
25 "AppId",
26 "BindImage",
27 "Class",
28 "Complus",
29 "CreateFolder",
30 "DuplicateFile",
31 "Environment",
32 "Extension",
33 "Font",
34 "IniFile",
35 "IsolatedComponent",
36 "LockPermissions",
37 "MIME",
38 "MoveFile",
39 "MsiLockPermissionsEx",
40 "MsiServiceConfig",
41 "MsiServiceConfigFailureActions",
42 "ODBCAttribute",
43 "ODBCDataSource",
44 "ODBCDriver",
45 "ODBCSourceAttribute",
46 "ODBCTranslator",
47 "ProgId",
48 "PublishComponent",
49 "RemoveIniFile",
50 "SelfReg",
51 "ServiceControl",
52 "ServiceInstall",
53 "TypeLib",
54 "Verb",
55 };
56
57 private readonly TableDefinitionCollection tableDefinitions;
58
59 public AttachPatchTransformsCommand(IMessaging messaging, Intermediate intermediate, IEnumerable<PatchTransform> transforms)
60 {
61 this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandardInternal.GetTableDefinitions());
62 this.Messaging = messaging;
63 this.Intermediate = intermediate;
64 this.Transforms = transforms;
65 }
66
67 private IMessaging Messaging { get; }
68
69 private Intermediate Intermediate { get; }
70
71 private IEnumerable<PatchTransform> Transforms { get; }
72
73 public IEnumerable<SubStorage> SubStorages { get; private set; }
74
75 public IEnumerable<SubStorage> Execute()
76 {
77 var subStorages = new List<SubStorage>();
78
79 if (this.Transforms == null || !this.Transforms.Any())
80 {
81 this.Messaging.Write(ErrorMessages.PatchWithoutTransforms());
82 return subStorages;
83 }
84
85 var summaryInfo = this.ExtractPatchSummaryInfo();
86
87 var section = this.Intermediate.Sections.First();
88
89 var tuples = this.Intermediate.Sections.SelectMany(s => s.Tuples).ToList();
90
91 // Get the patch id from the WixPatchId tuple.
92 var patchIdTuple = tuples.OfType<WixPatchIdTuple>().FirstOrDefault();
93
94 if (String.IsNullOrEmpty(patchIdTuple.Id?.Id))
95 {
96 this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp());
97 return subStorages;
98 }
99
100 if (String.IsNullOrEmpty(patchIdTuple.ClientPatchId))
101 {
102 this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp());
103 return subStorages;
104 }
105
106 // enumerate patch.Media to map diskId to Media row
107 var patchMediaByDiskId = tuples.OfType<MediaTuple>().ToDictionary(t => t.DiskId);
108
109 if (patchMediaByDiskId.Count == 0)
110 {
111 this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp());
112 return subStorages;
113 }
114
115 // populate MSP summary information
116 var patchMetadata = this.PopulateSummaryInformation(summaryInfo, tuples, patchIdTuple, section.Codepage);
117
118 // enumerate transforms
119 var productCodes = new SortedSet<string>();
120 var transformNames = new List<string>();
121 var validTransform = new List<Tuple<string, WindowsInstallerData>>();
122
123 var baselineTuplesById = tuples.OfType<WixPatchBaselineTuple>().ToDictionary(t => t.Id.Id);
124
125 foreach (var mainTransform in this.Transforms)
126 {
127 var baselineTuple = baselineTuplesById[mainTransform.Baseline];
128
129 var patchRefTuples = tuples.OfType<WixPatchRefTuple>().ToList();
130 if (patchRefTuples.Count > 0)
131 {
132 if (!this.ReduceTransform(mainTransform.Transform, patchRefTuples))
133 {
134 // transform has none of the content authored into this patch
135 continue;
136 }
137 }
138
139 // Validate the transform doesn't break any patch specific rules.
140 this.Validate(mainTransform);
141
142 // ensure consistent File.Sequence within each Media
143 var mediaTuple = patchMediaByDiskId[baselineTuple.DiskId];
144
145 // Ensure that files are sequenced after the last file in any transform.
146 var transformMediaTable = mainTransform.Transform.Tables["Media"];
147 if (null != transformMediaTable && 0 < transformMediaTable.Rows.Count)
148 {
149 foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
150 {
151 if (mediaTuple.LastSequence < transformMediaRow.LastSequence)
152 {
153 // The Binder will pre-increment the sequence.
154 mediaTuple.LastSequence = transformMediaRow.LastSequence;
155 }
156 }
157 }
158
159 // Use the Media/@DiskId if greater than the last sequence for backward compatibility.
160 if (mediaTuple.LastSequence < mediaTuple.DiskId)
161 {
162 mediaTuple.LastSequence = mediaTuple.DiskId;
163 }
164
165 // Ignore media table in the transform.
166 mainTransform.Transform.Tables.Remove("Media");
167 mainTransform.Transform.Tables.Remove("WixMedia");
168 mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
169
170 var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchIdTuple, mainTransform.Transform, mediaTuple, baselineTuple, out var productCode);
171
172 productCode = productCode.ToUpperInvariant();
173 productCodes.Add(productCode);
174 validTransform.Add(Tuple.Create(productCode, mainTransform.Transform));
175
176 // attach these transforms to the patch object
177 // TODO: is this an acceptable way to auto-generate transform stream names?
178 var transformName = mainTransform.Baseline + "." + validTransform.Count.ToString(CultureInfo.InvariantCulture);
179 subStorages.Add(new SubStorage(transformName, mainTransform.Transform));
180 subStorages.Add(new SubStorage("#" + transformName, pairedTransform));
181
182 transformNames.Add(":" + transformName);
183 transformNames.Add(":#" + transformName);
184 }
185
186 if (validTransform.Count == 0)
187 {
188 this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms());
189 return subStorages;
190 }
191
192 // Validate that a patch authored as removable is actually removable
193 if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1")
194 {
195 var uninstallable = true;
196
197 foreach (var entry in validTransform)
198 {
199 uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2);
200 }
201
202 if (!uninstallable)
203 {
204 this.Messaging.Write(ErrorMessages.PatchNotRemovable());
205 return subStorages;
206 }
207 }
208
209 // Finish filling tables with transform-dependent data.
210 productCodes = FinalizePatchProductCodes(tuples, productCodes);
211
212 // Semicolon delimited list of the product codes that can accept the patch.
213 summaryInfo.Add(SumaryInformationType.PatchProductCodes, new SummaryInformationTuple(patchIdTuple.SourceLineNumbers)
214 {
215 PropertyId = SumaryInformationType.PatchProductCodes,
216 Value = String.Join(";", productCodes)
217 });
218
219 // Semicolon delimited list of transform substorage names in the order they are applied.
220 summaryInfo.Add(SumaryInformationType.TransformNames, new SummaryInformationTuple(patchIdTuple.SourceLineNumbers)
221 {
222 PropertyId = SumaryInformationType.TransformNames,
223 Value = String.Join(";", transformNames)
224 });
225
226 // Put the summary information that was extracted back in now that it is updated.
227 foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId))
228 {
229 section.Tuples.Add(readSummaryInfo);
230 }
231
232 this.SubStorages = subStorages;
233
234 return subStorages;
235 }
236
237 private Dictionary<SumaryInformationType, SummaryInformationTuple> ExtractPatchSummaryInfo()
238 {
239 var result = new Dictionary<SumaryInformationType, SummaryInformationTuple>();
240
241 foreach (var section in this.Intermediate.Sections)
242 {
243 for (var i = section.Tuples.Count - 1; i >= 0; i--)
244 {
245 if (section.Tuples[i] is SummaryInformationTuple patchSummaryInfo)
246 {
247 // Remove all summary information from the tuples and remember those that
248 // are not calculated or reserved.
249 section.Tuples.RemoveAt(i);
250
251 if (patchSummaryInfo.PropertyId != SumaryInformationType.PatchProductCodes &&
252 patchSummaryInfo.PropertyId != SumaryInformationType.PatchCode &&
253 patchSummaryInfo.PropertyId != SumaryInformationType.PatchInstallerRequirement &&
254 patchSummaryInfo.PropertyId != SumaryInformationType.Reserved11 &&
255 patchSummaryInfo.PropertyId != SumaryInformationType.Reserved14 &&
256 patchSummaryInfo.PropertyId != SumaryInformationType.Reserved16)
257 {
258 result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo);
259 }
260 }
261 }
262 }
263
264 return result;
265 }
266
267 private Dictionary<string, MsiPatchMetadataTuple> PopulateSummaryInformation(Dictionary<SumaryInformationType, SummaryInformationTuple> summaryInfo, List<IntermediateTuple> tuples, WixPatchIdTuple patchIdTuple, int codepage)
268 {
269 // PID_CODEPAGE
270 if (!summaryInfo.ContainsKey(SumaryInformationType.Codepage))
271 {
272 // Set the code page by default to the same code page for the
273 // string pool in the database.
274 AddSummaryInformation(SumaryInformationType.Codepage, codepage.ToString(CultureInfo.InvariantCulture), patchIdTuple.SourceLineNumbers);
275 }
276
277 // GUID patch code for the patch.
278 AddSummaryInformation(SumaryInformationType.PatchCode, patchIdTuple.Id.Id, patchIdTuple.SourceLineNumbers);
279
280 // Indicates the minimum Windows Installer version that is required to install the patch.
281 AddSummaryInformation(SumaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchIdTuple.SourceLineNumbers);
282
283 if (!summaryInfo.ContainsKey(SumaryInformationType.Security))
284 {
285 AddSummaryInformation(SumaryInformationType.Security, "4", patchIdTuple.SourceLineNumbers); // Read-only enforced;
286 }
287
288 // Use authored comments or default to display name.
289 MsiPatchMetadataTuple commentsTuple = null;
290
291 var metadataTuples = tuples.OfType<MsiPatchMetadataTuple>().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property);
292
293 if (!summaryInfo.ContainsKey(SumaryInformationType.Title) &&
294 metadataTuples.TryGetValue("DisplayName", out var displayName))
295 {
296 AddSummaryInformation(SumaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers);
297
298 // Default comments to use display name as-is.
299 commentsTuple = displayName;
300 }
301
302 // TODO: This code below seems unnecessary given the codepage is set at the top of this method.
303 //if (!summaryInfo.ContainsKey(SumaryInformationType.Codepage) &&
304 // metadataValues.TryGetValue("CodePage", out var codepage))
305 //{
306 // AddSummaryInformation(SumaryInformationType.Codepage, codepage);
307 //}
308
309 if (!summaryInfo.ContainsKey(SumaryInformationType.PatchPackageName) &&
310 metadataTuples.TryGetValue("Description", out var description))
311 {
312 AddSummaryInformation(SumaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers);
313 }
314
315 if (!summaryInfo.ContainsKey(SumaryInformationType.Author) &&
316 metadataTuples.TryGetValue("ManufacturerName", out var manufacturer))
317 {
318 AddSummaryInformation(SumaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers);
319 }
320
321 // Special metadata marshalled through the build.
322 //var wixMetadataValues = tuples.OfType<WixPatchMetadataTuple>().ToDictionary(t => t.Id.Id, t => t.Value);
323
324 //if (wixMetadataValues.TryGetValue("Comments", out var wixComments))
325 if (metadataTuples.TryGetValue("Comments", out var wixComments))
326 {
327 commentsTuple = wixComments;
328 }
329
330 // Write the package comments to summary info.
331 if (!summaryInfo.ContainsKey(SumaryInformationType.Comments) &&
332 commentsTuple != null)
333 {
334 AddSummaryInformation(SumaryInformationType.Comments, commentsTuple.Value, commentsTuple.SourceLineNumbers);
335 }
336
337 return metadataTuples;
338
339 void AddSummaryInformation(SumaryInformationType type, string value, SourceLineNumber sourceLineNumber)
340 {
341 summaryInfo.Add(type, new SummaryInformationTuple(sourceLineNumber)
342 {
343 PropertyId = type,
344 Value = value
345 });
346 }
347 }
348
349 /// <summary>
350 /// Ensure transform is uninstallable.
351 /// </summary>
352 /// <param name="productCode">Product code in transform.</param>
353 /// <param name="transform">Transform generated by torch.</param>
354 /// <returns>True if the transform is uninstallable</returns>
355 private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform)
356 {
357 var success = true;
358
359 foreach (var tableName in PatchUninstallBreakingTables)
360 {
361 if (transform.TryGetTable(tableName, out var table))
362 {
363 foreach (var row in table.Rows)
364 {
365 if (row.Operation == RowOperation.Add)
366 {
367 success = false;
368
369 var primaryKey = row.GetPrimaryKey('/') ?? String.Empty;
370
371 this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey));
372 }
373 }
374 }
375 }
376
377 return success;
378 }
379
380 /// <summary>
381 /// Reduce the transform according to the patch references.
382 /// </summary>
383 /// <param name="transform">transform generated by torch.</param>
384 /// <param name="patchRefTuples">Table contains patch family filter.</param>
385 /// <returns>true if the transform is not empty</returns>
386 private bool ReduceTransform(WindowsInstallerData transform, IEnumerable<WixPatchRefTuple> patchRefTuples)
387 {
388 // identify sections to keep
389 var oldSections = new Dictionary<string, Row>();
390 var newSections = new Dictionary<string, Row>();
391 var tableKeyRows = new Dictionary<string, Dictionary<string, Row>>();
392 var sequenceList = new List<Table>();
393 var componentFeatureAddsIndex = new Dictionary<string, List<string>>();
394 var customActionTable = new Dictionary<string, Row>();
395 var directoryTableAdds = new Dictionary<string, Row>();
396 var featureTableAdds = new Dictionary<string, Row>();
397 var keptComponents = new Dictionary<string, Row>();
398 var keptDirectories = new Dictionary<string, Row>();
399 var keptFeatures = new Dictionary<string, Row>();
400 var keptLockPermissions = new HashSet<string>();
401 var keptMsiLockPermissionExs = new HashSet<string>();
402
403 var componentCreateFolderIndex = new Dictionary<string, List<string>>();
404 var directoryLockPermissionsIndex = new Dictionary<string, List<Row>>();
405 var directoryMsiLockPermissionsExIndex = new Dictionary<string, List<Row>>();
406
407 foreach (var patchRefTuple in patchRefTuples)
408 {
409 var tableName = patchRefTuple.Table;
410 var key = patchRefTuple.PrimaryKeys;
411
412 // Short circuit filtering if all changes should be included.
413 if ("*" == tableName && "*" == key)
414 {
415 RemoveProductCodeFromTransform(transform);
416 return true;
417 }
418
419 if (!transform.Tables.TryGetTable(tableName, out var table))
420 {
421 // Table not found.
422 continue;
423 }
424
425 // Index the table.
426 if (!tableKeyRows.TryGetValue(tableName, out var keyRows))
427 {
428 keyRows = new Dictionary<string, Row>();
429 tableKeyRows.Add(tableName, keyRows);
430
431 foreach (var newRow in table.Rows)
432 {
433 var primaryKey = newRow.GetPrimaryKey();
434 keyRows.Add(primaryKey, newRow);
435 }
436 }
437
438 if (!keyRows.TryGetValue(key, out var row))
439 {
440 // Row not found.
441 continue;
442 }
443
444 // Differ.sectionDelimiter
445 var sections = row.SectionId.Split('/');
446 oldSections[sections[0]] = row;
447 newSections[sections[1]] = row;
448 }
449
450 // throw away sections not referenced
451 var keptRows = 0;
452 Table directoryTable = null;
453 Table featureTable = null;
454 Table lockPermissionsTable = null;
455 Table msiLockPermissionsTable = null;
456
457 foreach (var table in transform.Tables)
458 {
459 if ("_SummaryInformation" == table.Name)
460 {
461 continue;
462 }
463
464 if (table.Name == "AdminExecuteSequence"
465 || table.Name == "AdminUISequence"
466 || table.Name == "AdvtExecuteSequence"
467 || table.Name == "InstallUISequence"
468 || table.Name == "InstallExecuteSequence")
469 {
470 sequenceList.Add(table);
471 continue;
472 }
473
474 for (var i = 0; i < table.Rows.Count; i++)
475 {
476 var row = table.Rows[i];
477
478 if (table.Name == "CreateFolder")
479 {
480 var createFolderComponentId = row.FieldAsString(1);
481
482 if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList))
483 {
484 directoryList = new List<string>();
485 componentCreateFolderIndex.Add(createFolderComponentId, directoryList);
486 }
487
488 directoryList.Add(row.FieldAsString(0));
489 }
490
491 if (table.Name == "CustomAction")
492 {
493 customActionTable.Add(row.FieldAsString(0), row);
494 }
495
496 if (table.Name == "Directory")
497 {
498 directoryTable = table;
499 if (RowOperation.Add == row.Operation)
500 {
501 directoryTableAdds.Add(row.FieldAsString(0), row);
502 }
503 }
504
505 if (table.Name == "Feature")
506 {
507 featureTable = table;
508 if (RowOperation.Add == row.Operation)
509 {
510 featureTableAdds.Add(row.FieldAsString(0), row);
511 }
512 }
513
514 if (table.Name == "FeatureComponents")
515 {
516 if (RowOperation.Add == row.Operation)
517 {
518 var featureId = row.FieldAsString(0);
519 var componentId = row.FieldAsString(1);
520
521 if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList))
522 {
523 featureList = new List<string>();
524 componentFeatureAddsIndex.Add(componentId, featureList);
525 }
526
527 featureList.Add(featureId);
528 }
529 }
530
531 if (table.Name == "LockPermissions")
532 {
533 lockPermissionsTable = table;
534 if ("CreateFolder" == row.FieldAsString(1))
535 {
536 var directoryId = row.FieldAsString(0);
537
538 if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList))
539 {
540 rowList = new List<Row>();
541 directoryLockPermissionsIndex.Add(directoryId, rowList);
542 }
543
544 rowList.Add(row);
545 }
546 }
547
548 if (table.Name == "MsiLockPermissionsEx")
549 {
550 msiLockPermissionsTable = table;
551 if ("CreateFolder" == row.FieldAsString(1))
552 {
553 var directoryId = row.FieldAsString(0);
554
555 if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList))
556 {
557 rowList = new List<Row>();
558 directoryMsiLockPermissionsExIndex.Add(directoryId, rowList);
559 }
560
561 rowList.Add(row);
562 }
563 }
564
565 if (null == row.SectionId)
566 {
567 table.Rows.RemoveAt(i);
568 i--;
569 }
570 else
571 {
572 var sections = row.SectionId.Split('/');
573 // ignore the row without section id.
574 if (0 == sections[0].Length && 0 == sections[1].Length)
575 {
576 table.Rows.RemoveAt(i);
577 i--;
578 }
579 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
580 {
581 if ("Component" == table.Name)
582 {
583 keptComponents.Add(row.FieldAsString(0), row);
584 }
585
586 if ("Directory" == table.Name)
587 {
588 keptDirectories.Add(row.FieldAsString(0), row);
589 }
590
591 if ("Feature" == table.Name)
592 {
593 keptFeatures.Add(row.FieldAsString(0), row);
594 }
595
596 keptRows++;
597 }
598 else
599 {
600 table.Rows.RemoveAt(i);
601 i--;
602 }
603 }
604 }
605 }
606
607 keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
608
609 if (null != directoryTable)
610 {
611 foreach (var componentRow in keptComponents.Values)
612 {
613 var componentId = componentRow.FieldAsString(0);
614
615 if (RowOperation.Add == componentRow.Operation)
616 {
617 // Make sure each added component has its required directory and feature heirarchy.
618 var directoryId = componentRow.FieldAsString(2);
619 while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow))
620 {
621 if (!keptDirectories.ContainsKey(directoryId))
622 {
623 directoryTable.Rows.Add(directoryRow);
624 keptDirectories.Add(directoryId, directoryRow);
625 keptRows++;
626 }
627
628 directoryId = directoryRow.FieldAsString(1);
629 }
630
631 if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds))
632 {
633 foreach (var featureId in componentFeatureIds)
634 {
635 var currentFeatureId = featureId;
636 while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow))
637 {
638 if (!keptFeatures.ContainsKey(currentFeatureId))
639 {
640 featureTable.Rows.Add(featureRow);
641 keptFeatures.Add(currentFeatureId, featureRow);
642 keptRows++;
643 }
644
645 currentFeatureId = featureRow.FieldAsString(1);
646 }
647 }
648 }
649 }
650
651 // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept.
652 foreach (var keptComponentId in keptComponents.Keys)
653 {
654 if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList))
655 {
656 foreach (var directoryId in directoryList)
657 {
658 if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList))
659 {
660 foreach (var lockPermissionsRow in lockPermissionsRowList)
661 {
662 var key = lockPermissionsRow.GetPrimaryKey('/');
663 if (keptLockPermissions.Add(key))
664 {
665 lockPermissionsTable.Rows.Add(lockPermissionsRow);
666 keptRows++;
667 }
668 }
669 }
670
671 if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList))
672 {
673 foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList)
674 {
675 var key = msiLockPermissionsExRow.GetPrimaryKey('/');
676 if (keptMsiLockPermissionExs.Add(key))
677 {
678 msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow);
679 keptRows++;
680 }
681 }
682 }
683 }
684 }
685 }
686 }
687 }
688
689 keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
690
691 // Delete tables that are empty.
692 var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name);
693
694 foreach (var tableName in tablesToDelete)
695 {
696 transform.Tables.Remove(tableName);
697 }
698
699 return keptRows > 0;
700 }
701
702 private void Validate(PatchTransform patchTransform)
703 {
704 var transformPath = patchTransform.Baseline; // TODO: this is used in error messages, how best to set it?
705 var transform = patchTransform.Transform;
706
707 // Changing the ProdocutCode in a patch transform is not recommended.
708 if (transform.TryGetTable("Property", out var propertyTable))
709 {
710 foreach (var row in propertyTable.Rows)
711 {
712 // Only interested in modified rows; fast check.
713 if (RowOperation.Modify == row.Operation &&
714 "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal))
715 {
716 this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended());
717 }
718 }
719 }
720
721 // If there is nothing in the component table we can return early because the remaining checks are component based.
722 if (!transform.TryGetTable("Component", out var componentTable))
723 {
724 return;
725 }
726
727 // Index Feature table row operations
728 var featureOps = new Dictionary<string, RowOperation>();
729 if (transform.TryGetTable("Feature", out var featureTable))
730 {
731 foreach (var row in featureTable.Rows)
732 {
733 featureOps[row.FieldAsString(0)] = row.Operation;
734 }
735 }
736
737 // Index Component table and check for keypath modifications
738 var componentKeyPath = new Dictionary<string, string>();
739 var deletedComponent = new Dictionary<string, Row>();
740 foreach (var row in componentTable.Rows)
741 {
742 var id = row.FieldAsString(0);
743 var keypath = row.FieldAsString(5) ?? String.Empty;
744
745 componentKeyPath.Add(id, keypath);
746
747 if (RowOperation.Delete == row.Operation)
748 {
749 deletedComponent.Add(id, row);
750 }
751 else if (RowOperation.Modify == row.Operation)
752 {
753 if (row.Fields[1].Modified)
754 {
755 // Changing the guid of a component is equal to deleting the old one and adding a new one.
756 deletedComponent.Add(id, row);
757 }
758
759 // If the keypath is modified its an error
760 if (row.Fields[5].Modified)
761 {
762 this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath));
763 }
764 }
765 }
766
767 // Verify changes in the file table
768 if (transform.TryGetTable("File", out var fileTable))
769 {
770 var componentWithChangedKeyPath = new Dictionary<string, string>();
771 foreach (var row in fileTable.Rows)
772 {
773 if (RowOperation.None == row.Operation)
774 {
775 continue;
776 }
777
778 var fileId = row.FieldAsString(0);
779 var componentId = row.FieldAsString(1);
780
781 // If this file is the keypath of a component
782 if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal))
783 {
784 if (row.Fields[2].Modified)
785 {
786 // You can't change the filename of a file that is the keypath of a component.
787 this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath));
788 }
789
790 if (!componentWithChangedKeyPath.ContainsKey(componentId))
791 {
792 componentWithChangedKeyPath.Add(componentId, fileId);
793 }
794 }
795
796 if (RowOperation.Delete == row.Operation)
797 {
798 // If the file is removed from a component that is not deleted.
799 if (!deletedComponent.ContainsKey(componentId))
800 {
801 var foundRemoveFileEntry = false;
802 var filename = Common.GetName(row.FieldAsString(2), false, true);
803
804 if (transform.TryGetTable("RemoveFile", out var removeFileTable))
805 {
806 foreach (var removeFileRow in removeFileTable.Rows)
807 {
808 if (RowOperation.Delete == removeFileRow.Operation)
809 {
810 continue;
811 }
812
813 if (componentId == removeFileRow.FieldAsString(1))
814 {
815 // Check if there is a RemoveFile entry for this file
816 if (null != removeFileRow[2])
817 {
818 var removeFileName = Common.GetName(removeFileRow.FieldAsString(2), false, true);
819
820 // Convert the MSI format for a wildcard string to Regex format.
821 removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\.");
822
823 var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
824 if (regex.IsMatch(filename))
825 {
826 foundRemoveFileEntry = true;
827 break;
828 }
829 }
830 }
831 }
832 }
833
834 if (!foundRemoveFileEntry)
835 {
836 this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId));
837 }
838 }
839 }
840 }
841 }
842
843 var featureComponentsTable = transform.Tables["FeatureComponents"];
844
845 if (0 < deletedComponent.Count)
846 {
847 // Index FeatureComponents table.
848 var featureComponents = new Dictionary<string, List<string>>();
849
850 if (null != featureComponentsTable)
851 {
852 foreach (var row in featureComponentsTable.Rows)
853 {
854 var componentId = row.FieldAsString(1);
855
856 if (!featureComponents.TryGetValue(componentId, out var features))
857 {
858 features = new List<string>();
859 featureComponents.Add(componentId, features);
860 }
861
862 features.Add(row.FieldAsString(0));
863 }
864 }
865
866 // Check to make sure if a component was deleted, the feature was too.
867 foreach (var entry in deletedComponent)
868 {
869 if (featureComponents.TryGetValue(entry.Key, out var features))
870 {
871 foreach (var featureId in features)
872 {
873 if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete)
874 {
875 // The feature was not deleted.
876 this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath));
877 }
878 }
879 }
880 }
881 }
882
883 // Warn if new components are added to existing features
884 if (null != featureComponentsTable)
885 {
886 foreach (var row in featureComponentsTable.Rows)
887 {
888 if (RowOperation.Add == row.Operation)
889 {
890 // Check if the feature is in the Feature table
891 var feature_ = row.FieldAsString(0);
892 var component_ = row.FieldAsString(1);
893
894 // Features may not be present if not referenced
895 if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_])
896 {
897 this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath));
898 }
899 }
900 }
901 }
902 }
903
904 /// <summary>
905 /// Remove the ProductCode property from the transform.
906 /// </summary>
907 /// <param name="transform">The transform.</param>
908 /// <remarks>
909 /// Changing the ProductCode is not supported in a patch.
910 /// </remarks>
911 private static void RemoveProductCodeFromTransform(WindowsInstallerData transform)
912 {
913 if (transform.Tables.TryGetTable("Property", out var propertyTable))
914 {
915 for (var i = 0; i < propertyTable.Rows.Count; ++i)
916 {
917 var propertyRow = propertyTable.Rows[i];
918 var property = (string)propertyRow[0];
919
920 if ("ProductCode" == property)
921 {
922 propertyTable.Rows.RemoveAt(i);
923 break;
924 }
925 }
926 }
927 }
928
929 /// <summary>
930 /// Check if the section is in a PatchFamily.
931 /// </summary>
932 /// <param name="oldSection">Section id in target wixout</param>
933 /// <param name="newSection">Section id in upgrade wixout</param>
934 /// <param name="oldSections">Dictionary contains section id should be kept in the baseline wixout.</param>
935 /// <param name="newSections">Dictionary contains section id should be kept in the upgrade wixout.</param>
936 /// <returns>true if section in patch family</returns>
937 private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections)
938 {
939 var result = false;
940
941 if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection)))
942 {
943 result = true;
944 }
945 else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection)))
946 {
947 result = true;
948 }
949
950 return result;
951 }
952
953 /// <summary>
954 /// Reduce the transform sequence tables.
955 /// </summary>
956 /// <param name="sequenceList">ArrayList of tables to be reduced</param>
957 /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
958 /// <param name="newSections">Hashtable contains section id should be kept in the target wixout.</param>
959 /// <param name="customAction">Hashtable contains all the rows in the CustomAction table.</param>
960 /// <returns>Number of rows left</returns>
961 private static int ReduceTransformSequenceTable(List<Table> sequenceList, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections, Dictionary<string, Row> customAction)
962 {
963 var keptRows = 0;
964
965 foreach (var currentTable in sequenceList)
966 {
967 for (var i = 0; i < currentTable.Rows.Count; i++)
968 {
969 var row = currentTable.Rows[i];
970 var actionName = row.Fields[0].Data.ToString();
971 var sections = row.SectionId.Split('/');
972 var isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0);
973
974 if (row.Operation == RowOperation.None)
975 {
976 // ignore the rows without section id.
977 if (isSectionIdEmpty)
978 {
979 currentTable.Rows.RemoveAt(i);
980 i--;
981 }
982 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
983 {
984 keptRows++;
985 }
986 else
987 {
988 currentTable.Rows.RemoveAt(i);
989 i--;
990 }
991 }
992 else if (row.Operation == RowOperation.Modify)
993 {
994 var sequenceChanged = row.Fields[2].Modified;
995 var conditionChanged = row.Fields[1].Modified;
996
997 if (sequenceChanged && !conditionChanged)
998 {
999 keptRows++;
1000 }
1001 else if (!sequenceChanged && conditionChanged)
1002 {
1003 if (isSectionIdEmpty)
1004 {
1005 currentTable.Rows.RemoveAt(i);
1006 i--;
1007 }
1008 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
1009 {
1010 keptRows++;
1011 }
1012 else
1013 {
1014 currentTable.Rows.RemoveAt(i);
1015 i--;
1016 }
1017 }
1018 else if (sequenceChanged && conditionChanged)
1019 {
1020 if (isSectionIdEmpty)
1021 {
1022 row.Fields[1].Modified = false;
1023 keptRows++;
1024 }
1025 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
1026 {
1027 keptRows++;
1028 }
1029 else
1030 {
1031 row.Fields[1].Modified = false;
1032 keptRows++;
1033 }
1034 }
1035 }
1036 else if (row.Operation == RowOperation.Delete)
1037 {
1038 if (isSectionIdEmpty)
1039 {
1040 // it is a stardard action which is added by wix, we should keep this action.
1041 row.Operation = RowOperation.None;
1042 keptRows++;
1043 }
1044 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
1045 {
1046 keptRows++;
1047 }
1048 else
1049 {
1050 if (customAction.ContainsKey(actionName))
1051 {
1052 currentTable.Rows.RemoveAt(i);
1053 i--;
1054 }
1055 else
1056 {
1057 // it is a stardard action, we should keep this action.
1058 row.Operation = RowOperation.None;
1059 keptRows++;
1060 }
1061 }
1062 }
1063 else if (row.Operation == RowOperation.Add)
1064 {
1065 if (isSectionIdEmpty)
1066 {
1067 keptRows++;
1068 }
1069 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
1070 {
1071 keptRows++;
1072 }
1073 else
1074 {
1075 if (customAction.ContainsKey(actionName))
1076 {
1077 currentTable.Rows.RemoveAt(i);
1078 i--;
1079 }
1080 else
1081 {
1082 keptRows++;
1083 }
1084 }
1085 }
1086 }
1087 }
1088
1089 return keptRows;
1090 }
1091
1092 /// <summary>
1093 /// Create the #transform for the given main transform.
1094 /// </summary>
1095 private WindowsInstallerData BuildPairedTransform(Dictionary<SumaryInformationType, SummaryInformationTuple> summaryInfo, Dictionary<string, MsiPatchMetadataTuple> patchMetadata, WixPatchIdTuple patchIdTuple, WindowsInstallerData mainTransform, MediaTuple mediaTuple, WixPatchBaselineTuple baselineTuple, out string productCode)
1096 {
1097 productCode = null;
1098
1099 var pairedTransform = new WindowsInstallerData(null)
1100 {
1101 Type = OutputType.Transform,
1102 Codepage = mainTransform.Codepage
1103 };
1104
1105 // lookup productVersion property to correct summaryInformation
1106 var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1);
1107
1108 var mainSummaryTable = mainTransform.Tables["_SummaryInformation"];
1109 var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0));
1110
1111 var baselineValidationFlags = ((int)baselineTuple.ValidationFlags).ToString(CultureInfo.InvariantCulture);
1112
1113 if (!mainSummaryRows.ContainsKey((int)SumaryInformationType.TransformValidationFlags))
1114 {
1115 var mainSummaryRow = mainSummaryTable.CreateRow(baselineTuple.SourceLineNumbers);
1116 mainSummaryRow[0] = (int)SumaryInformationType.TransformValidationFlags;
1117 mainSummaryRow[1] = baselineValidationFlags;
1118 }
1119
1120 // copy summary information from core transform
1121 var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
1122
1123 foreach (var mainSummaryRow in mainSummaryTable.Rows)
1124 {
1125 var type = (SumaryInformationType)mainSummaryRow.FieldAsInteger(0);
1126 var value = mainSummaryRow.FieldAsString(1);
1127 switch (type)
1128 {
1129 case SumaryInformationType.TransformProductCodes:
1130 var propertyData = value.Split(';');
1131 var oldProductVersion = propertyData[0].Substring(38);
1132 var upgradeCode = propertyData[2];
1133 productCode = propertyData[0].Substring(0, 38);
1134
1135 if (newProductVersion == null)
1136 {
1137 newProductVersion = oldProductVersion;
1138 }
1139
1140 // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade'
1141 mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
1142 value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
1143 break;
1144 case SumaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML.
1145 value = baselineValidationFlags;
1146 mainSummaryRow[1] = value;
1147 break;
1148 }
1149
1150 var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers);
1151 pairedSummaryRow[0] = mainSummaryRow[0];
1152 pairedSummaryRow[1] = value;
1153 }
1154
1155 if (productCode == null)
1156 {
1157 this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo());
1158 return null;
1159 }
1160
1161 // copy File table
1162 if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count)
1163 {
1164#if TODO_PATCHING
1165 // We require file source information.
1166 var mainWixFileTable = mainTransform.Tables["WixFile"];
1167 if (null == mainWixFileTable)
1168 {
1169 this.Messaging.Write(ErrorMessages.AdminImageRequired(productCode));
1170 return null;
1171 }
1172
1173 var mainFileRows = new RowDictionary<FileRow>(mainFileTable);
1174
1175 var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
1176 {
1177 var mainFileRow = mainFileRows[mainWixFileRow.File];
1178
1179 // set File.Sequence to non null to satisfy transform bind
1180 mainFileRow.Sequence = 1;
1181
1182 // delete's don't need rows in the paired transform
1183 if (mainFileRow.Operation == RowOperation.Delete)
1184 {
1185 continue;
1186 }
1187
1188 var pairedFileRow = (FileRow)pairedFileTable.CreateRow(null);
1189 pairedFileRow.Operation = RowOperation.Modify;
1190 for (var i = 0; i < mainFileRow.Fields.Length; i++)
1191 {
1192 pairedFileRow[i] = mainFileRow[i];
1193 }
1194
1195 // override authored media for patch bind
1196 mainWixFileRow.DiskId = mediaTuple.DiskId;
1197
1198 // suppress any change to File.Sequence to avoid bloat
1199 mainFileRow.Fields[7].Modified = false;
1200
1201 // force File row to appear in the transform
1202 switch (mainFileRow.Operation)
1203 {
1204 case RowOperation.Modify:
1205 case RowOperation.Add:
1206 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
1207 pairedFileRow.Fields[6].Modified = true;
1208 pairedFileRow.Operation = mainFileRow.Operation;
1209 break;
1210 default:
1211 pairedFileRow.Fields[6].Modified = false;
1212 break;
1213 }
1214 }
1215#endif
1216 }
1217
1218 // Add Media row to pairedTransform
1219 var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
1220 var pairedMediaRow = pairedMediaTable.CreateRow(mediaTuple.SourceLineNumbers);
1221 pairedMediaRow.Operation = RowOperation.Add;
1222 pairedMediaRow[0] = mediaTuple.DiskId;
1223 pairedMediaRow[1] = mediaTuple.LastSequence ?? 0;
1224 pairedMediaRow[2] = mediaTuple.DiskPrompt;
1225 pairedMediaRow[3] = mediaTuple.Cabinet;
1226 pairedMediaRow[4] = mediaTuple.VolumeLabel;
1227 pairedMediaRow[5] = mediaTuple.Source;
1228
1229 // Add PatchPackage for this Media
1230 var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
1231 pairedPackageTable.Operation = TableOperation.Add;
1232 var pairedPackageRow = pairedPackageTable.CreateRow(mediaTuple.SourceLineNumbers);
1233 pairedPackageRow.Operation = RowOperation.Add;
1234 pairedPackageRow[0] = patchIdTuple.Id.Id;
1235 pairedPackageRow[1] = mediaTuple.DiskId;
1236
1237 // Add the property to the patch transform's Property table.
1238 var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]);
1239 pairedPropertyTable.Operation = TableOperation.Add;
1240
1241 // Add property to both identify client patches and whether those patches are removable or not
1242 patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalTuple);
1243
1244 var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalTuple?.SourceLineNumbers);
1245 pairedPropertyRow.Operation = RowOperation.Add;
1246 pairedPropertyRow[0] = String.Concat(patchIdTuple.ClientPatchId, ".AllowRemoval");
1247 pairedPropertyRow[1] = allowRemovalTuple?.Value ?? "0";
1248
1249 // Add this patch code GUID to the patch transform to identify
1250 // which patches are installed, including in multi-patch
1251 // installations.
1252 pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdTuple.SourceLineNumbers);
1253 pairedPropertyRow.Operation = RowOperation.Add;
1254 pairedPropertyRow[0] = String.Concat(patchIdTuple.ClientPatchId, ".PatchCode");
1255 pairedPropertyRow[1] = patchIdTuple.Id.Id;
1256
1257 // Add PATCHNEWPACKAGECODE to apply to admin layouts.
1258 pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdTuple.SourceLineNumbers);
1259 pairedPropertyRow.Operation = RowOperation.Add;
1260 pairedPropertyRow[0] = "PATCHNEWPACKAGECODE";
1261 pairedPropertyRow[1] = patchIdTuple.Id.Id;
1262
1263 // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts.
1264 if (summaryInfo.TryGetValue(SumaryInformationType.Subject, out var subjectTuple))
1265 {
1266 pairedPropertyRow = pairedPropertyTable.CreateRow(subjectTuple.SourceLineNumbers);
1267 pairedPropertyRow.Operation = RowOperation.Add;
1268 pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT";
1269 pairedPropertyRow[1] = subjectTuple.Value;
1270 }
1271
1272 if (summaryInfo.TryGetValue(SumaryInformationType.Comments, out var commentsTuple))
1273 {
1274 pairedPropertyRow = pairedPropertyTable.CreateRow(commentsTuple.SourceLineNumbers);
1275 pairedPropertyRow.Operation = RowOperation.Add;
1276 pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS";
1277 pairedPropertyRow[1] = commentsTuple.Value;
1278 }
1279
1280 return pairedTransform;
1281 }
1282
1283 private static SortedSet<string> FinalizePatchProductCodes(List<IntermediateTuple> tuples, SortedSet<string> productCodes)
1284 {
1285 var patchTargetTuples = tuples.OfType<WixPatchTargetTuple>().ToList();
1286
1287 if (patchTargetTuples.Count > 0)
1288 {
1289 var targets = new SortedSet<string>();
1290 var replace = true;
1291 foreach (var wixPatchTargetRow in patchTargetTuples)
1292 {
1293 var target = wixPatchTargetRow.ProductCode.ToUpperInvariant();
1294 if (target == "*")
1295 {
1296 replace = false;
1297 }
1298 else
1299 {
1300 targets.Add(target);
1301 }
1302 }
1303
1304 // Replace the target ProductCodes with the authored list.
1305 if (replace)
1306 {
1307 productCodes = targets;
1308 }
1309 else
1310 {
1311 // Copy the authored target ProductCodes into the list.
1312 foreach (var target in targets)
1313 {
1314 productCodes.Add(target);
1315 }
1316 }
1317 }
1318
1319 return productCodes;
1320 }
1321 }
1322}