From 679e0e5cbbbc2a2406d968f478359504923da880 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 22 Aug 2026 12:51:58 +1000 Subject: [PATCH] Match extensions case insensitively ExtensionLookup, PathLookup and BinaryExtensions all compared ordinally, while every registration is lowercase. The file systems that produce these strings are not case sensitive on Windows or macOS, so .PNG, .JPG and .Docx matched nothing: the tool resolved for foo.png and not for foo.PNG, and the same file could get a diff tool or not depending on how it happened to be named. BinaryExtensions decides whether a pair is treated as binary, so the same mismatch also picks the wrong argument builder for a tool that has both. Viewer/ImageExtensions already uses OrdinalIgnoreCase for exactly this kind of lookup, so this makes the two agree rather than introducing a new rule. --- src/DiffEngine.Tests/ExtensionCaseTests.cs | 39 ++++++++++++++++++++++ src/DiffEngine/DiffTools.cs | 8 +++-- src/DiffEngine/ResolvedTool.cs | 4 ++- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/DiffEngine.Tests/ExtensionCaseTests.cs diff --git a/src/DiffEngine.Tests/ExtensionCaseTests.cs b/src/DiffEngine.Tests/ExtensionCaseTests.cs new file mode 100644 index 00000000..38ed8808 --- /dev/null +++ b/src/DiffEngine.Tests/ExtensionCaseTests.cs @@ -0,0 +1,39 @@ +/// +/// Extensions are matched the way file systems produce them. +/// +/// ExtensionLookup, PathLookup and BinaryExtensions all compared ordinally while every +/// registration is lowercase, so .PNG, .JPG and .Docx - which is exactly what Windows and macOS +/// hand back - matched nothing. The tool resolved for foo.png and not for foo.PNG. +/// Viewer/ImageExtensions already uses OrdinalIgnoreCase for the same kind of lookup. +/// +/// +[NotInParallel] +public class ExtensionCaseTests +{ + [Test] + public async Task ExtensionsResolveWhateverTheirCasing() + { + var extension = $".zz{Guid.NewGuid():N}"; + var tool = DiffTools.AddTool( + name: $"CaseProbe{Guid.NewGuid():N}", + autoRefresh: false, + isMdi: false, + supportsText: false, + requiresTarget: true, + useShellExecute: false, + launchArguments: new( + Left: (temp, target) => $"\"{temp}\" \"{target}\"", + Right: (temp, target) => $"\"{target}\" \"{temp}\""), + exePath: Environment.ProcessPath!, + binaryExtensions: [extension]); + + await Assert.That(tool).IsNotNull(); + + await Assert.That(DiffTools.TryFindByExtension(extension, out _)).IsTrue(); + await Assert.That(DiffTools.TryFindByExtension(extension.ToUpperInvariant(), out _)).IsTrue(); + + await Assert.That(DiffTools.TryFindForInputFilePath($"file{extension.ToUpperInvariant()}", out _)).IsTrue(); + + await Assert.That(tool!.BinaryExtensions.Contains(extension.ToUpperInvariant())).IsTrue(); + } +} diff --git a/src/DiffEngine/DiffTools.cs b/src/DiffEngine/DiffTools.cs index 103cbf35..aeb3fb06 100644 --- a/src/DiffEngine/DiffTools.cs +++ b/src/DiffEngine/DiffTools.cs @@ -2,8 +2,12 @@ public static partial class DiffTools { - static Dictionary ExtensionLookup = []; - static Dictionary PathLookup = []; + // Case insensitive, because the keys are file extensions and executable paths and the file + // systems that produce them are. An ordinal lookup meant .PNG, .JPG and .Docx - which is what + // Windows and macOS hand back - matched none of the lowercase registrations, so the tool + // resolved for foo.png and not for foo.PNG. Viewer/ImageExtensions already does this + static Dictionary ExtensionLookup = new(StringComparer.OrdinalIgnoreCase); + static Dictionary PathLookup = new(StringComparer.OrdinalIgnoreCase); static Dictionary ToolLookup = []; static ResolvedTool? firstTextTool; static List resolved = []; diff --git a/src/DiffEngine/ResolvedTool.cs b/src/DiffEngine/ResolvedTool.cs index d94e547b..24f1f4de 100644 --- a/src/DiffEngine/ResolvedTool.cs +++ b/src/DiffEngine/ResolvedTool.cs @@ -62,7 +62,9 @@ Extensions must begin with a period. } } - BinaryExtensions = binaryExtensions.ToFrozenSet(); + // Case insensitive for the same reason ExtensionLookup is: a binary extension is compared + // against whatever casing the file system produced + BinaryExtensions = binaryExtensions.ToFrozenSet(StringComparer.OrdinalIgnoreCase); RequiresTarget = requiresTarget; SupportsText = supportsText;