aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2020-05-30 14:53:05 -0700
committerRob Mensching <rob@firegiant.com>2020-05-30 15:07:21 -0700
commitd529525a1e331f3ef9ec2707791c99bd78fdd82f (patch)
tree1d9fe1f0c0ee9850371c916802eb03ec9dc37a87 /src
parent9c54d2fce80983bbee5f0f113b5aa30f22bc8a23 (diff)
downloadwix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.tar.gz
wix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.tar.bz2
wix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.zip
Basic patching support
Diffstat (limited to 'src')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs66
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs130
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs32
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs2
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateOutputFromIRCommand.cs3
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs2
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/FileSystemManager.cs71
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs9
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs13
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs16
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs576
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs59
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/PatchTransform.cs227
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs16
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs61
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs453
-rw-r--r--src/WixToolset.Core/Bind/FileFacade.cs24
-rw-r--r--src/WixToolset.Core/CommandLine/BuildCommand.cs1
-rw-r--r--src/test/WixToolsetTest.CoreIntegration/PatchFixture.cs4
19 files changed, 779 insertions, 986 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
index 214bf617..a16bafd7 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
@@ -147,7 +147,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
147 { 147 {
148 foreach (MediaRow transformMediaRow in transformMediaTable.Rows) 148 foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
149 { 149 {
150 if (mediaTuple.LastSequence < transformMediaRow.LastSequence) 150 if (!mediaTuple.LastSequence.HasValue || mediaTuple.LastSequence < transformMediaRow.LastSequence)
151 { 151 {
152 // The Binder will pre-increment the sequence. 152 // The Binder will pre-increment the sequence.
153 mediaTuple.LastSequence = transformMediaRow.LastSequence; 153 mediaTuple.LastSequence = transformMediaRow.LastSequence;
@@ -156,14 +156,13 @@ namespace WixToolset.Core.WindowsInstaller.Bind
156 } 156 }
157 157
158 // Use the Media/@DiskId if greater than the last sequence for backward compatibility. 158 // Use the Media/@DiskId if greater than the last sequence for backward compatibility.
159 if (mediaTuple.LastSequence < mediaTuple.DiskId) 159 if (!mediaTuple.LastSequence.HasValue || mediaTuple.LastSequence < mediaTuple.DiskId)
160 { 160 {
161 mediaTuple.LastSequence = mediaTuple.DiskId; 161 mediaTuple.LastSequence = mediaTuple.DiskId;
162 } 162 }
163 163
164 // Ignore media table in the transform. 164 // Ignore media table in the transform.
165 mainTransform.Transform.Tables.Remove("Media"); 165 mainTransform.Transform.Tables.Remove("Media");
166 mainTransform.Transform.Tables.Remove("WixMedia");
167 mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); 166 mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
168 167
169 var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchIdTuple, mainTransform.Transform, mediaTuple, baselineTuple, out var productCode); 168 var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchIdTuple, mainTransform.Transform, mediaTuple, baselineTuple, out var productCode);
@@ -767,15 +766,15 @@ namespace WixToolset.Core.WindowsInstaller.Bind
767 if (transform.TryGetTable("File", out var fileTable)) 766 if (transform.TryGetTable("File", out var fileTable))
768 { 767 {
769 var componentWithChangedKeyPath = new Dictionary<string, string>(); 768 var componentWithChangedKeyPath = new Dictionary<string, string>();
770 foreach (var row in fileTable.Rows) 769 foreach (FileRow row in fileTable.Rows)
771 { 770 {
772 if (RowOperation.None == row.Operation) 771 if (RowOperation.None == row.Operation)
773 { 772 {
774 continue; 773 continue;
775 } 774 }
776 775
777 var fileId = row.FieldAsString(0); 776 var fileId = row.File;
778 var componentId = row.FieldAsString(1); 777 var componentId = row.Component;
779 778
780 // If this file is the keypath of a component 779 // If this file is the keypath of a component
781 if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal)) 780 if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal))
@@ -972,7 +971,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
972 971
973 if (row.Operation == RowOperation.None) 972 if (row.Operation == RowOperation.None)
974 { 973 {
975 // ignore the rows without section id. 974 // Ignore the rows without section id.
976 if (isSectionIdEmpty) 975 if (isSectionIdEmpty)
977 { 976 {
978 currentTable.Rows.RemoveAt(i); 977 currentTable.Rows.RemoveAt(i);
@@ -1157,47 +1156,33 @@ namespace WixToolset.Core.WindowsInstaller.Bind
1157 return null; 1156 return null;
1158 } 1157 }
1159 1158
1160 // copy File table 1159 // Copy File table
1161 if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count) 1160 if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count)
1162 { 1161 {
1163#if TODO_PATCHING
1164 // We require file source information.
1165 var mainWixFileTable = mainTransform.Tables["WixFile"];
1166 if (null == mainWixFileTable)
1167 {
1168 this.Messaging.Write(ErrorMessages.AdminImageRequired(productCode));
1169 return null;
1170 }
1171
1172 var mainFileRows = new RowDictionary<FileRow>(mainFileTable);
1173
1174 var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); 1162 var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
1175 {
1176 var mainFileRow = mainFileRows[mainWixFileRow.File];
1177 1163
1178 // set File.Sequence to non null to satisfy transform bind 1164 foreach (FileRow mainFileRow in mainFileTable.Rows)
1165 {
1166 // Set File.Sequence to non null to satisfy transform bind.
1179 mainFileRow.Sequence = 1; 1167 mainFileRow.Sequence = 1;
1180 1168
1181 // delete's don't need rows in the paired transform 1169 // Delete's don't need rows in the paired transform.
1182 if (mainFileRow.Operation == RowOperation.Delete) 1170 if (mainFileRow.Operation == RowOperation.Delete)
1183 { 1171 {
1184 continue; 1172 continue;
1185 } 1173 }
1186 1174
1187 var pairedFileRow = (FileRow)pairedFileTable.CreateRow(null); 1175 var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers);
1188 pairedFileRow.Operation = RowOperation.Modify; 1176 pairedFileRow.Operation = RowOperation.Modify;
1189 for (var i = 0; i < mainFileRow.Fields.Length; i++) 1177 mainFileRow.CopyTo(pairedFileRow);
1190 {
1191 pairedFileRow[i] = mainFileRow[i];
1192 }
1193 1178
1194 // override authored media for patch bind 1179 // Override authored media for patch bind.
1195 mainWixFileRow.DiskId = mediaTuple.DiskId; 1180 mainFileRow.DiskId = mediaTuple.DiskId;
1196 1181
1197 // suppress any change to File.Sequence to avoid bloat 1182 // Suppress any change to File.Sequence to avoid bloat.
1198 mainFileRow.Fields[7].Modified = false; 1183 mainFileRow.Fields[7].Modified = false;
1199 1184
1200 // force File row to appear in the transform 1185 // Force File row to appear in the transform.
1201 switch (mainFileRow.Operation) 1186 switch (mainFileRow.Operation)
1202 { 1187 {
1203 case RowOperation.Modify: 1188 case RowOperation.Modify:
@@ -1211,19 +1196,18 @@ namespace WixToolset.Core.WindowsInstaller.Bind
1211 break; 1196 break;
1212 } 1197 }
1213 } 1198 }
1214#endif
1215 } 1199 }
1216 1200
1217 // Add Media row to pairedTransform 1201 // Add Media row to pairedTransform
1218 var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); 1202 var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
1219 var pairedMediaRow = pairedMediaTable.CreateRow(mediaTuple.SourceLineNumbers); 1203 var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaTuple.SourceLineNumbers);
1220 pairedMediaRow.Operation = RowOperation.Add; 1204 pairedMediaRow.Operation = RowOperation.Add;
1221 pairedMediaRow[0] = mediaTuple.DiskId; 1205 pairedMediaRow.DiskId = mediaTuple.DiskId;
1222 pairedMediaRow[1] = mediaTuple.LastSequence ?? 0; 1206 pairedMediaRow.LastSequence = mediaTuple.LastSequence ?? 0;
1223 pairedMediaRow[2] = mediaTuple.DiskPrompt; 1207 pairedMediaRow.DiskPrompt = mediaTuple.DiskPrompt;
1224 pairedMediaRow[3] = mediaTuple.Cabinet; 1208 pairedMediaRow.Cabinet = mediaTuple.Cabinet;
1225 pairedMediaRow[4] = mediaTuple.VolumeLabel; 1209 pairedMediaRow.VolumeLabel = mediaTuple.VolumeLabel;
1226 pairedMediaRow[5] = mediaTuple.Source; 1210 pairedMediaRow.Source = mediaTuple.Source;
1227 1211
1228 // Add PatchPackage for this Media 1212 // Add PatchPackage for this Media
1229 var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); 1213 var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
@@ -1283,7 +1267,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
1283 { 1267 {
1284 var patchTargetTuples = tuples.OfType<WixPatchTargetTuple>().ToList(); 1268 var patchTargetTuples = tuples.OfType<WixPatchTargetTuple>().ToList();
1285 1269
1286 if (patchTargetTuples.Count > 0) 1270 if (patchTargetTuples.Any())
1287 { 1271 {
1288 var targets = new SortedSet<string>(); 1272 var targets = new SortedSet<string>();
1289 var replace = true; 1273 var replace = true;
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
index 8e901d30..d9d246f5 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
@@ -43,7 +43,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
43 this.DefaultCompressionLevel = context.DefaultCompressionLevel; 43 this.DefaultCompressionLevel = context.DefaultCompressionLevel;
44 this.DelayedFields = context.DelayedFields; 44 this.DelayedFields = context.DelayedFields;
45 this.ExpectedEmbeddedFiles = context.ExpectedEmbeddedFiles; 45 this.ExpectedEmbeddedFiles = context.ExpectedEmbeddedFiles;
46 this.FileSystemExtensions = context.FileSystemExtensions; 46 this.FileSystemManager = new FileSystemManager(context.FileSystemExtensions);
47 this.Intermediate = context.IntermediateRepresentation; 47 this.Intermediate = context.IntermediateRepresentation;
48 this.IntermediateFolder = context.IntermediateFolder; 48 this.IntermediateFolder = context.IntermediateFolder;
49 this.OutputPath = context.OutputPath; 49 this.OutputPath = context.OutputPath;
@@ -79,7 +79,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
79 79
80 public IEnumerable<IExpectedExtractFile> ExpectedEmbeddedFiles { get; } 80 public IEnumerable<IExpectedExtractFile> ExpectedEmbeddedFiles { get; }
81 81
82 public IEnumerable<IFileSystemExtension> FileSystemExtensions { get; } 82 public FileSystemManager FileSystemManager { get; }
83 83
84 public bool DeltaBinaryPatch { get; set; } 84 public bool DeltaBinaryPatch { get; set; }
85 85
@@ -116,7 +116,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
116 var trackedFiles = new List<ITrackedFile>(); 116 var trackedFiles = new List<ITrackedFile>();
117 117
118 var containsMergeModules = false; 118 var containsMergeModules = false;
119 var suppressedTableNames = new HashSet<string>();
120 119
121 // If there are any fields to resolve later, create the cache to populate during bind. 120 // If there are any fields to resolve later, create the cache to populate during bind.
122 var variableCache = this.DelayedFields.Any() ? new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) : null; 121 var variableCache = this.DelayedFields.Any() ? new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) : null;
@@ -226,7 +225,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
226 return null; 225 return null;
227 } 226 }
228 227
229 WindowsInstallerData output;
230 this.Intermediate.UpdateLevel(Data.WindowsInstaller.IntermediateLevels.FullyBound); 228 this.Intermediate.UpdateLevel(Data.WindowsInstaller.IntermediateLevels.FullyBound);
231 this.Messaging.Write(VerboseMessages.UpdatingFileInformation()); 229 this.Messaging.Write(VerboseMessages.UpdatingFileInformation());
232 230
@@ -268,14 +266,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
268 } 266 }
269 else if (SectionType.Patch == section.Type) 267 else if (SectionType.Patch == section.Type)
270 { 268 {
271 // Merge transform data into the output object. 269 var command = new GetFileFacadesFromTransforms(this.Messaging, this.FileSystemManager, this.SubStorages);
272 //IEnumerable<FileFacade> filesFromTransform = this.CopyFromTransformData(this.Output);
273
274 //var command = new CopyTransformDataCommand(this.Messaging, /*output*/this.SubStorages, tableDefinitions, copyOutFileRows: true);
275 //command.Output = output;
276 //command.TableDefinitions = this.TableDefinitions;
277 //command.CopyOutFileRows = true;
278 var command = new GetFileFacadesFromTransforms(this.Messaging, this.SubStorages, tableDefinitions);
279 command.Execute(); 270 command.Execute();
280 var filesFromTransforms = command.FileFacades; 271 var filesFromTransforms = command.FileFacades;
281 272
@@ -338,6 +329,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
338 command.Execute(); 329 command.Execute();
339 } 330 }
340 331
332 // Update file sequence.
333 {
334 var command = new UpdateMediaSequencesCommand(section, fileFacades);
335 command.Execute();
336 }
337
341 // stop processing if an error previously occurred 338 // stop processing if an error previously occurred
342 if (this.Messaging.EncounteredError) 339 if (this.Messaging.EncounteredError)
343 { 340 {
@@ -345,6 +342,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
345 } 342 }
346 343
347 // Time to create the output object. Try to put as much above here as possible, updating the IR is better. 344 // Time to create the output object. Try to put as much above here as possible, updating the IR is better.
345 WindowsInstallerData output;
348 { 346 {
349 var command = new CreateOutputFromIRCommand(this.Messaging, section, tableDefinitions, this.BackendExtensions, this.WindowsInstallerBackendHelper); 347 var command = new CreateOutputFromIRCommand(this.Messaging, section, tableDefinitions, this.BackendExtensions, this.WindowsInstallerBackendHelper);
350 command.Execute(); 348 command.Execute();
@@ -352,14 +350,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
352 output = command.Output; 350 output = command.Output;
353 } 351 }
354 352
355 // Update file sequence.
356 {
357 var command = new UpdateMediaSequencesCommand(output, fileFacades);
358 command.Execute();
359 }
360
361 // Modularize identifiers. 353 // Modularize identifiers.
362 if (OutputType.Module == output.Type) 354 if (output.Type == OutputType.Module)
363 { 355 {
364 var command = new ModularizeCommand(output, modularizationSuffix, section.Tuples.OfType<WixSuppressModularizationTuple>()); 356 var command = new ModularizeCommand(output, modularizationSuffix, section.Tuples.OfType<WixSuppressModularizationTuple>());
365 command.Execute(); 357 command.Execute();
@@ -457,18 +449,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
457 trackedFiles.AddRange(command.TrackedFiles); 449 trackedFiles.AddRange(command.TrackedFiles);
458 } 450 }
459 451
460#if DELETE 452 if (output.Type == OutputType.Patch)
461 if (OutputType.Patch == output.Type) 453 {
462 { 454 // Copy output data back into the transforms.
463 // Copy output data back into the transforms. 455 var command = new UpdateTransformsWithFileFacades(this.Messaging, output, this.SubStorages, tableDefinitions, fileFacades);
464#if TODO_PATCHING 456 command.Execute();
465 var command = new CopyTransformDataCommand(this.Messaging, output, tableDefinitions, copyOutFileRows: false); 457 }
466 command.Execute();
467
468 this.CopyToTransformData(this.Output);
469#endif
470 }
471#endif
472 458
473 // stop processing if an error previously occurred 459 // stop processing if an error previously occurred
474 if (this.Messaging.EncounteredError) 460 if (this.Messaging.EncounteredError)
@@ -483,8 +469,10 @@ namespace WixToolset.Core.WindowsInstaller.Bind
483 var trackMsi = this.BackendHelper.TrackFile(this.OutputPath, TrackedFileType.Final); 469 var trackMsi = this.BackendHelper.TrackFile(this.OutputPath, TrackedFileType.Final);
484 trackedFiles.Add(trackMsi); 470 trackedFiles.Add(trackMsi);
485 471
486 var temporaryFiles = this.GenerateDatabase(output, tableDefinitions, trackMsi.Path, false, false); 472 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, output, trackMsi.Path, tableDefinitions, this.IntermediateFolder, this.Codepage, keepAddedColumns: false, this.SuppressAddingValidationRows, useSubdirectory: false);
487 trackedFiles.AddRange(temporaryFiles); 473 command.Execute();
474
475 trackedFiles.AddRange(command.GeneratedTemporaryFiles);
488 } 476 }
489 477
490 // Stop processing if an error previously occurred. 478 // Stop processing if an error previously occurred.
@@ -498,31 +486,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
498 { 486 {
499 this.Messaging.Write(VerboseMessages.MergingModules()); 487 this.Messaging.Write(VerboseMessages.MergingModules());
500 488
501 // Add back possibly suppressed sequence tables since all sequence tables must be present
502 // for the merge process to work. We'll drop the suppressed sequence tables again as
503 // necessary.
504 foreach (SequenceTable sequence in Enum.GetValues(typeof(SequenceTable)))
505 {
506 var sequenceTableName = (sequence == SequenceTable.AdvertiseExecuteSequence) ? "AdvtExecuteSequence" : sequence.ToString();
507 var sequenceTable = output.Tables[sequenceTableName];
508
509 if (null == sequenceTable)
510 {
511 sequenceTable = output.EnsureTable(tableDefinitions[sequenceTableName]);
512 }
513
514 if (0 == sequenceTable.Rows.Count)
515 {
516 suppressedTableNames.Add(sequenceTableName);
517 }
518 }
519
520 var command = new MergeModulesCommand(this.Messaging); 489 var command = new MergeModulesCommand(this.Messaging);
521 command.FileFacades = fileFacades; 490 command.FileFacades = fileFacades;
522 command.IntermediateFolder = this.IntermediateFolder; 491 command.IntermediateFolder = this.IntermediateFolder;
523 command.Output = output; 492 command.Output = output;
524 command.OutputPath = this.OutputPath; 493 command.OutputPath = this.OutputPath;
525 command.SuppressedTableNames = suppressedTableNames; 494 command.TableDefinitions = tableDefinitions;
526 command.Execute(); 495 command.Execute();
527 } 496 }
528 497
@@ -607,38 +576,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
607 return wixout; 576 return wixout;
608 } 577 }
609 578
610#if TODO_PATCHING
611 /// <summary>
612 /// Copy file data between transform substorages and the patch output object
613 /// </summary>
614 /// <param name="output">The output to bind.</param>
615 /// <param name="allFileRows">True if copying from transform to patch, false the other way.</param>
616 private IEnumerable<FileFacade> CopyFromTransformData(Output output)
617 {
618 var command = new CopyTransformDataCommand();
619 command.CopyOutFileRows = true;
620 command.Output = output;
621 command.TableDefinitions = this.TableDefinitions;
622 command.Execute();
623
624 return command.FileFacades;
625 }
626
627 /// <summary>
628 /// Copy file data between transform substorages and the patch output object
629 /// </summary>
630 /// <param name="output">The output to bind.</param>
631 /// <param name="allFileRows">True if copying from transform to patch, false the other way.</param>
632 private void CopyToTransformData(Output output)
633 {
634 var command = new CopyTransformDataCommand();
635 command.CopyOutFileRows = false;
636 command.Output = output;
637 command.TableDefinitions = this.TableDefinitions;
638 command.Execute();
639 }
640#endif
641
642 /// <summary> 579 /// <summary>
643 /// Validate that there are no duplicate GUIDs in the output. 580 /// Validate that there are no duplicate GUIDs in the output.
644 /// </summary> 581 /// </summary>
@@ -650,7 +587,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
650 { 587 {
651 if (output.TryGetTable("Component", out var componentTable)) 588 if (output.TryGetTable("Component", out var componentTable))
652 { 589 {
653 Dictionary<string, bool> componentGuidConditions = new Dictionary<string, bool>(componentTable.Rows.Count); 590 var componentGuidConditions = new Dictionary<string, bool>(componentTable.Rows.Count);
654 591
655 foreach (Data.WindowsInstaller.Rows.ComponentRow row in componentTable.Rows) 592 foreach (Data.WindowsInstaller.Rows.ComponentRow row in componentTable.Rows)
656 { 593 {
@@ -658,12 +595,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
658 // there's already an error that prevented it from being replaced with a real GUID. 595 // there's already an error that prevented it from being replaced with a real GUID.
659 if (!String.IsNullOrEmpty(row.Guid) && "*" != row.Guid) 596 if (!String.IsNullOrEmpty(row.Guid) && "*" != row.Guid)
660 { 597 {
661 bool thisComponentHasCondition = !String.IsNullOrEmpty(row.Condition); 598 var thisComponentHasCondition = !String.IsNullOrEmpty(row.Condition);
662 bool allComponentsHaveConditions = thisComponentHasCondition; 599 var allComponentsHaveConditions = thisComponentHasCondition;
663 600
664 if (componentGuidConditions.ContainsKey(row.Guid)) 601 if (componentGuidConditions.ContainsKey(row.Guid))
665 { 602 {
666 allComponentsHaveConditions = componentGuidConditions[row.Guid] && thisComponentHasCondition; 603 allComponentsHaveConditions = thisComponentHasCondition && componentGuidConditions[row.Guid];
667 604
668 if (allComponentsHaveConditions) 605 if (allComponentsHaveConditions)
669 { 606 {
@@ -728,20 +665,5 @@ namespace WixToolset.Core.WindowsInstaller.Bind
728 665
729 return layout; 666 return layout;
730 } 667 }
731
732 /// <summary>
733 /// Creates the MSI/MSM/PCP database.
734 /// </summary>
735 /// <param name="output">Output to create database for.</param>
736 /// <param name="databaseFile">The database file to create.</param>
737 /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param>
738 /// <param name="useSubdirectory">Whether to use a subdirectory based on the <paramref name="databaseFile"/> file name for intermediate files.</param>
739 private IEnumerable<ITrackedFile> GenerateDatabase(WindowsInstallerData output, TableDefinitionCollection tableDefinitions, string databaseFile, bool keepAddedColumns, bool useSubdirectory)
740 {
741 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystemExtensions, output, databaseFile, tableDefinitions, this.IntermediateFolder, this.Codepage, keepAddedColumns, this.SuppressAddingValidationRows, useSubdirectory);
742 command.Execute();
743
744 return command.GeneratedTemporaryFiles;
745 }
746 } 668 }
747} 669}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
index ffe26249..ac98c82d 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
@@ -3,23 +3,21 @@
3namespace WixToolset.Core.WindowsInstaller.Bind 3namespace WixToolset.Core.WindowsInstaller.Bind
4{ 4{
5 using System; 5 using System;
6 using System.Collections.Generic;
7 using System.Globalization; 6 using System.Globalization;
8 using System.IO; 7 using System.IO;
9 using WixToolset.Core.WindowsInstaller.Msi; 8 using WixToolset.Core.WindowsInstaller.Msi;
10 using WixToolset.Data; 9 using WixToolset.Data;
11 using WixToolset.Data.Tuples; 10 using WixToolset.Data.Tuples;
12 using WixToolset.Data.WindowsInstaller; 11 using WixToolset.Data.WindowsInstaller;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Services; 12 using WixToolset.Extensibility.Services;
15 13
16 internal class BindTransformCommand 14 internal class BindTransformCommand
17 { 15 {
18 public BindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, IEnumerable<IFileSystemExtension> extensions, string intermediateFolder, WindowsInstallerData transform, string outputPath, TableDefinitionCollection tableDefinitions) 16 public BindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, FileSystemManager fileSystemManager, string intermediateFolder, WindowsInstallerData transform, string outputPath, TableDefinitionCollection tableDefinitions)
19 { 17 {
20 this.Messaging = messaging; 18 this.Messaging = messaging;
21 this.BackendHelper = backendHelper; 19 this.BackendHelper = backendHelper;
22 this.Extensions = extensions; 20 this.FileSystemManager = fileSystemManager;
23 this.IntermediateFolder = intermediateFolder; 21 this.IntermediateFolder = intermediateFolder;
24 this.Transform = transform; 22 this.Transform = transform;
25 this.OutputPath = outputPath; 23 this.OutputPath = outputPath;
@@ -30,7 +28,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
30 28
31 private IBackendHelper BackendHelper { get; } 29 private IBackendHelper BackendHelper { get; }
32 30
33 private IEnumerable<IFileSystemExtension> Extensions { get; } 31 private FileSystemManager FileSystemManager { get; }
34 32
35 private TableDefinitionCollection TableDefinitions { get; } 33 private TableDefinitionCollection TableDefinitions { get; }
36 34
@@ -353,7 +351,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
353 targetRow[i] = emptyFile; 351 targetRow[i] = emptyFile;
354 modifiedRow = true; 352 modifiedRow = true;
355 } 353 }
356 else if (!this.CompareFiles(objectField.PreviousData, (string)objectField.Data)) 354 else if (!this.FileSystemManager.CompareFiles(objectField.PreviousData, (string)objectField.Data))
357 { 355 {
358 targetRow[i] = objectField.PreviousData; 356 targetRow[i] = objectField.PreviousData;
359 modifiedRow = true; 357 modifiedRow = true;
@@ -438,29 +436,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
438 } 436 }
439 } 437 }
440 438
441 private bool CompareFiles(string targetFile, string updatedFile)
442 {
443 bool? compared = null;
444 foreach (var extension in this.Extensions)
445 {
446 compared = extension.CompareFiles(targetFile, updatedFile);
447 if (compared.HasValue)
448 {
449 break;
450 }
451 }
452
453 if (!compared.HasValue)
454 {
455 throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result.
456 }
457
458 return compared.Value;
459 }
460
461 private void GenerateDatabase(WindowsInstallerData output, string outputPath, bool keepAddedColumns) 439 private void GenerateDatabase(WindowsInstallerData output, string outputPath, bool keepAddedColumns)
462 { 440 {
463 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.Extensions, output, outputPath, this.TableDefinitions, this.IntermediateFolder, codepage: -1, keepAddedColumns, suppressAddingValidationRows: true, useSubdirectory: true ); 441 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, output, outputPath, this.TableDefinitions, this.IntermediateFolder, codepage: -1, keepAddedColumns, suppressAddingValidationRows: true, useSubdirectory: true);
464 command.Execute(); 442 command.Execute();
465 } 443 }
466 } 444 }
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs
index f5ac00e6..c54e9c53 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs
@@ -33,7 +33,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
33 var optimizePatchSizeForLargeFiles = this.WixPatchId?.OptimizePatchSizeForLargeFiles ?? false; 33 var optimizePatchSizeForLargeFiles = this.WixPatchId?.OptimizePatchSizeForLargeFiles ?? false;
34 var apiPatchingSymbolFlags = (PatchSymbolFlagsType)(this.WixPatchId?.ApiPatchingSymbolFlags ?? 0); 34 var apiPatchingSymbolFlags = (PatchSymbolFlagsType)(this.WixPatchId?.ApiPatchingSymbolFlags ?? 0);
35 35
36#if TODO_PATCHING 36#if TODO_PATCHING_DELTA
37 foreach (FileFacade facade in this.FileFacades) 37 foreach (FileFacade facade in this.FileFacades)
38 { 38 {
39 if (RowOperation.Modify == facade.File.Operation && 39 if (RowOperation.Modify == facade.File.Operation &&
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateOutputFromIRCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateOutputFromIRCommand.cs
index 0cbb81d8..ffc4e84d 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/CreateOutputFromIRCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateOutputFromIRCommand.cs
@@ -499,7 +499,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
499 row.FileSize = tuple.FileSize; 499 row.FileSize = tuple.FileSize;
500 row.Version = tuple.Version; 500 row.Version = tuple.Version;
501 row.Language = tuple.Language; 501 row.Language = tuple.Language;
502 row.DiskId = tuple.DiskId ?? 1; // TODO: is 0 the correct thing to default here 502 row.DiskId = tuple.DiskId ?? 1; // TODO: is 1 the correct thing to default here
503 row.Sequence = tuple.Sequence;
503 row.Source = tuple.Source.Path; 504 row.Source = tuple.Source.Path;
504 505
505 var attributes = (tuple.Attributes & FileTupleAttributes.Checksum) == FileTupleAttributes.Checksum ? WindowsInstallerConstants.MsidbFileAttributesChecksum : 0; 506 var attributes = (tuple.Attributes & FileTupleAttributes.Checksum) == FileTupleAttributes.Checksum ? WindowsInstallerConstants.MsidbFileAttributesChecksum : 0;
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
index 854d973e..f65f885b 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
@@ -45,7 +45,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
45 var baselineData = this.GetData(tuple.BaselineFile.Path); 45 var baselineData = this.GetData(tuple.BaselineFile.Path);
46 var updateData = this.GetData(tuple.UpdateFile.Path); 46 var updateData = this.GetData(tuple.UpdateFile.Path);
47 47
48 var command = new GenerateTransformCommand(this.Messaging, baselineData, updateData, false); 48 var command = new GenerateTransformCommand(this.Messaging, baselineData, updateData, preserveUnchangedRows: true, showPedanticMessages: false);
49 transform = command.Execute(); 49 transform = command.Execute();
50 } 50 }
51 else 51 else
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/FileSystemManager.cs b/src/WixToolset.Core.WindowsInstaller/Bind/FileSystemManager.cs
new file mode 100644
index 00000000..75477271
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/FileSystemManager.cs
@@ -0,0 +1,71 @@
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.Collections.Generic;
6 using System.IO;
7 using WixToolset.Extensibility;
8
9 internal class FileSystemManager
10 {
11 public FileSystemManager(IEnumerable<IFileSystemExtension> fileSystemExtensions)
12 {
13 this.Extensions = fileSystemExtensions;
14 }
15
16 private IEnumerable<IFileSystemExtension> Extensions { get; }
17
18 public bool CompareFiles(string firstPath, string secondPath)
19 {
20 foreach (var extension in this.Extensions)
21 {
22 var compared = extension.CompareFiles(firstPath, secondPath);
23 if (compared.HasValue)
24 {
25 return compared.Value;
26 }
27 }
28
29 return BuiltinCompareFiles(firstPath, secondPath);
30 }
31
32 private static bool BuiltinCompareFiles(string firstPath, string secondPath)
33 {
34 using (var firstStream = File.OpenRead(firstPath))
35 using (var secondStream = File.OpenRead(secondPath))
36 {
37 if (firstStream.Length != secondStream.Length)
38 {
39 return false;
40 }
41
42 // Using a larger buffer than the default buffer of 4 * 1024 used by FileStream.ReadByte improves performance.
43 // The buffer size is based on user feedback. Based on performance results, a better buffer size may be determined.
44 var firstBuffer = new byte[16 * 1024];
45 var secondBuffer = new byte[16 * 1024];
46
47 var firstReadLength = 0;
48 do
49 {
50 firstReadLength = firstStream.Read(firstBuffer, 0, firstBuffer.Length);
51 var secondReadLength = secondStream.Read(secondBuffer, 0, secondBuffer.Length);
52
53 if (firstReadLength != secondReadLength)
54 {
55 return false;
56 }
57
58 for (var i = 0; i < firstReadLength; ++i)
59 {
60 if (firstBuffer[i] != secondBuffer[i])
61 {
62 return false;
63 }
64 }
65 } while (0 < firstReadLength);
66 }
67
68 return true;
69 }
70 }
71}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs
index ed3b6f01..eff94e80 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs
@@ -10,17 +10,16 @@ namespace WixToolset.Core.WindowsInstaller.Bind
10 using WixToolset.Core.WindowsInstaller.Msi; 10 using WixToolset.Core.WindowsInstaller.Msi;
11 using WixToolset.Data; 11 using WixToolset.Data;
12 using WixToolset.Data.WindowsInstaller; 12 using WixToolset.Data.WindowsInstaller;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Data; 13 using WixToolset.Extensibility.Data;
15 using WixToolset.Extensibility.Services; 14 using WixToolset.Extensibility.Services;
16 15
17 internal class GenerateDatabaseCommand 16 internal class GenerateDatabaseCommand
18 { 17 {
19 public GenerateDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IEnumerable<IFileSystemExtension> fileSystemExtensions, WindowsInstallerData data, string outputPath, TableDefinitionCollection tableDefinitions, string intermediateFolder, int codepage, bool keepAddedColumns, bool suppressAddingValidationRows, bool useSubdirectory) 18 public GenerateDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, FileSystemManager fileSystemManager, WindowsInstallerData data, string outputPath, TableDefinitionCollection tableDefinitions, string intermediateFolder, int codepage, bool keepAddedColumns, bool suppressAddingValidationRows, bool useSubdirectory)
20 { 19 {
21 this.Messaging = messaging; 20 this.Messaging = messaging;
22 this.BackendHelper = backendHelper; 21 this.BackendHelper = backendHelper;
23 this.Extensions = fileSystemExtensions; 22 this.FileSystemManager = fileSystemManager;
24 this.Data = data; 23 this.Data = data;
25 this.OutputPath = outputPath; 24 this.OutputPath = outputPath;
26 this.TableDefinitions = tableDefinitions; 25 this.TableDefinitions = tableDefinitions;
@@ -35,7 +34,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
35 34
36 private IBackendHelper BackendHelper { get; } 35 private IBackendHelper BackendHelper { get; }
37 36
38 private IEnumerable<IFileSystemExtension> Extensions { get; } 37 private FileSystemManager FileSystemManager { get; }
39 38
40 /// <summary> 39 /// <summary>
41 /// Whether to keep columns added in a transform. 40 /// Whether to keep columns added in a transform.
@@ -371,7 +370,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
371 var transformFile = Path.Combine(this.IntermediateFolder, String.Concat(subStorage.Name, ".mst")); 370 var transformFile = Path.Combine(this.IntermediateFolder, String.Concat(subStorage.Name, ".mst"));
372 371
373 // Bind the transform. 372 // Bind the transform.
374 var command = new BindTransformCommand(this.Messaging, this.BackendHelper, this.Extensions, this.IntermediateFolder, subStorage.Data, transformFile, this.TableDefinitions); 373 var command = new BindTransformCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, this.IntermediateFolder, subStorage.Data, transformFile, this.TableDefinitions);
375 command.Execute(); 374 command.Execute();
376 375
377 if (this.Messaging.EncounteredError) 376 if (this.Messaging.EncounteredError)
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
index 8a7dd702..201a890c 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
@@ -10,7 +10,6 @@ namespace WixToolset.Core.WindowsInstaller
10 using WixToolset.Data.Tuples; 10 using WixToolset.Data.Tuples;
11 using WixToolset.Data.WindowsInstaller; 11 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Data.WindowsInstaller.Rows; 12 using WixToolset.Data.WindowsInstaller.Rows;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Services; 13 using WixToolset.Extensibility.Services;
15 14
16 /// <summary> 15 /// <summary>
@@ -25,11 +24,12 @@ namespace WixToolset.Core.WindowsInstaller
25 /// <summary> 24 /// <summary>
26 /// Instantiates a new Differ class. 25 /// Instantiates a new Differ class.
27 /// </summary> 26 /// </summary>
28 public GenerateTransformCommand(IMessaging messaging, WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, bool showPedanticMessages) 27 public GenerateTransformCommand(IMessaging messaging, WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, bool preserveUnchangedRows, bool showPedanticMessages)
29 { 28 {
30 this.messaging = messaging; 29 this.messaging = messaging;
31 this.TargetOutput = targetOutput; 30 this.TargetOutput = targetOutput;
32 this.UpdatedOutput = updatedOutput; 31 this.UpdatedOutput = updatedOutput;
32 this.PreserveUnchangedRows = preserveUnchangedRows;
33 this.ShowPedanticMessages = showPedanticMessages; 33 this.ShowPedanticMessages = showPedanticMessages;
34 } 34 }
35 35
@@ -111,10 +111,10 @@ namespace WixToolset.Core.WindowsInstaller
111 } 111 }
112 else if (TableOperation.None == operation) 112 else if (TableOperation.None == operation)
113 { 113 {
114 var modified = transform.EnsureTable(updatedTable.Definition); 114 var modifiedTable = transform.EnsureTable(updatedTable.Definition);
115 foreach (var row in rows) 115 foreach (var row in rows)
116 { 116 {
117 modified.Rows.Add(row); 117 modifiedTable.Rows.Add(row);
118 } 118 }
119 } 119 }
120 } 120 }
@@ -242,10 +242,7 @@ namespace WixToolset.Core.WindowsInstaller
242 { 242 {
243 var columnDefinition = updatedRow.Fields[i].Column; 243 var columnDefinition = updatedRow.Fields[i].Column;
244 244
245 if (columnDefinition.Unreal) 245 if (!columnDefinition.PrimaryKey)
246 {
247 }
248 else if (!columnDefinition.PrimaryKey)
249 { 246 {
250 var modified = false; 247 var modified = false;
251 248
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs
index 2844f797..55171da4 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs
@@ -26,28 +26,26 @@ namespace WixToolset.Core.WindowsInstaller.Bind
26 var facades = new List<FileFacade>(); 26 var facades = new List<FileFacade>();
27 27
28 var assemblyFile = this.Section.Tuples.OfType<AssemblyTuple>().ToDictionary(t => t.Id.Id); 28 var assemblyFile = this.Section.Tuples.OfType<AssemblyTuple>().ToDictionary(t => t.Id.Id);
29 //var wixFiles = this.Section.Tuples.OfType<WixFileTuple>().ToDictionary(t => t.Id.Id);
30 //var deltaPatchFiles = this.Section.Tuples.OfType<WixDeltaPatchFileTuple>().ToDictionary(t => t.Id.Id); 29 //var deltaPatchFiles = this.Section.Tuples.OfType<WixDeltaPatchFileTuple>().ToDictionary(t => t.Id.Id);
31 30
32 foreach (var file in this.Section.Tuples.OfType<FileTuple>()) 31 foreach (var file in this.Section.Tuples.OfType<FileTuple>())
33 { 32 {
34 //var wixFile = wixFiles[file.Id.Id]; 33 assemblyFile.TryGetValue(file.Id.Id, out var assembly);
35 34
36 //deltaPatchFiles.TryGetValue(file.Id.Id, out var deltaPatchFile); 35 //deltaPatchFiles.TryGetValue(file.Id.Id, out var deltaPatchFile);
37 36
38 //facades.Add(new FileFacade(file, wixFile, deltaPatchFile));
39
40 assemblyFile.TryGetValue(file.Id.Id, out var assembly);
41
42 facades.Add(new FileFacade(file, assembly)); 37 facades.Add(new FileFacade(file, assembly));
38 //facades.Add(new FileFacade(file, wixFile, deltaPatchFile));
43 } 39 }
44 40
45 //this.ResolveDeltaPatchSymbolPaths(deltaPatchFiles, facades); 41#if TODO_PATCHING_DELTA
42 this.ResolveDeltaPatchSymbolPaths(deltaPatchFiles, facades);
43#endif
46 44
47 this.FileFacades = facades; 45 this.FileFacades = facades;
48 } 46 }
49 47
50#if FIX_THIS 48#if TODO_PATCHING_DELTA
51 /// <summary> 49 /// <summary>
52 /// Merge data from the WixPatchSymbolPaths rows into the WixDeltaPatchFile rows. 50 /// Merge data from the WixPatchSymbolPaths rows into the WixDeltaPatchFile rows.
53 /// </summary> 51 /// </summary>
@@ -132,7 +130,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
132 file.SymbolPaths = String.Concat(file.SymbolPaths, ";", row.SymbolPaths); 130 file.SymbolPaths = String.Concat(file.SymbolPaths, ";", row.SymbolPaths);
133 } 131 }
134 132
135#if TODO_PATCHING
136 Field field = row.Fields[2]; 133 Field field = row.Fields[2];
137 if (null != field.PreviousData) 134 if (null != field.PreviousData)
138 { 135 {
@@ -145,7 +142,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
145 file.PreviousSymbols = String.Concat(file.PreviousSymbols, ";", field.PreviousData); 142 file.PreviousSymbols = String.Concat(file.PreviousSymbols, ";", field.PreviousData);
146 } 143 }
147 } 144 }
148#endif
149 } 145 }
150#endif 146#endif
151 } 147 }
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
index 9818f01a..99bf7101 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
@@ -4,12 +4,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
4{ 4{
5 using System; 5 using System;
6 using System.Collections.Generic; 6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.IO;
9 using System.Linq; 7 using System.Linq;
10 using WixToolset.Core.Bind; 8 using WixToolset.Core.Bind;
11 using WixToolset.Data; 9 using WixToolset.Data;
12 using WixToolset.Data.Tuples;
13 using WixToolset.Data.WindowsInstaller; 10 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Data.WindowsInstaller.Rows; 11 using WixToolset.Data.WindowsInstaller.Rows;
15 using WixToolset.Extensibility; 12 using WixToolset.Extensibility;
@@ -17,90 +14,39 @@ namespace WixToolset.Core.WindowsInstaller.Bind
17 14
18 internal class GetFileFacadesFromTransforms 15 internal class GetFileFacadesFromTransforms
19 { 16 {
20 public GetFileFacadesFromTransforms(IMessaging messaging, IEnumerable<SubStorage> subStorages, TableDefinitionCollection tableDefinitions) 17 public GetFileFacadesFromTransforms(IMessaging messaging, FileSystemManager fileSystemManager, IEnumerable<SubStorage> subStorages)
21 { 18 {
22 this.Messaging = messaging; 19 this.Messaging = messaging;
20 this.FileSystemManager = fileSystemManager;
23 this.SubStorages = subStorages; 21 this.SubStorages = subStorages;
24 this.TableDefinitions = tableDefinitions;
25 } 22 }
26 23
27 public IEnumerable<IFileSystemExtension> Extensions { get; }
28
29 private IMessaging Messaging { get; } 24 private IMessaging Messaging { get; }
30 25
31 public IEnumerable<SubStorage> SubStorages { get; } 26 private FileSystemManager FileSystemManager { get; }
32 27
33 private TableDefinitionCollection TableDefinitions { get; } 28 private IEnumerable<SubStorage> SubStorages { get; }
34 29
35 public IEnumerable<FileFacade> FileFacades { get; private set; } 30 public IEnumerable<FileFacade> FileFacades { get; private set; }
36 31
37 public void Execute() 32 public void Execute()
38 { 33 {
39 var allFileRows = new List<FileFacade>(); 34 var allFileRows = new List<FileFacade>();
40 var copyToPatch = (allFileRows != null);
41#if TODO_PATCHING
42 var patchMediaRows = new RowDictionary<MediaRow>();
43 35
44 var patchMediaFileRows = new Dictionary<int, RowDictionary<WixFileRow>>(); 36 var patchMediaRows = new RowDictionary<MediaRow>();
45 37
46 var patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]); 38 var patchMediaFileRows = new Dictionary<int, RowDictionary<FileRow>>();
47 var patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]);
48 39
49 if (copyFromPatch) 40 //var patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]);
50 {
51 // index patch files by diskId+fileId
52 foreach (WixFileRow patchFileRow in patchFileTable.Rows)
53 {
54 int diskId = patchFileRow.DiskId;
55 if (!patchMediaFileRows.TryGetValue(diskId, out var mediaFileRows))
56 {
57 mediaFileRows = new RowDictionary<WixFileRow>();
58 patchMediaFileRows.Add(diskId, mediaFileRows);
59 }
60
61 mediaFileRows.Add(patchFileRow);
62 }
63
64 var patchMediaTable = this.Output.EnsureTable(this.TableDefinitions["Media"]);
65 patchMediaRows = new RowDictionary<MediaRow>(patchMediaTable);
66 }
67 41
68 // Index paired transforms by name without their "#" prefix. 42 // Index paired transforms by name without their "#" prefix.
69 var pairedTransforms = this.SubStorages.Where(s => s.Name.StartsWith("#")).ToDictionary(s => s.Name.Substring(1), s => s.Data); 43 var pairedTransforms = this.SubStorages.Where(s => s.Name.StartsWith("#")).ToDictionary(s => s.Name, s => s.Data);
70 44
71 foreach (var substorage in this.SubStorages) 45 // Enumerate through main transforms.
46 foreach (var substorage in this.SubStorages.Where(s => !s.Name.StartsWith("#")))
72 { 47 {
73 if (substorage.Name.StartsWith("#"))
74 {
75 // Skip paired transforms.
76 continue;
77 }
78
79 var mainTransform = substorage.Data; 48 var mainTransform = substorage.Data;
80 var mainWixFiles = new RowDictionary<WixFileRow>(mainTransform.Tables["WixFile"]);
81 var mainMsiFileHashIndex = new RowDictionary<Row>(mainTransform.Tables["MsiFileHash"]);
82
83 var mainFileTable = mainTransform.Tables["File"]; 49 var mainFileTable = mainTransform.Tables["File"];
84 var pairedTransform = pairedTransforms[substorage.Name];
85
86 // copy Media.LastSequence and index the MsiFileHash table if it exists.
87 if (copyFromPatch)
88 {
89 var pairedMediaTable = pairedTransform.Tables["Media"];
90 foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows)
91 {
92 var patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId);
93 pairedMediaRow.Fields[1] = patchMediaRow.Fields[1];
94 }
95
96 if (null != mainMsiFileHashTable)
97 {
98 mainMsiFileHashIndex = new RowDictionary<Row>(mainMsiFileHashTable);
99 }
100
101 // Validate file row changes for keypath-related issues
102 this.ValidateFileRowChanges(mainTransform);
103 }
104 50
105 if (null == mainFileTable) 51 if (null == mainFileTable)
106 { 52 {
@@ -108,74 +54,21 @@ namespace WixToolset.Core.WindowsInstaller.Bind
108 } 54 }
109 55
110 // Index File table of pairedTransform 56 // Index File table of pairedTransform
57 var pairedTransform = pairedTransforms["#" + substorage.Name];
111 var pairedFileRows = new RowDictionary<FileRow>(pairedTransform.Tables["File"]); 58 var pairedFileRows = new RowDictionary<FileRow>(pairedTransform.Tables["File"]);
112 59
113 foreach (FileRow mainFileRow in mainFileTable.Rows) 60 foreach (FileRow mainFileRow in mainFileTable.Rows.Where(f => f.Operation != RowOperation.Delete))
114 { 61 {
115 if (RowOperation.Delete == mainFileRow.Operation) 62 var mainFileId = mainFileRow.File;
116 {
117 continue;
118 }
119 else if (RowOperation.None == mainFileRow.Operation)
120 {
121 continue;
122 }
123 63
124 var mainWixFileRow = mainWixFiles.Get(mainFileRow.File); 64 // We need compare the underlying files and include all file changes.
65 var objectField = (ObjectField)mainFileRow.Fields[9];
66 var pairedFileRow = pairedFileRows.Get(mainFileId);
125 67
126 if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes. 68 // If the file is new, we always need to add it to the patch.
69 if (mainFileRow.Operation == RowOperation.Add)
127 { 70 {
128 var objectField = (ObjectField)mainWixFileRow.Fields[6]; 71 if (null != pairedFileRow) // RowOperation.Add
129 var pairedFileRow = pairedFileRows.Get(mainFileRow.File);
130
131 // If the file is new, we always need to add it to the patch.
132 if (mainFileRow.Operation != RowOperation.Add)
133 {
134 // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare.
135 if (null == objectField.PreviousData)
136 {
137 if (mainFileRow.Operation == RowOperation.None)
138 {
139 continue;
140 }
141 }
142 else
143 {
144 // TODO: should this entire condition be placed in the binder file manager?
145 if ((0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) &&
146 !this.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString()))
147 {
148 // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified.
149 mainFileRow.Operation = RowOperation.Modify;
150 if (null != pairedFileRow)
151 {
152 // Always patch-added, but never non-compressed.
153 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
154 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
155 pairedFileRow.Fields[6].Modified = true;
156 pairedFileRow.Operation = RowOperation.Modify;
157 }
158 }
159 else
160 {
161 // The File is same. We need mark all the attributes as unchanged.
162 mainFileRow.Operation = RowOperation.None;
163 foreach (var field in mainFileRow.Fields)
164 {
165 field.Modified = false;
166 }
167
168 if (null != pairedFileRow)
169 {
170 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
171 pairedFileRow.Fields[6].Modified = false;
172 pairedFileRow.Operation = RowOperation.None;
173 }
174 continue;
175 }
176 }
177 }
178 else if (null != pairedFileRow) // RowOperation.Add
179 { 72 {
180 // Always patch-added, but never non-compressed. 73 // Always patch-added, but never non-compressed.
181 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded; 74 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
@@ -184,402 +77,93 @@ namespace WixToolset.Core.WindowsInstaller.Bind
184 pairedFileRow.Operation = RowOperation.Add; 77 pairedFileRow.Operation = RowOperation.Add;
185 } 78 }
186 } 79 }
187 80 else
188 // index patch files by diskId+fileId
189 int diskId = mainWixFileRow.DiskId;
190
191 if (!patchMediaFileRows.TryGetValue(diskId, out var mediaFileRows))
192 {
193 mediaFileRows = new RowDictionary<WixFileRow>();
194 patchMediaFileRows.Add(diskId, mediaFileRows);
195 }
196
197 var fileId = mainFileRow.File;
198 var patchFileRow = mediaFileRows.Get(fileId);
199 if (copyToPatch)
200 { 81 {
201 if (null == patchFileRow) 82 // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare.
202 { 83 if (null == objectField.PreviousData)
203 var patchActualFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
204 patchActualFileRow.CopyFrom(mainFileRow);
205
206 patchFileRow = (WixFileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
207 patchFileRow.CopyFrom(mainWixFileRow);
208
209 mediaFileRows.Add(patchFileRow);
210
211 allFileRows.Add(new FileFacade(patchActualFileRow, patchFileRow, null)); // TODO: should we be passing along delta information? Probably, right?
212 }
213 else
214 { 84 {
215 // TODO: confirm the rest of data is identical? 85 if (mainFileRow.Operation == RowOperation.None)
216
217 // make sure Source is same. Otherwise we are silently ignoring a file.
218 if (0 != String.Compare(patchFileRow.Source, mainWixFileRow.Source, StringComparison.OrdinalIgnoreCase))
219 { 86 {
220 this.Messaging.Write(ErrorMessages.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source)); 87 continue;
221 } 88 }
222
223 // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow.
224 patchFileRow.AppendPreviousDataFrom(mainWixFileRow);
225 }
226 }
227 //else
228 //{
229 // // copy data from the patch back to the transform
230 // if (null != patchFileRow)
231 // {
232 // var pairedFileRow = pairedFileRows.Get(fileId);
233 // for (var i = 0; i < patchFileRow.Fields.Length; i++)
234 // {
235 // var patchValue = patchFileRow[i] == null ? String.Empty : patchFileRow.FieldAsString(i);
236 // var mainValue = mainFileRow[i] == null ? String.Empty : mainFileRow.FieldAsString(i);
237
238 // if (1 == i)
239 // {
240 // // File.Component_ changes should not come from the shared file rows
241 // // that contain the file information as each individual transform might
242 // // have different changes (or no changes at all).
243 // }
244 // // File.Attributes should not changed for binary deltas
245 // else if (6 == i)
246 // {
247 // if (null != patchFileRow.Patch)
248 // {
249 // // File.Attribute should not change for binary deltas
250 // pairedFileRow.Attributes = mainFileRow.Attributes;
251 // mainFileRow.Fields[i].Modified = false;
252 // }
253 // }
254 // // File.Sequence is updated in pairedTransform, not mainTransform
255 // else if (7 == i)
256 // {
257 // // file sequence is updated in Patch table instead of File table for delta patches
258 // if (null != patchFileRow.Patch)
259 // {
260 // pairedFileRow.Fields[i].Modified = false;
261 // }
262 // else
263 // {
264 // pairedFileRow[i] = patchFileRow[i];
265 // pairedFileRow.Fields[i].Modified = true;
266 // }
267 // mainFileRow.Fields[i].Modified = false;
268 // }
269 // else if (patchValue != mainValue)
270 // {
271 // mainFileRow[i] = patchFileRow[i];
272 // mainFileRow.Fields[i].Modified = true;
273 // if (mainFileRow.Operation == RowOperation.None)
274 // {
275 // mainFileRow.Operation = RowOperation.Modify;
276 // }
277 // }
278 // }
279
280 // // copy MsiFileHash row for this File
281 // if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out var patchHashRow))
282 // {
283 // patchHashRow = patchFileRow.Hash;
284 // }
285
286 // if (null != patchHashRow)
287 // {
288 // var mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]);
289 // var mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers);
290 // for (var i = 0; i < patchHashRow.Fields.Length; i++)
291 // {
292 // mainHashRow[i] = patchHashRow[i];
293 // if (i > 1)
294 // {
295 // // assume all hash fields have been modified
296 // mainHashRow.Fields[i].Modified = true;
297 // }
298 // }
299
300 // // assume the MsiFileHash operation follows the File one
301 // mainHashRow.Operation = mainFileRow.Operation;
302 // }
303
304 // // copy MsiAssemblyName rows for this File
305 // List<Row> patchAssemblyNameRows = patchFileRow.AssemblyNames;
306 // if (null != patchAssemblyNameRows)
307 // {
308 // var mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
309 // foreach (var patchAssemblyNameRow in patchAssemblyNameRows)
310 // {
311 // // Copy if there isn't an identical modified/added row already in the transform.
312 // var foundMatchingModifiedRow = false;
313 // foreach (var mainAssemblyNameRow in mainAssemblyNameTable.Rows)
314 // {
315 // if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/')))
316 // {
317 // foundMatchingModifiedRow = true;
318 // break;
319 // }
320 // }
321
322 // if (!foundMatchingModifiedRow)
323 // {
324 // var mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers);
325 // for (var i = 0; i < patchAssemblyNameRow.Fields.Length; i++)
326 // {
327 // mainAssemblyNameRow[i] = patchAssemblyNameRow[i];
328 // }
329
330 // // assume value field has been modified
331 // mainAssemblyNameRow.Fields[2].Modified = true;
332 // mainAssemblyNameRow.Operation = mainFileRow.Operation;
333 // }
334 // }
335 // }
336
337 // // Add patch header for this file
338 // if (null != patchFileRow.Patch)
339 // {
340 // // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables.
341 // this.AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow);
342 // this.AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow);
343
344 // // Add to Patch table
345 // var patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]);
346 // if (0 == patchTable.Rows.Count)
347 // {
348 // patchTable.Operation = TableOperation.Add;
349 // }
350
351 // var patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers);
352 // patchRow[0] = patchFileRow.File;
353 // patchRow[1] = patchFileRow.Sequence;
354
355 // var patchFile = new FileInfo(patchFileRow.Source);
356 // patchRow[2] = (int)patchFile.Length;
357 // patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1;
358
359 // var streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1];
360 // if (Msi.MsiInterop.MsiMaxStreamNameLength < streamName.Length)
361 // {
362 // streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_');
363
364 // var patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]);
365 // if (0 == patchHeadersTable.Rows.Count)
366 // {
367 // patchHeadersTable.Operation = TableOperation.Add;
368 // }
369
370 // var patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers);
371 // patchHeadersRow[0] = streamName;
372 // patchHeadersRow[1] = patchFileRow.Patch;
373 // patchRow[5] = streamName;
374 // patchHeadersRow.Operation = RowOperation.Add;
375 // }
376 // else
377 // {
378 // patchRow[4] = patchFileRow.Patch;
379 // }
380 // patchRow.Operation = RowOperation.Add;
381 // }
382 // }
383 // else
384 // {
385 // // TODO: throw because all transform rows should have made it into the patch
386 // }
387 //}
388 }
389 }
390#endif
391 this.FileFacades = allFileRows;
392 }
393
394 /// <summary>
395 /// Adds the PatchFiles action to the sequence table if it does not already exist.
396 /// </summary>
397 /// <param name="table">The sequence table to check or modify.</param>
398 /// <param name="mainTransform">The primary authoring transform.</param>
399 /// <param name="pairedTransform">The secondary patch transform.</param>
400 /// <param name="mainFileRow">The file row that contains information about the patched file.</param>
401 private void AddPatchFilesActionToSequenceTable(SequenceTable table, WindowsInstallerData mainTransform, WindowsInstallerData pairedTransform, Row mainFileRow)
402 {
403 var tableName = table.ToString();
404
405 // Find/add PatchFiles action (also determine sequence for it).
406 // Search mainTransform first, then pairedTransform (pairedTransform overrides).
407 var hasPatchFilesAction = false;
408 var installFilesSequence = 0;
409 var duplicateFilesSequence = 0;
410
411 TestSequenceTableForPatchFilesAction(
412 mainTransform.Tables[tableName],
413 ref hasPatchFilesAction,
414 ref installFilesSequence,
415 ref duplicateFilesSequence);
416 TestSequenceTableForPatchFilesAction(
417 pairedTransform.Tables[tableName],
418 ref hasPatchFilesAction,
419 ref installFilesSequence,
420 ref duplicateFilesSequence);
421 if (!hasPatchFilesAction)
422 {
423 WindowsInstallerStandard.TryGetStandardAction(tableName, "PatchFiles", out var patchFilesActionTuple);
424
425 var sequence = patchFilesActionTuple.Sequence;
426
427 // Test for default sequence value's appropriateness
428 if (installFilesSequence >= sequence || (0 != duplicateFilesSequence && duplicateFilesSequence <= sequence))
429 {
430 if (0 != duplicateFilesSequence)
431 {
432 if (duplicateFilesSequence < installFilesSequence)
433 {
434 throw new WixException(ErrorMessages.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
435 } 89 }
436 else 90 else
437 { 91 {
438 sequence = (duplicateFilesSequence + installFilesSequence) / 2; 92 // TODO: should this entire condition be placed in the binder file manager?
439 if (installFilesSequence == sequence || duplicateFilesSequence == sequence) 93 if (/*(0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) &&*/
94 !this.FileSystemManager.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString()))
440 { 95 {
441 throw new WixException(ErrorMessages.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action)); 96 // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified.
97 mainFileRow.Operation = RowOperation.Modify;
98 if (null != pairedFileRow)
99 {
100 // Always patch-added, but never non-compressed.
101 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
102 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
103 pairedFileRow.Fields[6].Modified = true;
104 pairedFileRow.Operation = RowOperation.Modify;
105 }
106 }
107 else
108 {
109 // The File is same. We need mark all the attributes as unchanged.
110 mainFileRow.Operation = RowOperation.None;
111 foreach (var field in mainFileRow.Fields)
112 {
113 field.Modified = false;
114 }
115
116 if (null != pairedFileRow)
117 {
118 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
119 pairedFileRow.Fields[6].Modified = false;
120 pairedFileRow.Operation = RowOperation.None;
121 }
122 continue;
442 } 123 }
443 } 124 }
444 } 125 }
445 else
446 {
447 sequence = installFilesSequence + 1;
448 }
449 }
450 126
451 var sequenceTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]); 127 // index patch files by diskId+fileId
452 if (0 == sequenceTable.Rows.Count) 128 var diskId = mainFileRow.DiskId;
453 {
454 sequenceTable.Operation = TableOperation.Add;
455 }
456
457 var patchAction = sequenceTable.CreateRow(null);
458 patchAction[0] = patchFilesActionTuple.Action;
459 patchAction[1] = patchFilesActionTuple.Condition;
460 patchAction[2] = sequence;
461 patchAction.Operation = RowOperation.Add;
462 }
463 }
464 129
465 /// <summary> 130 if (!patchMediaFileRows.TryGetValue(diskId, out var mediaFileRows))
466 /// Tests sequence table for PatchFiles and associated actions
467 /// </summary>
468 /// <param name="sequenceTable">The table to test.</param>
469 /// <param name="hasPatchFilesAction">Set to true if PatchFiles action is found. Left unchanged otherwise.</param>
470 /// <param name="installFilesSequence">Set to sequence value of InstallFiles action if found. Left unchanged otherwise.</param>
471 /// <param name="duplicateFilesSequence">Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise.</param>
472 private static void TestSequenceTableForPatchFilesAction(Table sequenceTable, ref bool hasPatchFilesAction, ref int installFilesSequence, ref int duplicateFilesSequence)
473 {
474 if (null != sequenceTable)
475 {
476 foreach (var row in sequenceTable.Rows)
477 {
478 var actionName = row.FieldAsString(0);
479 switch (actionName)
480 { 131 {
481 case "PatchFiles": 132 mediaFileRows = new RowDictionary<FileRow>();
482 hasPatchFilesAction = true; 133 patchMediaFileRows.Add(diskId, mediaFileRows);
483 break;
484
485 case "InstallFiles":
486 installFilesSequence = row.FieldAsInteger(2);
487 break;
488
489 case "DuplicateFiles":
490 duplicateFilesSequence = row.FieldAsInteger(2);
491 break;
492 } 134 }
493 }
494 }
495 }
496
497 /// <summary>
498 /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component.
499 /// </summary>
500 /// <param name="output">The output to validate.</param>
501 private void ValidateFileRowChanges(WindowsInstallerData transform)
502 {
503 var componentTable = transform.Tables["Component"];
504 var fileTable = transform.Tables["File"];
505
506 // There's no sense validating keypaths if the transform has no component or file table
507 if (componentTable == null || fileTable == null)
508 {
509 return;
510 }
511 135
512 var componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count); 136 var patchFileRow = mediaFileRows.Get(mainFileId);
513
514 // Index the Component table for non-directory & non-registry key paths.
515 foreach (var row in componentTable.Rows)
516 {
517 var keyPath = row.FieldAsString(5);
518 if (keyPath != null && 0 != (row.FieldAsInteger(3) & WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath))
519 {
520 componentKeyPath.Add(row.FieldAsString(0), keyPath);
521 }
522 }
523 137
524 var componentWithChangedKeyPath = new Dictionary<string, string>(); 138 if (null == patchFileRow)
525 var componentWithNonKeyPathChanged = new Dictionary<string, string>(); 139 {
526 // Verify changes in the file table, now that file diffing has occurred 140 //patchFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
527 foreach (FileRow row in fileTable.Rows) 141 patchFileRow = (FileRow)mainFileRow.TableDefinition.CreateRow(mainFileRow.SourceLineNumbers);
528 { 142 mainFileRow.CopyTo(patchFileRow);
529 if (RowOperation.Modify != row.Operation)
530 {
531 continue;
532 }
533 143
534 var fileId = row.FieldAsString(0); 144 mediaFileRows.Add(patchFileRow);
535 var componentId = row.FieldAsString(1);
536 145
537 // If this file is the keypath of a component 146 allFileRows.Add(new FileFacade(patchFileRow)); // TODO: should we be passing along delta information? Probably, right?
538 if (componentKeyPath.ContainsValue(fileId))
539 {
540 if (!componentWithChangedKeyPath.ContainsKey(componentId))
541 {
542 componentWithChangedKeyPath.Add(componentId, fileId);
543 } 147 }
544 } 148 else
545 else
546 {
547 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
548 { 149 {
549 componentWithNonKeyPathChanged.Add(componentId, fileId); 150 // TODO: confirm the rest of data is identical?
550 }
551 }
552 }
553
554 foreach (var componentFile in componentWithNonKeyPathChanged)
555 {
556 // Make sure all changes to non keypath files also had a change in the keypath.
557 if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.TryGetValue(componentFile.Key, out var keyPath))
558 {
559 this.Messaging.Write(WarningMessages.UpdateOfNonKeyPathFile(componentFile.Value, componentFile.Key, keyPath));
560 }
561 }
562 }
563 151
564 private bool CompareFiles(string targetFile, string updatedFile) 152 // make sure Source is same. Otherwise we are silently ignoring a file.
565 { 153 if (0 != String.Compare(patchFileRow.Source, mainFileRow.Source, StringComparison.OrdinalIgnoreCase))
566 bool? compared = null; 154 {
567 foreach (var extension in this.Extensions) 155 this.Messaging.Write(ErrorMessages.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, mainFileId, patchFileRow.Source, mainFileRow.Source));
568 { 156 }
569 compared = extension.CompareFiles(targetFile, updatedFile);
570 157
571 if (compared.HasValue) 158#if TODO_PATCHING_DELTA
572 { 159 // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow.
573 break; 160 patchFileRow.AppendPreviousDataFrom(mainFileRow);
161#endif
162 }
574 } 163 }
575 } 164 }
576 165
577 if (!compared.HasValue) 166 this.FileFacades = allFileRows;
578 {
579 throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result.
580 }
581
582 return compared.Value;
583 } 167 }
584 } 168 }
585} 169}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
index cd6170d0..bddcccb7 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
@@ -12,6 +12,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
12 using WixToolset.Core.Native; 12 using WixToolset.Core.Native;
13 using WixToolset.Core.WindowsInstaller.Msi; 13 using WixToolset.Core.WindowsInstaller.Msi;
14 using WixToolset.Data; 14 using WixToolset.Data;
15 using WixToolset.Data.Tuples;
15 using WixToolset.Data.WindowsInstaller; 16 using WixToolset.Data.WindowsInstaller;
16 using WixToolset.Data.WindowsInstaller.Rows; 17 using WixToolset.Data.WindowsInstaller.Rows;
17 using WixToolset.Extensibility.Services; 18 using WixToolset.Extensibility.Services;
@@ -34,7 +35,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
34 35
35 public string OutputPath { private get; set; } 36 public string OutputPath { private get; set; }
36 37
37 public IEnumerable<string> SuppressedTableNames { private get; set; } 38 public TableDefinitionCollection TableDefinitions { private get; set; }
38 39
39 public string IntermediateFolder { private get; set; } 40 public string IntermediateFolder { private get; set; }
40 41
@@ -49,6 +50,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
49 return; 50 return;
50 } 51 }
51 52
53 var suppressedTableNames = this.AddBackSuppresedSequenceTables();
54
52 IMsmMerge2 merge = null; 55 IMsmMerge2 merge = null;
53 bool commit = true; 56 bool commit = true;
54 bool logOpen = false; 57 bool logOpen = false;
@@ -212,9 +215,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
212 return; 215 return;
213 } 216 }
214 217
215 using (Database db = new Database(this.OutputPath, OpenDatabase.Direct)) 218 using (var db = new Database(this.OutputPath, OpenDatabase.Direct))
216 { 219 {
217 Table suppressActionTable = this.Output.Tables["WixSuppressAction"]; 220 var suppressActionTable = this.Output.Tables["WixSuppressAction"];
218 221
219 // suppress individual actions 222 // suppress individual actions
220 if (null != suppressActionTable) 223 if (null != suppressActionTable)
@@ -239,40 +242,38 @@ namespace WixToolset.Core.WindowsInstaller.Bind
239 } 242 }
240 243
241 // query for merge module actions in suppressed sequences and drop them 244 // query for merge module actions in suppressed sequences and drop them
242 foreach (string tableName in this.SuppressedTableNames) 245 foreach (var tableName in suppressedTableNames)
243 { 246 {
244 if (!db.TableExists(tableName)) 247 if (!db.TableExists(tableName))
245 { 248 {
246 continue; 249 continue;
247 } 250 }
248 251
249 using (View view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName))) 252 using (var view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName)))
250 { 253 {
251 foreach (Record resultRecord in view.Records) 254 foreach (var resultRecord in view.Records)
252 { 255 {
253 this.Messaging.Write(WarningMessages.SuppressMergedAction(resultRecord.GetString(1), tableName)); 256 this.Messaging.Write(WarningMessages.SuppressMergedAction(resultRecord.GetString(1), tableName));
254 } 257 }
255 } 258 }
256 259
257 // drop suppressed sequences 260 // drop suppressed sequences
258 using (View view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName))) 261 using (var view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName)))
259 { 262 {
260 } 263 }
261 264
262 // delete the validation rows 265 // delete the validation rows
263 using (View view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?"))) 266 using (var view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?")))
267 using (var record = new Record(1))
264 { 268 {
265 using (Record record = new Record(1)) 269 record.SetString(1, tableName);
266 { 270 view.Execute(record);
267 record.SetString(1, tableName);
268 view.Execute(record);
269 }
270 } 271 }
271 } 272 }
272 273
273 // now update the Attributes column for the files from the Merge Modules 274 // now update the Attributes column for the files from the Merge Modules
274 this.Messaging.Write(VerboseMessages.ResequencingMergeModuleFiles()); 275 this.Messaging.Write(VerboseMessages.ResequencingMergeModuleFiles());
275 using (View view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?")) 276 using (var view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?"))
276 { 277 {
277 foreach (var file in this.FileFacades) 278 foreach (var file in this.FileFacades)
278 { 279 {
@@ -281,13 +282,13 @@ namespace WixToolset.Core.WindowsInstaller.Bind
281 continue; 282 continue;
282 } 283 }
283 284
284 using (Record record = new Record(1)) 285 using (var record = new Record(1))
285 { 286 {
286 record.SetString(1, file.Id); 287 record.SetString(1, file.Id);
287 view.Execute(record); 288 view.Execute(record);
288 } 289 }
289 290
290 using (Record recordUpdate = view.Fetch()) 291 using (var recordUpdate = view.Fetch())
291 { 292 {
292 if (null == recordUpdate) 293 if (null == recordUpdate)
293 { 294 {
@@ -332,5 +333,31 @@ namespace WixToolset.Core.WindowsInstaller.Bind
332 db.Commit(); 333 db.Commit();
333 } 334 }
334 } 335 }
336
337 private IEnumerable<string> AddBackSuppresedSequenceTables()
338 {
339 // Add back possibly suppressed sequence tables since all sequence tables must be present
340 // for the merge process to work. We'll drop the suppressed sequence tables again as
341 // necessary.
342 var suppressedTableNames = new HashSet<string>();
343
344 foreach (SequenceTable sequence in Enum.GetValues(typeof(SequenceTable)))
345 {
346 var sequenceTableName = (sequence == SequenceTable.AdvertiseExecuteSequence) ? "AdvtExecuteSequence" : sequence.ToString();
347 var sequenceTable = this.Output.Tables[sequenceTableName];
348
349 if (null == sequenceTable)
350 {
351 sequenceTable = this.Output.EnsureTable(this.TableDefinitions[sequenceTableName]);
352 }
353
354 if (0 == sequenceTable.Rows.Count)
355 {
356 suppressedTableNames.Add(sequenceTableName);
357 }
358 }
359
360 return suppressedTableNames;
361 }
335 } 362 }
336} 363}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/PatchTransform.cs b/src/WixToolset.Core.WindowsInstaller/Bind/PatchTransform.cs
index 5ada29de..4d849753 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/PatchTransform.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/PatchTransform.cs
@@ -2,14 +2,7 @@
2 2
3namespace WixToolset.Core.WindowsInstaller.Bind 3namespace WixToolset.Core.WindowsInstaller.Bind
4{ 4{
5 using System;
6 using System.Collections;
7 using System.Globalization;
8 using System.Text;
9 using System.Text.RegularExpressions;
10 using WixToolset.Data;
11 using WixToolset.Data.WindowsInstaller; 5 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Extensibility;
13 6
14 internal class PatchTransform 7 internal class PatchTransform
15 { 8 {
@@ -22,225 +15,5 @@ namespace WixToolset.Core.WindowsInstaller.Bind
22 public string Baseline { get; } 15 public string Baseline { get; }
23 16
24 public WindowsInstallerData Transform { get; } 17 public WindowsInstallerData Transform { get; }
25
26 /// <summary>
27 /// Validates that the differences in the transform are valid for patch transforms.
28 /// </summary>
29 public void Validate()
30 {
31#if TODO_PATCHING
32 // Changing the ProdocutCode in a patch transform is not recommended.
33 Table propertyTable = this.Transform.Tables["Property"];
34 if (null != propertyTable)
35 {
36 foreach (Row row in propertyTable.Rows)
37 {
38 // Only interested in modified rows; fast check.
39 if (RowOperation.Modify == row.Operation)
40 {
41 if (0 == String.CompareOrdinal("ProductCode", (string)row[0]))
42 {
43 this.OnMessage(WixWarnings.MajorUpgradePatchNotRecommended());
44 }
45 }
46 }
47 }
48
49 // If there is nothing in the component table we can return early because the remaining checks are component based.
50 Table componentTable = this.Transform.Tables["Component"];
51 if (null == componentTable)
52 {
53 return;
54 }
55
56 // Index Feature table row operations
57 Table featureTable = this.Transform.Tables["Feature"];
58 Table featureComponentsTable = this.Transform.Tables["FeatureComponents"];
59 Hashtable featureOps = null;
60 if (null != featureTable)
61 {
62 int capacity = featureTable.Rows.Count;
63 featureOps = new Hashtable(capacity);
64
65 foreach (Row row in featureTable.Rows)
66 {
67 featureOps[(string)row[0]] = row.Operation;
68 }
69 }
70 else
71 {
72 featureOps = new Hashtable();
73 }
74
75 // Index Component table and check for keypath modifications
76 Hashtable deletedComponent = new Hashtable();
77 Hashtable componentKeyPath = new Hashtable();
78 foreach (Row row in componentTable.Rows)
79 {
80 string id = row.Fields[0].Data.ToString();
81 string keypath = (null == row.Fields[5].Data) ? String.Empty : row.Fields[5].Data.ToString();
82
83 componentKeyPath.Add(id, keypath);
84 if (RowOperation.Delete == row.Operation)
85 {
86 deletedComponent.Add(id, row);
87 }
88 else if (RowOperation.Modify == row.Operation)
89 {
90 if (row.Fields[1].Modified)
91 {
92 // Changing the guid of a component is equal to deleting the old one and adding a new one.
93 deletedComponent.Add(id, row);
94 }
95
96 // If the keypath is modified its an error
97 if (row.Fields[5].Modified)
98 {
99 this.OnMessage(WixErrors.InvalidKeypathChange(row.SourceLineNumbers, id, this.transformPath));
100 }
101 }
102 }
103
104 // Verify changes in the file table
105 Table fileTable = this.Transform.Tables["File"];
106 if (null != fileTable)
107 {
108 Hashtable componentWithChangedKeyPath = new Hashtable();
109 foreach (Row row in fileTable.Rows)
110 {
111 if (RowOperation.None != row.Operation)
112 {
113 string fileId = row.Fields[0].Data.ToString();
114 string componentId = row.Fields[1].Data.ToString();
115
116 // If this file is the keypath of a component
117 if (String.Equals((string)componentKeyPath[componentId], fileId, StringComparison.Ordinal))
118 {
119 if (row.Fields[2].Modified)
120 {
121 // You cant change the filename of a file that is the keypath of a component.
122 this.OnMessage(WixErrors.InvalidKeypathChange(row.SourceLineNumbers, componentId, this.transformPath));
123 }
124
125 if (!componentWithChangedKeyPath.ContainsKey(componentId))
126 {
127 componentWithChangedKeyPath.Add(componentId, fileId);
128 }
129 }
130
131 if (RowOperation.Delete == row.Operation)
132 {
133 // If the file is removed from a component that is not deleted.
134 if (!deletedComponent.ContainsKey(componentId))
135 {
136 bool foundRemoveFileEntry = false;
137 string filename = Common.GetName((string)row[2], false, true);
138
139 Table removeFileTable = this.Transform.Tables["RemoveFile"];
140 if (null != removeFileTable)
141 {
142 foreach (Row removeFileRow in removeFileTable.Rows)
143 {
144 if (RowOperation.Delete == removeFileRow.Operation)
145 {
146 continue;
147 }
148
149 if (componentId == (string)removeFileRow[1])
150 {
151 // Check if there is a RemoveFile entry for this file
152 if (null != removeFileRow[2])
153 {
154 string removeFileName = Common.GetName((string)removeFileRow[2], false, true);
155
156 // Convert the MSI format for a wildcard string to Regex format.
157 removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\.");
158
159 Regex regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
160 if (regex.IsMatch(filename))
161 {
162 foundRemoveFileEntry = true;
163 break;
164 }
165 }
166 }
167 }
168 }
169
170 if (!foundRemoveFileEntry)
171 {
172 this.OnMessage(WixWarnings.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId));
173 }
174 }
175 }
176 }
177 }
178 }
179
180 if (0 < deletedComponent.Count)
181 {
182 // Index FeatureComponents table.
183 Hashtable featureComponents = new Hashtable();
184
185 if (null != featureComponentsTable)
186 {
187 foreach (Row row in featureComponentsTable.Rows)
188 {
189 ArrayList features;
190 string componentId = row.Fields[1].Data.ToString();
191
192 if (featureComponents.Contains(componentId))
193 {
194 features = (ArrayList)featureComponents[componentId];
195 }
196 else
197 {
198 features = new ArrayList();
199 featureComponents.Add(componentId, features);
200 }
201 features.Add(row.Fields[0].Data.ToString());
202 }
203 }
204
205 // Check to make sure if a component was deleted, the feature was too.
206 foreach (DictionaryEntry entry in deletedComponent)
207 {
208 if (featureComponents.Contains(entry.Key.ToString()))
209 {
210 ArrayList features = (ArrayList)featureComponents[entry.Key.ToString()];
211 foreach (string featureId in features)
212 {
213 if (!featureOps.ContainsKey(featureId) || RowOperation.Delete != (RowOperation)featureOps[featureId])
214 {
215 // The feature was not deleted.
216 this.OnMessage(WixErrors.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, this.transformPath));
217 }
218 }
219 }
220 }
221 }
222
223 // Warn if new components are added to existing features
224 if (null != featureComponentsTable)
225 {
226 foreach (Row row in featureComponentsTable.Rows)
227 {
228 if (RowOperation.Add == row.Operation)
229 {
230 // Check if the feature is in the Feature table
231 string feature_ = (string)row[0];
232 string component_ = (string)row[1];
233
234 // Features may not be present if not referenced
235 if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_])
236 {
237 this.OnMessage(WixWarnings.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, this.transformPath));
238 }
239 }
240 }
241 }
242#endif
243 throw new NotImplementedException();
244 }
245 } 18 }
246} 19}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
index 4ca5ec48..63a8b3d9 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
@@ -39,20 +39,16 @@ namespace WixToolset.Core.WindowsInstaller.Bind
39 39
40 public void Execute() 40 public void Execute()
41 { 41 {
42 var assemblyNameTuples = this.Section.Tuples.OfType<MsiAssemblyNameTuple>().ToDictionary(t => t.Id.Id);
43
42 foreach (var file in this.UpdateFileFacades) 44 foreach (var file in this.UpdateFileFacades)
43 { 45 {
44 this.UpdateFileFacade(file); 46 this.UpdateFileFacade(file, assemblyNameTuples);
45 } 47 }
46 } 48 }
47 49
48 private void UpdateFileFacade(FileFacade facade) 50 private void UpdateFileFacade(FileFacade facade, Dictionary<string, MsiAssemblyNameTuple> assemblyNameTuples)
49 { 51 {
50 var assemblyNameTuples = new Dictionary<string, MsiAssemblyNameTuple>();
51 foreach (var assemblyTuple in this.Section.Tuples.OfType<MsiAssemblyNameTuple>())
52 {
53 assemblyNameTuples.Add(assemblyTuple.ComponentRef + "/" + assemblyTuple.Name, assemblyTuple);
54 }
55
56 FileInfo fileInfo = null; 52 FileInfo fileInfo = null;
57 try 53 try
58 { 54 {
@@ -335,7 +331,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
335 var lookup = String.Concat(facade.ComponentRef, "/", name); 331 var lookup = String.Concat(facade.ComponentRef, "/", name);
336 if (!assemblyNameTuples.TryGetValue(lookup, out var assemblyNameTuple)) 332 if (!assemblyNameTuples.TryGetValue(lookup, out var assemblyNameTuple))
337 { 333 {
338 assemblyNameTuple = this.Section.AddTuple(new MsiAssemblyNameTuple(facade.SourceLineNumber) 334 assemblyNameTuple = this.Section.AddTuple(new MsiAssemblyNameTuple(facade.SourceLineNumber, new Identifier(AccessModifier.Private, facade.ComponentRef, name))
339 { 335 {
340 ComponentRef = facade.ComponentRef, 336 ComponentRef = facade.ComponentRef,
341 Name = name, 337 Name = name,
@@ -348,6 +344,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
348 } 344 }
349 345
350 facade.AssemblyNames.Add(assemblyNameTuple); 346 facade.AssemblyNames.Add(assemblyNameTuple);
347
348 assemblyNameTuples.Add(assemblyNameTuple.Id.Id, assemblyNameTuple);
351 } 349 }
352 350
353 assemblyNameTuple.Value = value; 351 assemblyNameTuple.Value = value;
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs
index 5d18a230..9aab7b98 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs
@@ -6,62 +6,59 @@ namespace WixToolset.Core.WindowsInstaller.Bind
6 using System.Linq; 6 using System.Linq;
7 using WixToolset.Core.Bind; 7 using WixToolset.Core.Bind;
8 using WixToolset.Data; 8 using WixToolset.Data;
9 using WixToolset.Data.WindowsInstaller; 9 using WixToolset.Data.Tuples;
10 using WixToolset.Data.WindowsInstaller.Rows;
11 10
12 internal class UpdateMediaSequencesCommand 11 internal class UpdateMediaSequencesCommand
13 { 12 {
14 public UpdateMediaSequencesCommand(WindowsInstallerData output, List<FileFacade> fileFacades) 13 public UpdateMediaSequencesCommand(IntermediateSection section, List<FileFacade> fileFacades)
15 { 14 {
16 this.Output = output; 15 this.Section = section;
17 this.FileFacades = fileFacades; 16 this.FileFacades = fileFacades;
18 } 17 }
19 18
20 private WindowsInstallerData Output { get; } 19 private IntermediateSection Section { get; }
21 20
22 private List<FileFacade> FileFacades { get; } 21 private List<FileFacade> FileFacades { get; }
23 22
24 public void Execute() 23 public void Execute()
25 { 24 {
26 var fileRows = new RowDictionary<FileRow>(this.Output.Tables["File"]); 25 var mediaRows = this.Section.Tuples.OfType<MediaTuple>().ToDictionary(t => t.DiskId);
27 var mediaRows = new RowDictionary<MediaRow>(this.Output.Tables["Media"]);
28 26
29 // Calculate sequence numbers and media disk id layout for all file media information objects. 27 // Calculate sequence numbers and media disk id layout for all file media information objects.
30 if (OutputType.Module == this.Output.Type) 28 if (SectionType.Module == this.Section.Type)
31 { 29 {
32 var lastSequence = 0; 30 var lastSequence = 0;
33 31
34 // Order by Component to group the files by directory. 32 // Order by Component to group the files by directory.
35 var optimized = this.OptimizedFileFacades(); 33 var optimized = this.OptimizedFileFacades();
36 foreach (var fileId in optimized.Select(f => f.Id)) 34 foreach (var facade in optimized)
37 { 35 {
38 var fileRow = fileRows.Get(fileId); 36 facade.Sequence = ++lastSequence;
39 fileRow.Sequence = ++lastSequence;
40 } 37 }
41 } 38 }
42 else 39 else
43 { 40 {
44 var lastSequence = 0; 41 var lastSequence = 0;
45 MediaRow mediaRow = null; 42 MediaTuple mediaTuple = null;
46 var patchGroups = new Dictionary<int, List<FileFacade>>(); 43 var patchGroups = new Dictionary<int, List<FileFacade>>();
47 44
48 // sequence the non-patch-added files 45 // sequence the non-patch-added files
49 var optimized = this.OptimizedFileFacades(); 46 var optimized = this.OptimizedFileFacades();
50 foreach (var facade in optimized) 47 foreach (var facade in optimized)
51 { 48 {
52 if (null == mediaRow) 49 if (null == mediaTuple)
53 { 50 {
54 mediaRow = mediaRows.Get(facade.DiskId); 51 mediaTuple = mediaRows[facade.DiskId];
55 if (OutputType.Patch == this.Output.Type) 52 if (SectionType.Patch == this.Section.Type)
56 { 53 {
57 // patch Media cannot start at zero 54 // patch Media cannot start at zero
58 lastSequence = mediaRow.LastSequence; 55 lastSequence = mediaTuple.LastSequence ?? 1;
59 } 56 }
60 } 57 }
61 else if (mediaRow.DiskId != facade.DiskId) 58 else if (mediaTuple.DiskId != facade.DiskId)
62 { 59 {
63 mediaRow.LastSequence = lastSequence; 60 mediaTuple.LastSequence = lastSequence;
64 mediaRow = mediaRows.Get(facade.DiskId); 61 mediaTuple = mediaRows[facade.DiskId];
65 } 62 }
66 63
67 if (facade.PatchGroup.HasValue) 64 if (facade.PatchGroup.HasValue)
@@ -76,15 +73,14 @@ namespace WixToolset.Core.WindowsInstaller.Bind
76 } 73 }
77 else if (!facade.FromModule) 74 else if (!facade.FromModule)
78 { 75 {
79 var fileRow = fileRows.Get(facade.Id); 76 facade.Sequence = ++lastSequence;
80 fileRow.Sequence = ++lastSequence;
81 } 77 }
82 } 78 }
83 79
84 if (null != mediaRow) 80 if (null != mediaTuple)
85 { 81 {
86 mediaRow.LastSequence = lastSequence; 82 mediaTuple.LastSequence = lastSequence;
87 mediaRow = null; 83 mediaTuple = null;
88 } 84 }
89 85
90 // sequence the patch-added files 86 // sequence the patch-added files
@@ -92,24 +88,23 @@ namespace WixToolset.Core.WindowsInstaller.Bind
92 { 88 {
93 foreach (var facade in patchGroup) 89 foreach (var facade in patchGroup)
94 { 90 {
95 if (null == mediaRow) 91 if (null == mediaTuple)
96 { 92 {
97 mediaRow = mediaRows.Get(facade.DiskId); 93 mediaTuple = mediaRows[facade.DiskId];
98 } 94 }
99 else if (mediaRow.DiskId != facade.DiskId) 95 else if (mediaTuple.DiskId != facade.DiskId)
100 { 96 {
101 mediaRow.LastSequence = lastSequence; 97 mediaTuple.LastSequence = lastSequence;
102 mediaRow = mediaRows.Get(facade.DiskId); 98 mediaTuple = mediaRows[facade.DiskId];
103 } 99 }
104 100
105 var fileRow = fileRows.Get(facade.Id); 101 facade.Sequence = ++lastSequence;
106 fileRow.Sequence = ++lastSequence;
107 } 102 }
108 } 103 }
109 104
110 if (null != mediaRow) 105 if (null != mediaTuple)
111 { 106 {
112 mediaRow.LastSequence = lastSequence; 107 mediaTuple.LastSequence = lastSequence;
113 } 108 }
114 } 109 }
115 } 110 }
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
new file mode 100644
index 00000000..af2e8f85
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
@@ -0,0 +1,453 @@
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.Diagnostics;
8 using System.IO;
9 using System.Linq;
10 using WixToolset.Core.Bind;
11 using WixToolset.Data;
12 using WixToolset.Data.Tuples;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Data.WindowsInstaller.Rows;
15 using WixToolset.Extensibility.Services;
16
17 internal class UpdateTransformsWithFileFacades
18 {
19 public UpdateTransformsWithFileFacades(IMessaging messaging, WindowsInstallerData output, IEnumerable<SubStorage> subStorages, TableDefinitionCollection tableDefinitions, IEnumerable<FileFacade> fileFacades)
20 {
21 this.Messaging = messaging;
22 this.Output = output;
23 this.SubStorages = subStorages;
24 this.TableDefinitions = tableDefinitions;
25 this.FileFacades = fileFacades;
26 }
27
28 private IMessaging Messaging { get; }
29
30 private WindowsInstallerData Output { get; }
31
32 private IEnumerable<SubStorage> SubStorages { get; }
33
34 private TableDefinitionCollection TableDefinitions { get; }
35
36 private IEnumerable<FileFacade> FileFacades { get; }
37
38 public void Execute()
39 {
40 var fileFacadesByDiskId = new Dictionary<int, Dictionary<string, FileFacade>>();
41
42 // Index patch file facades by diskId+fileId.
43 foreach (var facade in this.FileFacades)
44 {
45 if (!fileFacadesByDiskId.TryGetValue(facade.DiskId, out var mediaFacades))
46 {
47 mediaFacades = new Dictionary<string, FileFacade>();
48 fileFacadesByDiskId.Add(facade.DiskId, mediaFacades);
49 }
50
51 mediaFacades.Add(facade.Id, facade);
52 }
53
54 var patchMediaRows = new RowDictionary<MediaRow>(this.Output.Tables["Media"]);
55
56 // Index paired transforms by name without the "#" prefix.
57 var pairedTransforms = this.SubStorages.Where(s => s.Name.StartsWith("#")).ToDictionary(s => s.Name, s => s.Data);
58
59 // Copy File bind data into substorages
60 foreach (var substorage in this.SubStorages.Where(s => !s.Name.StartsWith("#")))
61 {
62 var mainTransform = substorage.Data;
63
64 var mainMsiFileHashIndex = new RowDictionary<Row>(mainTransform.Tables["MsiFileHash"]);
65
66 var pairedTransform = pairedTransforms["#" + substorage.Name];
67
68 // Copy Media.LastSequence.
69 var pairedMediaTable = pairedTransform.Tables["Media"];
70 foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows)
71 {
72 var patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId);
73 pairedMediaRow.LastSequence = patchMediaRow.LastSequence;
74 }
75
76 // Validate file row changes for keypath-related issues
77 this.ValidateFileRowChanges(mainTransform);
78
79 // Index File table of pairedTransform
80 var pairedFileRows = new RowDictionary<FileRow>(pairedTransform.Tables["File"]);
81
82 var mainFileTable = mainTransform.Tables["File"];
83 if (null != mainFileTable)
84 {
85 // Remove the MsiFileHash table because it will be updated later with the final file hash for each file
86 mainTransform.Tables.Remove("MsiFileHash");
87
88 foreach (FileRow mainFileRow in mainFileTable.Rows)
89 {
90 if (RowOperation.Delete == mainFileRow.Operation)
91 {
92 continue;
93 }
94 else if (RowOperation.None == mainFileRow.Operation)
95 {
96 continue;
97 }
98
99 // Index patch files by diskId+fileId
100 if (!fileFacadesByDiskId.TryGetValue(mainFileRow.DiskId, out var mediaFacades))
101 {
102 mediaFacades = new Dictionary<string, FileFacade>();
103 fileFacadesByDiskId.Add(mainFileRow.DiskId, mediaFacades);
104 }
105
106 // copy data from the patch back to the transform
107 if (mediaFacades.TryGetValue(mainFileRow.File, out var facade))
108 {
109 var patchFileRow = facade.GetFileRow();
110 var pairedFileRow = pairedFileRows.Get(mainFileRow.File);
111
112 for (var i = 0; i < patchFileRow.Fields.Length; i++)
113 {
114 var patchValue = patchFileRow.FieldAsString(i) ?? String.Empty;
115 var mainValue = mainFileRow.FieldAsString(i) ?? String.Empty;
116
117 if (1 == i)
118 {
119 // File.Component_ changes should not come from the shared file rows
120 // that contain the file information as each individual transform might
121 // have different changes (or no changes at all).
122 }
123 else if (6 == i) // File.Attributes should not changed for binary deltas
124 {
125#if TODO_PATCHING_DELTA
126 if (null != patchFileRow.Patch)
127 {
128 // File.Attribute should not change for binary deltas
129 pairedFileRow.Attributes = mainFileRow.Attributes;
130 mainFileRow.Fields[i].Modified = false;
131 }
132#endif
133 }
134 else if (7 == i) // File.Sequence is updated in pairedTransform, not mainTransform
135 {
136 // file sequence is updated in Patch table instead of File table for delta patches
137#if TODO_PATCHING_DELTA
138 if (null != patchFileRow.Patch)
139 {
140 pairedFileRow.Fields[i].Modified = false;
141 }
142 else
143#endif
144 {
145 pairedFileRow[i] = patchFileRow[i];
146 pairedFileRow.Fields[i].Modified = true;
147 }
148 mainFileRow.Fields[i].Modified = false;
149 }
150 else if (patchValue != mainValue)
151 {
152 mainFileRow[i] = patchFileRow[i];
153 mainFileRow.Fields[i].Modified = true;
154 if (mainFileRow.Operation == RowOperation.None)
155 {
156 mainFileRow.Operation = RowOperation.Modify;
157 }
158 }
159 }
160
161 // Copy MsiFileHash row for this File.
162 if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out var patchHashRow))
163 {
164 //patchHashRow = patchFileRow.Hash;
165 throw new NotImplementedException();
166 }
167
168 if (null != patchHashRow)
169 {
170 var mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]);
171 var mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers);
172 for (var i = 0; i < patchHashRow.Fields.Length; i++)
173 {
174 mainHashRow[i] = patchHashRow[i];
175 if (i > 1)
176 {
177 // assume all hash fields have been modified
178 mainHashRow.Fields[i].Modified = true;
179 }
180 }
181
182 // assume the MsiFileHash operation follows the File one
183 mainHashRow.Operation = mainFileRow.Operation;
184 }
185
186 // copy MsiAssemblyName rows for this File
187#if TODO_PATCHING
188 List<Row> patchAssemblyNameRows = patchFileRow.AssemblyNames;
189 if (null != patchAssemblyNameRows)
190 {
191 var mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
192 foreach (var patchAssemblyNameRow in patchAssemblyNameRows)
193 {
194 // Copy if there isn't an identical modified/added row already in the transform.
195 var foundMatchingModifiedRow = false;
196 foreach (var mainAssemblyNameRow in mainAssemblyNameTable.Rows)
197 {
198 if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/')))
199 {
200 foundMatchingModifiedRow = true;
201 break;
202 }
203 }
204
205 if (!foundMatchingModifiedRow)
206 {
207 var mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers);
208 for (var i = 0; i < patchAssemblyNameRow.Fields.Length; i++)
209 {
210 mainAssemblyNameRow[i] = patchAssemblyNameRow[i];
211 }
212
213 // assume value field has been modified
214 mainAssemblyNameRow.Fields[2].Modified = true;
215 mainAssemblyNameRow.Operation = mainFileRow.Operation;
216 }
217 }
218 }
219#endif
220
221 // Add patch header for this file
222#if TODO_PATCHING_DELTA
223 if (null != patchFileRow.Patch)
224 {
225 // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables.
226 this.AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow);
227 this.AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow);
228
229 // Add to Patch table
230 var patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]);
231 if (0 == patchTable.Rows.Count)
232 {
233 patchTable.Operation = TableOperation.Add;
234 }
235
236 var patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers);
237 patchRow[0] = patchFileRow.File;
238 patchRow[1] = patchFileRow.Sequence;
239
240 var patchFile = new FileInfo(patchFileRow.Source);
241 patchRow[2] = (int)patchFile.Length;
242 patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1;
243
244 var streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1];
245 if (Msi.MsiInterop.MsiMaxStreamNameLength < streamName.Length)
246 {
247 streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_');
248
249 var patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]);
250 if (0 == patchHeadersTable.Rows.Count)
251 {
252 patchHeadersTable.Operation = TableOperation.Add;
253 }
254
255 var patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers);
256 patchHeadersRow[0] = streamName;
257 patchHeadersRow[1] = patchFileRow.Patch;
258 patchRow[5] = streamName;
259 patchHeadersRow.Operation = RowOperation.Add;
260 }
261 else
262 {
263 patchRow[4] = patchFileRow.Patch;
264 }
265 patchRow.Operation = RowOperation.Add;
266 }
267#endif
268 }
269 else
270 {
271 // TODO: throw because all transform rows should have made it into the patch
272 }
273 }
274 }
275
276 this.Output.Tables.Remove("Media");
277 this.Output.Tables.Remove("File");
278 this.Output.Tables.Remove("MsiFileHash");
279 this.Output.Tables.Remove("MsiAssemblyName");
280 }
281 }
282
283 /// <summary>
284 /// Adds the PatchFiles action to the sequence table if it does not already exist.
285 /// </summary>
286 /// <param name="table">The sequence table to check or modify.</param>
287 /// <param name="mainTransform">The primary authoring transform.</param>
288 /// <param name="pairedTransform">The secondary patch transform.</param>
289 /// <param name="mainFileRow">The file row that contains information about the patched file.</param>
290 private void AddPatchFilesActionToSequenceTable(SequenceTable table, WindowsInstallerData mainTransform, WindowsInstallerData pairedTransform, Row mainFileRow)
291 {
292 var tableName = table.ToString();
293
294 // Find/add PatchFiles action (also determine sequence for it).
295 // Search mainTransform first, then pairedTransform (pairedTransform overrides).
296 var hasPatchFilesAction = false;
297 var installFilesSequence = 0;
298 var duplicateFilesSequence = 0;
299
300 TestSequenceTableForPatchFilesAction(
301 mainTransform.Tables[tableName],
302 ref hasPatchFilesAction,
303 ref installFilesSequence,
304 ref duplicateFilesSequence);
305 TestSequenceTableForPatchFilesAction(
306 pairedTransform.Tables[tableName],
307 ref hasPatchFilesAction,
308 ref installFilesSequence,
309 ref duplicateFilesSequence);
310 if (!hasPatchFilesAction)
311 {
312 WindowsInstallerStandard.TryGetStandardAction(tableName, "PatchFiles", out var patchFilesActionTuple);
313
314 var sequence = patchFilesActionTuple.Sequence;
315
316 // Test for default sequence value's appropriateness
317 if (installFilesSequence >= sequence || (0 != duplicateFilesSequence && duplicateFilesSequence <= sequence))
318 {
319 if (0 != duplicateFilesSequence)
320 {
321 if (duplicateFilesSequence < installFilesSequence)
322 {
323 throw new WixException(ErrorMessages.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
324 }
325 else
326 {
327 sequence = (duplicateFilesSequence + installFilesSequence) / 2;
328 if (installFilesSequence == sequence || duplicateFilesSequence == sequence)
329 {
330 throw new WixException(ErrorMessages.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
331 }
332 }
333 }
334 else
335 {
336 sequence = installFilesSequence + 1;
337 }
338 }
339
340 var sequenceTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]);
341 if (0 == sequenceTable.Rows.Count)
342 {
343 sequenceTable.Operation = TableOperation.Add;
344 }
345
346 var patchAction = sequenceTable.CreateRow(null);
347 patchAction[0] = patchFilesActionTuple.Action;
348 patchAction[1] = patchFilesActionTuple.Condition;
349 patchAction[2] = sequence;
350 patchAction.Operation = RowOperation.Add;
351 }
352 }
353
354 /// <summary>
355 /// Tests sequence table for PatchFiles and associated actions
356 /// </summary>
357 /// <param name="sequenceTable">The table to test.</param>
358 /// <param name="hasPatchFilesAction">Set to true if PatchFiles action is found. Left unchanged otherwise.</param>
359 /// <param name="installFilesSequence">Set to sequence value of InstallFiles action if found. Left unchanged otherwise.</param>
360 /// <param name="duplicateFilesSequence">Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise.</param>
361 private static void TestSequenceTableForPatchFilesAction(Table sequenceTable, ref bool hasPatchFilesAction, ref int installFilesSequence, ref int duplicateFilesSequence)
362 {
363 if (null != sequenceTable)
364 {
365 foreach (var row in sequenceTable.Rows)
366 {
367 var actionName = row.FieldAsString(0);
368 switch (actionName)
369 {
370 case "PatchFiles":
371 hasPatchFilesAction = true;
372 break;
373
374 case "InstallFiles":
375 installFilesSequence = row.FieldAsInteger(2);
376 break;
377
378 case "DuplicateFiles":
379 duplicateFilesSequence = row.FieldAsInteger(2);
380 break;
381 }
382 }
383 }
384 }
385
386 /// <summary>
387 /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component.
388 /// </summary>
389 /// <param name="output">The output to validate.</param>
390 private void ValidateFileRowChanges(WindowsInstallerData transform)
391 {
392 var componentTable = transform.Tables["Component"];
393 var fileTable = transform.Tables["File"];
394
395 // There's no sense validating keypaths if the transform has no component or file table
396 if (componentTable == null || fileTable == null)
397 {
398 return;
399 }
400
401 var componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count);
402
403 // Index the Component table for non-directory & non-registry key paths.
404 foreach (var row in componentTable.Rows)
405 {
406 var keyPath = row.FieldAsString(5);
407 if (keyPath != null && 0 != (row.FieldAsInteger(3) & WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath))
408 {
409 componentKeyPath.Add(row.FieldAsString(0), keyPath);
410 }
411 }
412
413 var componentWithChangedKeyPath = new Dictionary<string, string>();
414 var componentWithNonKeyPathChanged = new Dictionary<string, string>();
415 // Verify changes in the file table, now that file diffing has occurred
416 foreach (FileRow row in fileTable.Rows)
417 {
418 if (RowOperation.Modify != row.Operation)
419 {
420 continue;
421 }
422
423 var fileId = row.FieldAsString(0);
424 var componentId = row.FieldAsString(1);
425
426 // If this file is the keypath of a component
427 if (componentKeyPath.ContainsValue(fileId))
428 {
429 if (!componentWithChangedKeyPath.ContainsKey(componentId))
430 {
431 componentWithChangedKeyPath.Add(componentId, fileId);
432 }
433 }
434 else
435 {
436 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
437 {
438 componentWithNonKeyPathChanged.Add(componentId, fileId);
439 }
440 }
441 }
442
443 foreach (var componentFile in componentWithNonKeyPathChanged)
444 {
445 // Make sure all changes to non keypath files also had a change in the keypath.
446 if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.TryGetValue(componentFile.Key, out var keyPath))
447 {
448 this.Messaging.Write(WarningMessages.UpdateOfNonKeyPathFile(componentFile.Value, componentFile.Key, keyPath));
449 }
450 }
451 }
452 }
453}
diff --git a/src/WixToolset.Core/Bind/FileFacade.cs b/src/WixToolset.Core/Bind/FileFacade.cs
index 7bfdb9bb..f0ce14ca 100644
--- a/src/WixToolset.Core/Bind/FileFacade.cs
+++ b/src/WixToolset.Core/Bind/FileFacade.cs
@@ -15,18 +15,27 @@ namespace WixToolset.Core.Bind
15 { 15 {
16 this.FileTuple = file; 16 this.FileTuple = file;
17 this.AssemblyTuple = assembly; 17 this.AssemblyTuple = assembly;
18
19 this.Identifier = file.Id;
20 this.ComponentRef = file.ComponentRef;
18 } 21 }
19 22
20 public FileFacade(bool fromModule, FileTuple file) 23 public FileFacade(bool fromModule, FileTuple file)
21 { 24 {
22 this.FromModule = fromModule; 25 this.FromModule = fromModule;
23 this.FileTuple = file; 26 this.FileTuple = file;
27
28 this.Identifier = file.Id;
29 this.ComponentRef = file.ComponentRef;
24 } 30 }
25 31
26 internal FileFacade(FileRow row) 32 public FileFacade(FileRow row)
27 { 33 {
28 this.FromTransform = true; 34 this.FromTransform = true;
29 this.FileRow = row; 35 this.FileRow = row;
36
37 this.Identifier = new Identifier(AccessModifier.Private, row.File);
38 this.ComponentRef = row.Component;
30 } 39 }
31 40
32 public bool FromModule { get; } 41 public bool FromModule { get; }
@@ -39,11 +48,11 @@ namespace WixToolset.Core.Bind
39 48
40 private AssemblyTuple AssemblyTuple { get; } 49 private AssemblyTuple AssemblyTuple { get; }
41 50
42 public string Id => this.FileRow == null ? this.FileTuple.Id.Id : this.FileRow.File; 51 public string Id => this.Identifier.Id;
43 52
44 public Identifier Identifier => this.FileRow == null ? this.FileTuple.Id : throw new NotImplementedException(); 53 public Identifier Identifier { get; }
45 54
46 public string ComponentRef => this.FileRow == null ? this.FileTuple.ComponentRef : this.FileRow.Component; 55 public string ComponentRef { get; }
47 56
48 public int DiskId 57 public int DiskId
49 { 58 {
@@ -137,7 +146,7 @@ namespace WixToolset.Core.Bind
137 } 146 }
138 } 147 }
139 148
140 public AssemblyType? AssemblyType => this.FileRow == null ? this.AssemblyTuple?.Type : throw new NotImplementedException(); 149 public AssemblyType? AssemblyType => this.FileRow == null ? this.AssemblyTuple?.Type : null;
141 150
142 public string AssemblyApplicationFileRef => this.FileRow == null ? this.AssemblyTuple?.ApplicationFileRef : throw new NotImplementedException(); 151 public string AssemblyApplicationFileRef => this.FileRow == null ? this.AssemblyTuple?.ApplicationFileRef : throw new NotImplementedException();
143 152
@@ -153,5 +162,10 @@ namespace WixToolset.Core.Bind
153 /// Gets or sets the MsiFileHash row for this file. 162 /// Gets or sets the MsiFileHash row for this file.
154 /// </summary> 163 /// </summary>
155 public MsiFileHashTuple Hash { get; set; } 164 public MsiFileHashTuple Hash { get; set; }
165
166 /// <summary>
167 /// Allows direct access to the underlying FileRow as requried for patching.
168 /// </summary>
169 public FileRow GetFileRow() => this.FileRow ?? throw new NotImplementedException();
156 } 170 }
157} 171}
diff --git a/src/WixToolset.Core/CommandLine/BuildCommand.cs b/src/WixToolset.Core/CommandLine/BuildCommand.cs
index 80003392..8392131f 100644
--- a/src/WixToolset.Core/CommandLine/BuildCommand.cs
+++ b/src/WixToolset.Core/CommandLine/BuildCommand.cs
@@ -334,6 +334,7 @@ namespace WixToolset.Core.CommandLine
334 context.DelayedFields = resolveResult.DelayedFields; 334 context.DelayedFields = resolveResult.DelayedFields;
335 context.ExpectedEmbeddedFiles = resolveResult.ExpectedEmbeddedFiles; 335 context.ExpectedEmbeddedFiles = resolveResult.ExpectedEmbeddedFiles;
336 context.Extensions = this.ExtensionManager.GetServices<IBinderExtension>(); 336 context.Extensions = this.ExtensionManager.GetServices<IBinderExtension>();
337 context.FileSystemExtensions = this.ExtensionManager.GetServices<IFileSystemExtension>();
337 context.Ices = Array.Empty<string>(); // TODO: set this correctly 338 context.Ices = Array.Empty<string>(); // TODO: set this correctly
338 context.IntermediateFolder = intermediateFolder; 339 context.IntermediateFolder = intermediateFolder;
339 context.IntermediateRepresentation = resolveResult.IntermediateRepresentation; 340 context.IntermediateRepresentation = resolveResult.IntermediateRepresentation;
diff --git a/src/test/WixToolsetTest.CoreIntegration/PatchFixture.cs b/src/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
index 584f86fe..3616bcab 100644
--- a/src/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
+++ b/src/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
@@ -16,7 +16,7 @@ namespace WixToolsetTest.CoreIntegration
16 { 16 {
17 private static readonly XNamespace PatchNamespace = "http://www.microsoft.com/msi/patch_applicability.xsd"; 17 private static readonly XNamespace PatchNamespace = "http://www.microsoft.com/msi/patch_applicability.xsd";
18 18
19 [Fact(Skip = "Skip until patches have files in them")] 19 [Fact]
20 public void CanBuildSimplePatch() 20 public void CanBuildSimplePatch()
21 { 21 {
22 var folder = TestData.Get(@"TestData\PatchSingle"); 22 var folder = TestData.Get(@"TestData\PatchSingle");
@@ -45,7 +45,7 @@ namespace WixToolsetTest.CoreIntegration
45 Assert.True(File.Exists(cab)); 45 Assert.True(File.Exists(cab));
46 46
47 var files = Query.GetCabinetFiles(cab); 47 var files = Query.GetCabinetFiles(cab);
48 Assert.Equal(new[] { "a", "b" }, files.Select(f => f.ArchiveName).ToArray()); // This test may not be quite correct, yet. 48 Assert.Equal(new[] { "a.txt", "b.txt" }, files.Select(f => f.Name).ToArray());
49 } 49 }
50 } 50 }
51 51