aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs630
1 files changed, 0 insertions, 630 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs
deleted file mode 100644
index 8a85a975..00000000
--- a/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs
+++ /dev/null
@@ -1,630 +0,0 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3#if DELETE
4
5namespace WixToolset.Core.WindowsInstaller.Bind
6{
7 using System;
8 using System.Collections.Generic;
9 using System.Diagnostics;
10 using System.IO;
11 using System.Linq;
12 using WixToolset.Core.Bind;
13 using WixToolset.Data;
14 using WixToolset.Data.Symbols;
15 using WixToolset.Data.WindowsInstaller;
16 using WixToolset.Data.WindowsInstaller.Rows;
17 using WixToolset.Extensibility;
18 using WixToolset.Extensibility.Services;
19
20 internal class CopyTransformDataCommand
21 {
22 public CopyTransformDataCommand(IMessaging messaging, WindowsInstallerData output, TableDefinitionCollection tableDefinitions, bool copyOutFileRows)
23 {
24 this.Messaging = messaging;
25 this.Output = output;
26 this.TableDefinitions = tableDefinitions;
27 this.CopyOutFileRows = copyOutFileRows;
28 }
29
30 private bool CopyOutFileRows { get; }
31
32 public IEnumerable<IFileSystemExtension> Extensions { get; }
33
34 private IMessaging Messaging { get; }
35
36 private WindowsInstallerData Output { get; }
37
38 private TableDefinitionCollection TableDefinitions { get; }
39
40 public IEnumerable<FileFacade> FileFacades { get; private set; }
41
42 public void Execute()
43 {
44 Debug.Assert(OutputType.Patch != this.Output.Type);
45
46 var allFileRows = this.CopyOutFileRows ? new List<FileFacade>() : null;
47
48 var copyToPatch = (allFileRows != null);
49 var copyFromPatch = !copyToPatch;
50
51 var patchMediaRows = new RowDictionary<MediaRow>();
52
53 var patchMediaFileRows = new Dictionary<int, RowDictionary<WixFileRow>>();
54
55 var patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]);
56 var patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]);
57
58 if (copyFromPatch)
59 {
60 // index patch files by diskId+fileId
61 foreach (WixFileRow patchFileRow in patchFileTable.Rows)
62 {
63 int diskId = patchFileRow.DiskId;
64 if (!patchMediaFileRows.TryGetValue(diskId, out var mediaFileRows))
65 {
66 mediaFileRows = new RowDictionary<WixFileRow>();
67 patchMediaFileRows.Add(diskId, mediaFileRows);
68 }
69
70 mediaFileRows.Add(patchFileRow);
71 }
72
73 var patchMediaTable = this.Output.EnsureTable(this.TableDefinitions["Media"]);
74 patchMediaRows = new RowDictionary<MediaRow>(patchMediaTable);
75 }
76
77 // Index paired transforms by name without the "#" prefix.
78 var pairedTransforms = this.Output.SubStorages.Where(s => s.Name.StartsWith("#")).ToDictionary(s => s.Name.Substring(1), s => s.Data);
79 //Dictionary<string, Output> pairedTransforms = new Dictionary<string, Output>();
80 //foreach (SubStorage substorage in this.Output.SubStorages)
81 //{
82 // if (substorage.Name.StartsWith("#"))
83 // {
84 // pairedTransforms.Add(substorage.Name.Substring(1), substorage.Data);
85 // }
86 //}
87
88 try
89 {
90 // Copy File bind data into substorages
91 foreach (var substorage in this.Output.SubStorages)
92 {
93 if (substorage.Name.StartsWith("#"))
94 {
95 // no changes necessary for paired transforms
96 continue;
97 }
98
99 var mainTransform = substorage.Data;
100 var mainWixFileTable = mainTransform.Tables["WixFile"];
101 var mainMsiFileHashTable = mainTransform.Tables["MsiFileHash"];
102
103 this.FileManagerCore.ActiveSubStorage = substorage;
104
105 var mainWixFiles = new RowDictionary<WixFileRow>(mainWixFileTable);
106 var mainMsiFileHashIndex = new RowDictionary<Row>();
107
108 var mainFileTable = mainTransform.Tables["File"];
109 var pairedTransform = pairedTransforms[substorage.Name];
110
111 // copy Media.LastSequence and index the MsiFileHash table if it exists.
112 if (copyFromPatch)
113 {
114 var pairedMediaTable = pairedTransform.Tables["Media"];
115 foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows)
116 {
117 var patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId);
118 pairedMediaRow.Fields[1] = patchMediaRow.Fields[1];
119 }
120
121 if (null != mainMsiFileHashTable)
122 {
123 mainMsiFileHashIndex = new RowDictionary<Row>(mainMsiFileHashTable);
124 }
125
126 // Validate file row changes for keypath-related issues
127 this.ValidateFileRowChanges(mainTransform);
128 }
129
130 // Index File table of pairedTransform
131 var pairedFileTable = pairedTransform.Tables["File"];
132 var pairedFileRows = new RowDictionary<FileRow>(pairedFileTable);
133
134 if (null != mainFileTable)
135 {
136 if (copyFromPatch)
137 {
138 // Remove the MsiFileHash table because it will be updated later with the final file hash for each file
139 mainTransform.Tables.Remove("MsiFileHash");
140 }
141
142 foreach (FileRow mainFileRow in mainFileTable.Rows)
143 {
144 if (RowOperation.Delete == mainFileRow.Operation)
145 {
146 continue;
147 }
148 else if (RowOperation.None == mainFileRow.Operation && !copyToPatch)
149 {
150 continue;
151 }
152
153 var mainWixFileRow = mainWixFiles.Get(mainFileRow.File);
154
155 if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes.
156 {
157 var objectField = (ObjectField)mainWixFileRow.Fields[6];
158 var pairedFileRow = pairedFileRows.Get(mainFileRow.File);
159
160 // If the file is new, we always need to add it to the patch.
161 if (mainFileRow.Operation != RowOperation.Add)
162 {
163 // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare.
164 if (null == objectField.PreviousData)
165 {
166 if (mainFileRow.Operation == RowOperation.None)
167 {
168 continue;
169 }
170 }
171 else
172 {
173 // TODO: should this entire condition be placed in the binder file manager?
174 if ((0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) &&
175 !this.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString()))
176 {
177 // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified.
178 mainFileRow.Operation = RowOperation.Modify;
179 if (null != pairedFileRow)
180 {
181 // Always patch-added, but never non-compressed.
182 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
183 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
184 pairedFileRow.Fields[6].Modified = true;
185 pairedFileRow.Operation = RowOperation.Modify;
186 }
187 }
188 else
189 {
190 // The File is same. We need mark all the attributes as unchanged.
191 mainFileRow.Operation = RowOperation.None;
192 foreach (var field in mainFileRow.Fields)
193 {
194 field.Modified = false;
195 }
196
197 if (null != pairedFileRow)
198 {
199 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
200 pairedFileRow.Fields[6].Modified = false;
201 pairedFileRow.Operation = RowOperation.None;
202 }
203 continue;
204 }
205 }
206 }
207 else if (null != pairedFileRow) // RowOperation.Add
208 {
209 // Always patch-added, but never non-compressed.
210 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
211 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
212 pairedFileRow.Fields[6].Modified = true;
213 pairedFileRow.Operation = RowOperation.Add;
214 }
215 }
216
217 // index patch files by diskId+fileId
218 int diskId = mainWixFileRow.DiskId;
219
220 if (!patchMediaFileRows.TryGetValue(diskId, out var mediaFileRows))
221 {
222 mediaFileRows = new RowDictionary<WixFileRow>();
223 patchMediaFileRows.Add(diskId, mediaFileRows);
224 }
225
226 var fileId = mainFileRow.File;
227 var patchFileRow = mediaFileRows.Get(fileId);
228 if (copyToPatch)
229 {
230 if (null == patchFileRow)
231 {
232 var patchActualFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
233 patchActualFileRow.CopyFrom(mainFileRow);
234
235 patchFileRow = (WixFileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
236 patchFileRow.CopyFrom(mainWixFileRow);
237
238 mediaFileRows.Add(patchFileRow);
239
240 allFileRows.Add(new FileFacade(patchActualFileRow, patchFileRow, null)); // TODO: should we be passing along delta information? Probably, right?
241 }
242 else
243 {
244 // TODO: confirm the rest of data is identical?
245
246 // make sure Source is same. Otherwise we are silently ignoring a file.
247 if (0 != String.Compare(patchFileRow.Source, mainWixFileRow.Source, StringComparison.OrdinalIgnoreCase))
248 {
249 this.Messaging.Write(ErrorMessages.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source));
250 }
251
252 // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow.
253 patchFileRow.AppendPreviousDataFrom(mainWixFileRow);
254 }
255 }
256 else
257 {
258 // copy data from the patch back to the transform
259 if (null != patchFileRow)
260 {
261 var pairedFileRow = pairedFileRows.Get(fileId);
262 for (var i = 0; i < patchFileRow.Fields.Length; i++)
263 {
264 var patchValue = patchFileRow[i] == null ? String.Empty : patchFileRow.FieldAsString(i);
265 var mainValue = mainFileRow[i] == null ? String.Empty : mainFileRow.FieldAsString(i);
266
267 if (1 == i)
268 {
269 // File.Component_ changes should not come from the shared file rows
270 // that contain the file information as each individual transform might
271 // have different changes (or no changes at all).
272 }
273 // File.Attributes should not changed for binary deltas
274 else if (6 == i)
275 {
276 if (null != patchFileRow.Patch)
277 {
278 // File.Attribute should not change for binary deltas
279 pairedFileRow.Attributes = mainFileRow.Attributes;
280 mainFileRow.Fields[i].Modified = false;
281 }
282 }
283 // File.Sequence is updated in pairedTransform, not mainTransform
284 else if (7 == i)
285 {
286 // file sequence is updated in Patch table instead of File table for delta patches
287 if (null != patchFileRow.Patch)
288 {
289 pairedFileRow.Fields[i].Modified = false;
290 }
291 else
292 {
293 pairedFileRow[i] = patchFileRow[i];
294 pairedFileRow.Fields[i].Modified = true;
295 }
296 mainFileRow.Fields[i].Modified = false;
297 }
298 else if (patchValue != mainValue)
299 {
300 mainFileRow[i] = patchFileRow[i];
301 mainFileRow.Fields[i].Modified = true;
302 if (mainFileRow.Operation == RowOperation.None)
303 {
304 mainFileRow.Operation = RowOperation.Modify;
305 }
306 }
307 }
308
309 // copy MsiFileHash row for this File
310 if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out var patchHashRow))
311 {
312 patchHashRow = patchFileRow.Hash;
313 }
314
315 if (null != patchHashRow)
316 {
317 var mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]);
318 var mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers);
319 for (var i = 0; i < patchHashRow.Fields.Length; i++)
320 {
321 mainHashRow[i] = patchHashRow[i];
322 if (i > 1)
323 {
324 // assume all hash fields have been modified
325 mainHashRow.Fields[i].Modified = true;
326 }
327 }
328
329 // assume the MsiFileHash operation follows the File one
330 mainHashRow.Operation = mainFileRow.Operation;
331 }
332
333 // copy MsiAssemblyName rows for this File
334 List<Row> patchAssemblyNameRows = patchFileRow.AssemblyNames;
335 if (null != patchAssemblyNameRows)
336 {
337 var mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
338 foreach (var patchAssemblyNameRow in patchAssemblyNameRows)
339 {
340 // Copy if there isn't an identical modified/added row already in the transform.
341 var foundMatchingModifiedRow = false;
342 foreach (var mainAssemblyNameRow in mainAssemblyNameTable.Rows)
343 {
344 if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/')))
345 {
346 foundMatchingModifiedRow = true;
347 break;
348 }
349 }
350
351 if (!foundMatchingModifiedRow)
352 {
353 var mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers);
354 for (var i = 0; i < patchAssemblyNameRow.Fields.Length; i++)
355 {
356 mainAssemblyNameRow[i] = patchAssemblyNameRow[i];
357 }
358
359 // assume value field has been modified
360 mainAssemblyNameRow.Fields[2].Modified = true;
361 mainAssemblyNameRow.Operation = mainFileRow.Operation;
362 }
363 }
364 }
365
366 // Add patch header for this file
367 if (null != patchFileRow.Patch)
368 {
369 // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables.
370 this.AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow);
371 this.AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow);
372
373 // Add to Patch table
374 var patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]);
375 if (0 == patchTable.Rows.Count)
376 {
377 patchTable.Operation = TableOperation.Add;
378 }
379
380 var patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers);
381 patchRow[0] = patchFileRow.File;
382 patchRow[1] = patchFileRow.Sequence;
383
384 var patchFile = new FileInfo(patchFileRow.Source);
385 patchRow[2] = (int)patchFile.Length;
386 patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1;
387
388 var streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1];
389 if (Msi.MsiInterop.MsiMaxStreamNameLength < streamName.Length)
390 {
391 streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_');
392
393 var patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]);
394 if (0 == patchHeadersTable.Rows.Count)
395 {
396 patchHeadersTable.Operation = TableOperation.Add;
397 }
398
399 var patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers);
400 patchHeadersRow[0] = streamName;
401 patchHeadersRow[1] = patchFileRow.Patch;
402 patchRow[5] = streamName;
403 patchHeadersRow.Operation = RowOperation.Add;
404 }
405 else
406 {
407 patchRow[4] = patchFileRow.Patch;
408 }
409 patchRow.Operation = RowOperation.Add;
410 }
411 }
412 else
413 {
414 // TODO: throw because all transform rows should have made it into the patch
415 }
416 }
417 }
418 }
419
420 if (copyFromPatch)
421 {
422 this.Output.Tables.Remove("Media");
423 this.Output.Tables.Remove("File");
424 this.Output.Tables.Remove("MsiFileHash");
425 this.Output.Tables.Remove("MsiAssemblyName");
426 }
427 }
428 }
429 finally
430 {
431 this.FileManagerCore.ActiveSubStorage = null;
432 }
433
434 this.FileFacades = allFileRows;
435 }
436
437 /// <summary>
438 /// Adds the PatchFiles action to the sequence table if it does not already exist.
439 /// </summary>
440 /// <param name="table">The sequence table to check or modify.</param>
441 /// <param name="mainTransform">The primary authoring transform.</param>
442 /// <param name="pairedTransform">The secondary patch transform.</param>
443 /// <param name="mainFileRow">The file row that contains information about the patched file.</param>
444 private void AddPatchFilesActionToSequenceTable(SequenceTable table, WindowsInstallerData mainTransform, WindowsInstallerData pairedTransform, Row mainFileRow)
445 {
446 var tableName = table.ToString();
447
448 // Find/add PatchFiles action (also determine sequence for it).
449 // Search mainTransform first, then pairedTransform (pairedTransform overrides).
450 var hasPatchFilesAction = false;
451 var installFilesSequence = 0;
452 var duplicateFilesSequence = 0;
453
454 TestSequenceTableForPatchFilesAction(
455 mainTransform.Tables[tableName],
456 ref hasPatchFilesAction,
457 ref installFilesSequence,
458 ref duplicateFilesSequence);
459 TestSequenceTableForPatchFilesAction(
460 pairedTransform.Tables[tableName],
461 ref hasPatchFilesAction,
462 ref installFilesSequence,
463 ref duplicateFilesSequence);
464 if (!hasPatchFilesAction)
465 {
466 WindowsInstallerStandard.TryGetStandardAction(tableName, "PatchFiles", out var patchFilesActionSymbol);
467
468 var sequence = patchFilesActionSymbol.Sequence;
469
470 // Test for default sequence value's appropriateness
471 if (installFilesSequence >= sequence || (0 != duplicateFilesSequence && duplicateFilesSequence <= sequence))
472 {
473 if (0 != duplicateFilesSequence)
474 {
475 if (duplicateFilesSequence < installFilesSequence)
476 {
477 throw new WixException(ErrorMessages.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionSymbol.Action));
478 }
479 else
480 {
481 sequence = (duplicateFilesSequence + installFilesSequence) / 2;
482 if (installFilesSequence == sequence || duplicateFilesSequence == sequence)
483 {
484 throw new WixException(ErrorMessages.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionSymbol.Action));
485 }
486 }
487 }
488 else
489 {
490 sequence = installFilesSequence + 1;
491 }
492 }
493
494 var sequenceTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]);
495 if (0 == sequenceTable.Rows.Count)
496 {
497 sequenceTable.Operation = TableOperation.Add;
498 }
499
500 var patchAction = sequenceTable.CreateRow(null);
501 patchAction[0] = patchFilesActionSymbol.Action;
502 patchAction[1] = patchFilesActionSymbol.Condition;
503 patchAction[2] = sequence;
504 patchAction.Operation = RowOperation.Add;
505 }
506 }
507
508 /// <summary>
509 /// Tests sequence table for PatchFiles and associated actions
510 /// </summary>
511 /// <param name="sequenceTable">The table to test.</param>
512 /// <param name="hasPatchFilesAction">Set to true if PatchFiles action is found. Left unchanged otherwise.</param>
513 /// <param name="installFilesSequence">Set to sequence value of InstallFiles action if found. Left unchanged otherwise.</param>
514 /// <param name="duplicateFilesSequence">Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise.</param>
515 private static void TestSequenceTableForPatchFilesAction(Table sequenceTable, ref bool hasPatchFilesAction, ref int installFilesSequence, ref int duplicateFilesSequence)
516 {
517 if (null != sequenceTable)
518 {
519 foreach (var row in sequenceTable.Rows)
520 {
521 var actionName = row.FieldAsString(0);
522 switch (actionName)
523 {
524 case "PatchFiles":
525 hasPatchFilesAction = true;
526 break;
527
528 case "InstallFiles":
529 installFilesSequence = row.FieldAsInteger(2);
530 break;
531
532 case "DuplicateFiles":
533 duplicateFilesSequence = row.FieldAsInteger(2);
534 break;
535 }
536 }
537 }
538 }
539
540 /// <summary>
541 /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component.
542 /// </summary>
543 /// <param name="output">The output to validate.</param>
544 private void ValidateFileRowChanges(WindowsInstallerData transform)
545 {
546 var componentTable = transform.Tables["Component"];
547 var fileTable = transform.Tables["File"];
548
549 // There's no sense validating keypaths if the transform has no component or file table
550 if (componentTable == null || fileTable == null)
551 {
552 return;
553 }
554
555 var componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count);
556
557 // Index the Component table for non-directory & non-registry key paths.
558 foreach (var row in componentTable.Rows)
559 {
560 var keyPath = row.FieldAsString(5);
561 if (keyPath != null && 0 != (row.FieldAsInteger(3) & WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath))
562 {
563 componentKeyPath.Add(row.FieldAsString(0), keyPath);
564 }
565 }
566
567 var componentWithChangedKeyPath = new Dictionary<string, string>();
568 var componentWithNonKeyPathChanged = new Dictionary<string, string>();
569 // Verify changes in the file table, now that file diffing has occurred
570 foreach (FileRow row in fileTable.Rows)
571 {
572 if (RowOperation.Modify != row.Operation)
573 {
574 continue;
575 }
576
577 var fileId = row.FieldAsString(0);
578 var componentId = row.FieldAsString(1);
579
580 // If this file is the keypath of a component
581 if (componentKeyPath.ContainsValue(fileId))
582 {
583 if (!componentWithChangedKeyPath.ContainsKey(componentId))
584 {
585 componentWithChangedKeyPath.Add(componentId, fileId);
586 }
587 }
588 else
589 {
590 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
591 {
592 componentWithNonKeyPathChanged.Add(componentId, fileId);
593 }
594 }
595 }
596
597 foreach (var componentFile in componentWithNonKeyPathChanged)
598 {
599 // Make sure all changes to non keypath files also had a change in the keypath.
600 if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.TryGetValue(componentFile.Key, out var keyPath))
601 {
602 this.Messaging.Write(WarningMessages.UpdateOfNonKeyPathFile(componentFile.Value, componentFile.Key, keyPath));
603 }
604 }
605 }
606
607 private bool CompareFiles(string targetFile, string updatedFile)
608 {
609 bool? compared = null;
610 foreach (var extension in this.Extensions)
611 {
612 compared = extension.CompareFiles(targetFile, updatedFile);
613
614 if (compared.HasValue)
615 {
616 break;
617 }
618 }
619
620 if (!compared.HasValue)
621 {
622 throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result.
623 }
624
625 return compared.Value;
626 }
627 }
628}
629
630#endif