aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2020-01-24 15:27:20 -0800
committerRob Mensching <rob@firegiant.com>2020-02-05 16:15:47 -0800
commit6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d (patch)
treec717333cd10d5592e59dfb898b391275bba1f389 /src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
parent6e2e67ab55c75f4655397588c0dcc64f50d22f92 (diff)
downloadwix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.gz
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.bz2
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.zip
Start on new patch infrastructure
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs585
1 files changed, 585 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
new file mode 100644
index 00000000..9818f01a
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesFromTransforms.cs
@@ -0,0 +1,585 @@
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;
16 using WixToolset.Extensibility.Services;
17
18 internal class GetFileFacadesFromTransforms
19 {
20 public GetFileFacadesFromTransforms(IMessaging messaging, IEnumerable<SubStorage> subStorages, TableDefinitionCollection tableDefinitions)
21 {
22 this.Messaging = messaging;
23 this.SubStorages = subStorages;
24 this.TableDefinitions = tableDefinitions;
25 }
26
27 public IEnumerable<IFileSystemExtension> Extensions { get; }
28
29 private IMessaging Messaging { get; }
30
31 public IEnumerable<SubStorage> SubStorages { get; }
32
33 private TableDefinitionCollection TableDefinitions { get; }
34
35 public IEnumerable<FileFacade> FileFacades { get; private set; }
36
37 public void Execute()
38 {
39 var allFileRows = new List<FileFacade>();
40 var copyToPatch = (allFileRows != null);
41#if TODO_PATCHING
42 var patchMediaRows = new RowDictionary<MediaRow>();
43
44 var patchMediaFileRows = new Dictionary<int, RowDictionary<WixFileRow>>();
45
46 var patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]);
47 var patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]);
48
49 if (copyFromPatch)
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
68 // 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);
70
71 foreach (var substorage in this.SubStorages)
72 {
73 if (substorage.Name.StartsWith("#"))
74 {
75 // Skip paired transforms.
76 continue;
77 }
78
79 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"];
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
105 if (null == mainFileTable)
106 {
107 continue;
108 }
109
110 // Index File table of pairedTransform
111 var pairedFileRows = new RowDictionary<FileRow>(pairedTransform.Tables["File"]);
112
113 foreach (FileRow mainFileRow in mainFileTable.Rows)
114 {
115 if (RowOperation.Delete == mainFileRow.Operation)
116 {
117 continue;
118 }
119 else if (RowOperation.None == mainFileRow.Operation)
120 {
121 continue;
122 }
123
124 var mainWixFileRow = mainWixFiles.Get(mainFileRow.File);
125
126 if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes.
127 {
128 var objectField = (ObjectField)mainWixFileRow.Fields[6];
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 {
180 // Always patch-added, but never non-compressed.
181 pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
182 pairedFileRow.Attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
183 pairedFileRow.Fields[6].Modified = true;
184 pairedFileRow.Operation = RowOperation.Add;
185 }
186 }
187
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 {
201 if (null == patchFileRow)
202 {
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 {
215 // TODO: confirm the rest of data is identical?
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 {
220 this.Messaging.Write(ErrorMessages.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source));
221 }
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 }
436 else
437 {
438 sequence = (duplicateFilesSequence + installFilesSequence) / 2;
439 if (installFilesSequence == sequence || duplicateFilesSequence == sequence)
440 {
441 throw new WixException(ErrorMessages.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
442 }
443 }
444 }
445 else
446 {
447 sequence = installFilesSequence + 1;
448 }
449 }
450
451 var sequenceTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]);
452 if (0 == sequenceTable.Rows.Count)
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
465 /// <summary>
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 {
481 case "PatchFiles":
482 hasPatchFilesAction = true;
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 }
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
512 var componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count);
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
524 var componentWithChangedKeyPath = new Dictionary<string, string>();
525 var componentWithNonKeyPathChanged = new Dictionary<string, string>();
526 // Verify changes in the file table, now that file diffing has occurred
527 foreach (FileRow row in fileTable.Rows)
528 {
529 if (RowOperation.Modify != row.Operation)
530 {
531 continue;
532 }
533
534 var fileId = row.FieldAsString(0);
535 var componentId = row.FieldAsString(1);
536
537 // If this file is the keypath of a component
538 if (componentKeyPath.ContainsValue(fileId))
539 {
540 if (!componentWithChangedKeyPath.ContainsKey(componentId))
541 {
542 componentWithChangedKeyPath.Add(componentId, fileId);
543 }
544 }
545 else
546 {
547 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
548 {
549 componentWithNonKeyPathChanged.Add(componentId, fileId);
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
564 private bool CompareFiles(string targetFile, string updatedFile)
565 {
566 bool? compared = null;
567 foreach (var extension in this.Extensions)
568 {
569 compared = extension.CompareFiles(targetFile, updatedFile);
570
571 if (compared.HasValue)
572 {
573 break;
574 }
575 }
576
577 if (!compared.HasValue)
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 }
584 }
585}