From fe27a63bde0a1e9586b6f8cf207ea88e90905128 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 10 Sep 2026 15:28:38 +1000 Subject: [PATCH] Probe the net9 Uri unescape bug rather than infer it from the target framework The tests added in #595 asserted that Uri.TryUnescapeDataString throws on net9.0 and net10.0, which bets that neither is ever serviced. dotnet/runtime#128610 is a written backport of the fix to release/10.0 that was deferred rather than rejected, so that bet could be lost, and net10.0 is in support until 2028. The behaviour is now probed once instead. net9.0 and net10.0 accept either outcome, and the assertion that it must not throw is kept for net11.0 and for every target where the polyfill is active. --- src/Tests/UriPolyfillTests.cs | 48 +++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/Tests/UriPolyfillTests.cs b/src/Tests/UriPolyfillTests.cs index 8d04f9c5..642a174c 100644 --- a/src/Tests/UriPolyfillTests.cs +++ b/src/Tests/UriPolyfillTests.cs @@ -108,19 +108,23 @@ public async Task TryUnescapeDataString_Sizing() } } - // net9.0 and net10.0 throw ArgumentOutOfRangeException instead of returning false when the - // destination cannot hold the literal text preceding the first escape sequence. Fixed in - // net11, and the polyfill behaves the way net11 does. [Test] public async Task TryUnescapeDataString_DestinationSmallerThanLiteralPrefix() { -#if NET9_0_OR_GREATER && !NET11_0_OR_GREATER - await Assert.That(() => TryUnescape("abc%20def", 2)).Throws(); -#else +#if !NET9_0_OR_GREATER || NET11_0_OR_GREATER + // only net9.0 and net10.0 are allowed to throw here + await Assert.That(throwsOnShortDestination).IsFalse(); +#endif + + if (throwsOnShortDestination) + { + await Assert.That(() => TryUnescape("abc%20def", 2)).Throws(); + return; + } + var result = TryUnescape("abc%20def", 2); await Assert.That(result.Succeeded).IsFalse(); await Assert.That(result.Written).IsEqualTo(0); -#endif } // unescaping only ever shrinks, so a destination that overlaps the source is safe @@ -139,13 +143,35 @@ public async Task TryUnescapeDataString_InPlace() static int SmallestTestableSize(string sample) { -#if NET9_0_OR_GREATER && !NET11_0_OR_GREATER + if (!throwsOnShortDestination) + { + return 0; + } + // skip the sizes covered by TryUnescapeDataString_DestinationSmallerThanLiteralPrefix var index = sample.IndexOf('%'); return index < 0 ? 0 : index; -#else - return 0; -#endif + } + + // net9.0 and net10.0 shipped Uri.TryUnescapeDataString throwing ArgumentOutOfRangeException + // instead of returning false when the destination cannot hold the literal text preceding the + // first escape sequence (dotnet/runtime#124654, fixed for net11 by dotnet/runtime#124655). + // The backport to release/10.0, dotnet/runtime#128610, was written and then deferred, so the + // behaviour is probed rather than inferred from the target framework: a serviced net9.0 or + // net10.0 runtime would otherwise turn these tests red. The polyfill never throws. + static bool throwsOnShortDestination = ProbeThrowsOnShortDestination(); + + static bool ProbeThrowsOnShortDestination() + { + try + { + Uri.TryUnescapeDataString("abc%20def".AsSpan(), new char[2], out _); + return false; + } + catch (ArgumentOutOfRangeException) + { + return true; + } } // the spans stay inside these helpers, so nothing ref struct shaped has to live across an await