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