From be5cce35ef07fd27f91789082ddffd9379efcad5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 22 Aug 2026 17:36:13 +1000 Subject: [PATCH] Encode the issue title like the body The URL the tray opens after an error put the raw message in title= while encoding body=. The message carries a file path, so a '#' in one - a directory named C# will do it - started a fragment: the body went, and the title ended at the hash. An '&' started a parameter GitHub does not have, truncating the title there. Both halves are built in one place now, and the title goes through UrlEncode. The body still arrives already encoded from its callers, which is why it is appended rather than encoded again. --- .../IssueLauncherTests.cs | 49 +++++++++++++++++++ src/DiffEngineTray/IssueLauncher.cs | 16 ++++-- 2 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/DiffEngineTray.Tests/IssueLauncherTests.cs diff --git a/src/DiffEngineTray.Tests/IssueLauncherTests.cs b/src/DiffEngineTray.Tests/IssueLauncherTests.cs new file mode 100644 index 00000000..9268b899 --- /dev/null +++ b/src/DiffEngineTray.Tests/IssueLauncherTests.cs @@ -0,0 +1,49 @@ +/// +/// The issue URL the tray opens after an error. Its title is built from a message that carries a +/// file path, so it holds whatever characters the path does. +/// +public class IssueLauncherTests +{ + [Test] + public async Task A_hash_in_the_title_does_not_start_a_fragment() + { + // Perfectly ordinary in a solution directory, and everything after it used to be read by + // the browser as a fragment: no body, and a title ending at the hash + const string message = @"Cannot start. Failed to read settings: C:\code\C#\settings.json"; + + var url = IssueLauncher.BuildUrl(message); + + await Assert.That(url).DoesNotContain("#"); + await Assert.That(Query(url)["title"]).IsEqualTo(message); + await Assert.That(Query(url)["body"]).IsNotEmpty(); + } + + [Test] + public async Task An_ampersand_in_the_title_does_not_truncate_it() + { + const string message = @"Could not accept 'R&D.received.txt'"; + + var url = IssueLauncher.BuildUrl(message); + + await Assert.That(Query(url)["title"]).IsEqualTo(message); + await Assert.That(Query(url)["body"]).IsNotEmpty(); + } + + /// + /// The body is encoded by its callers and handed over already escaped, so encoding the title + /// must not have changed what reaches GitHub as the body. + /// + [Test] + public async Task Keeps_the_body_it_is_given() + { + var url = IssueLauncher.BuildUrl("TheTitle", WebUtility.UrlEncode("\n * Action: TheAction")); + + await Assert.That(Query(url)["body"]).Contains("* Action: TheAction"); + } + + static Dictionary Query(string url) => + url[(url.IndexOf('?') + 1)..] + .Split('&') + .Select(_ => _.Split('=', 2)) + .ToDictionary(_ => _[0], _ => WebUtility.UrlDecode(_[1])); +} diff --git a/src/DiffEngineTray/IssueLauncher.cs b/src/DiffEngineTray/IssueLauncher.cs index f09bbb10..69ca628a 100644 --- a/src/DiffEngineTray/IssueLauncher.cs +++ b/src/DiffEngineTray/IssueLauncher.cs @@ -11,7 +11,15 @@ static IssueLauncher() => """); public static void Launch() => - LinkLauncher.LaunchUrl($"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/VerifyTests/DiffEngine/issues/new?title=TODO&body={defaultBody}"); + LinkLauncher.LaunchUrl(BuildUrl("TODO")); + + /// + /// The title is encoded like the body is. It is built from a message carrying a file path, and + /// a '#' in one started a fragment - dropping the body, and everything of the title after it - + /// while an '&' started a parameter GitHub does not have, truncating the title there. + /// + internal static string BuildUrl(string title, string extraBody = "") => + $"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/VerifyTests/DiffEngine/issues/new?title={WebUtility.UrlEncode(title)}&body={defaultBody}{extraBody}"; public static void LaunchForException(string message, Exception exception) { @@ -43,8 +51,7 @@ Open an issue on GitHub? {exception} ``` """); - var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}"; - LinkLauncher.LaunchUrl(url); + LinkLauncher.LaunchUrl(BuildUrl(message, extraBody)); } public static void LaunchForException(string message) @@ -71,8 +78,7 @@ Open an issue on GitHub? * Action: {message} """); - var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}"; - LinkLauncher.LaunchUrl(url); + LinkLauncher.LaunchUrl(BuildUrl(message, extraBody)); } static bool AskIfOpenIssue(string text)