From 12f70d8f77ff5784674e955dedfcd2c12b057940 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 10 Sep 2026 15:19:53 +1000 Subject: [PATCH] Add net9 Uri span escaping APIs EscapeDataString, UnescapeDataString, TryEscapeDataString and TryUnescapeDataString over spans. Each routes through the string overload on the same target, so the escaped set matches whatever that framework's Uri.EscapeDataString produces rather than backporting net11 behaviour onto .NET Framework. That costs an intermediate string, which is the whole point of the span overloads, so it is noted on all four. Verified against net11: a failed Try reports zero chars written, an exactly sized destination succeeds, and an overlapping destination works for unescaping, which only ever shrinks. While testing, found that net9.0 and net10.0 throw ArgumentOutOfRangeException from Uri.TryUnescapeDataString instead of returning false whenever the destination is smaller than the literal text preceding the first escape sequence. Fixed in net11. The polyfill matches net11, so net8.0 and below return false. Covered by a test that asserts the throw on the two affected frameworks. API count 1105 -> 1109. --- apiCount.include.md | 40 +++--- api_list.include.md | 8 ++ assemblySize.include.md | 72 +++++----- readme.md | 124 ++++++++-------- src/Consume/Consume.cs | 11 ++ src/Polyfill/UriPolyfill.cs | 53 +++++++ src/Split/net461/UriPolyfill.cs | 35 +++++ src/Split/net462/UriPolyfill.cs | 35 +++++ src/Split/net47/UriPolyfill.cs | 35 +++++ src/Split/net471/UriPolyfill.cs | 35 +++++ src/Split/net472/UriPolyfill.cs | 35 +++++ src/Split/net48/UriPolyfill.cs | 35 +++++ src/Split/net481/UriPolyfill.cs | 35 +++++ src/Split/net5.0/UriPolyfill.cs | 35 +++++ src/Split/net6.0/UriPolyfill.cs | 35 +++++ src/Split/net7.0/UriPolyfill.cs | 35 +++++ src/Split/net8.0/UriPolyfill.cs | 35 +++++ src/Split/netcoreapp2.0/UriPolyfill.cs | 35 +++++ src/Split/netcoreapp2.1/UriPolyfill.cs | 35 +++++ src/Split/netcoreapp2.2/UriPolyfill.cs | 35 +++++ src/Split/netcoreapp3.0/UriPolyfill.cs | 35 +++++ src/Split/netcoreapp3.1/UriPolyfill.cs | 35 +++++ src/Split/netstandard2.0/UriPolyfill.cs | 35 +++++ src/Split/netstandard2.1/UriPolyfill.cs | 35 +++++ src/Split/uap10.0/UriPolyfill.cs | 35 +++++ src/Tests/UriPolyfillTests.cs | 179 ++++++++++++++++++++++++ 26 files changed, 1038 insertions(+), 114 deletions(-) create mode 100644 src/Tests/UriPolyfillTests.cs diff --git a/apiCount.include.md b/apiCount.include.md index 7149901f..b1c1c59d 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,28 +1,28 @@ -**API count: 1105** +**API count: 1109** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 1032 | -| `net462` | 1032 | -| `net47` | 1031 | -| `net471` | 1030 | -| `net472` | 1026 | -| `net48` | 1026 | -| `net481` | 1026 | -| `netstandard2.0` | 1028 | -| `netstandard2.1` | 881 | -| `netcoreapp2.0` | 951 | -| `netcoreapp2.1` | 892 | -| `netcoreapp2.2` | 892 | -| `netcoreapp3.0` | 853 | -| `netcoreapp3.1` | 852 | -| `net5.0` | 722 | -| `net6.0` | 623 | -| `net7.0` | 474 | -| `net8.0` | 353 | +| `net461` | 1036 | +| `net462` | 1036 | +| `net47` | 1035 | +| `net471` | 1034 | +| `net472` | 1030 | +| `net48` | 1030 | +| `net481` | 1030 | +| `netstandard2.0` | 1032 | +| `netstandard2.1` | 885 | +| `netcoreapp2.0` | 955 | +| `netcoreapp2.1` | 896 | +| `netcoreapp2.2` | 896 | +| `netcoreapp3.0` | 857 | +| `netcoreapp3.1` | 856 | +| `net5.0` | 726 | +| `net6.0` | 627 | +| `net7.0` | 478 | +| `net8.0` | 357 | | `net9.0` | 236 | | `net10.0` | 182 | | `net11.0` | 58 | -| `uap10.0` | 1018 | +| `uap10.0` | 1022 | diff --git a/api_list.include.md b/api_list.include.md index d0ca7ab5..668d8752 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -1473,6 +1473,14 @@ #### Uri + * `string EscapeDataString(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.escapedatastring?view=net-11.0#system-uri-escapedatastring(system-readonlyspan((system-char)))) + * Note: Copies the span to a string first, so this allocates where the BCL escapes straight from the span. + * `bool TryEscapeDataString(ReadOnlySpan, Span, int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryescapedatastring?view=net-11.0) + * Note: Escapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + * `bool TryUnescapeDataString(ReadOnlySpan, Span, int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryunescapedatastring?view=net-11.0) + * Note: Unescapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + * `string UnescapeDataString(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.unescapedatastring?view=net-11.0#system-uri-unescapedatastring(system-readonlyspan((system-char)))) + * Note: Copies the span to a string first, so this allocates where the BCL unescapes straight from the span. * `string UriSchemeData` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.urischemedata?view=net-11.0) diff --git a/assemblySize.include.md b/assemblySize.include.md index e3799e63..7fddf141 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -2,24 +2,24 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 370.5KB | +362.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 326.0KB | +317.5KB | +8.5KB | +6.5KB | +8.5KB | +13.5KB | -| net461 | 8.5KB | 369.0KB | +360.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 374.0KB | +367.0KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | -| net47 | 7.0KB | 374.0KB | +367.0KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | -| net471 | 8.5KB | 371.5KB | +363.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net48 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net481 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| netcoreapp2.0 | 9.0KB | 349.0KB | +340.0KB | +8.0KB | +7.0KB | +9.5KB | +14.0KB | -| netcoreapp2.1 | 9.0KB | 329.5KB | +320.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 329.5KB | +320.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 325.5KB | +316.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 323.5KB | +314.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net5.0 | 9.5KB | 287.5KB | +278.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net6.0 | 10.0KB | 229.0KB | +219.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 195.5KB | +185.5KB | +9.5KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 165.5KB | +156.0KB | +8.5KB | | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 371.0KB | +363.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netstandard2.1 | 8.5KB | 326.5KB | +318.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 370.0KB | +361.5KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | +| net462 | 7.0KB | 375.0KB | +368.0KB | +7.0KB | +6.5KB | +7.5KB | +11.5KB | +| net47 | 7.0KB | 374.5KB | +367.5KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | +| net471 | 8.5KB | 372.5KB | +364.0KB | +8.5KB | +6.0KB | +8.5KB | +13.0KB | +| net472 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 350.0KB | +341.0KB | +7.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 330.0KB | +321.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 330.0KB | +321.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 326.0KB | +316.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 324.0KB | +314.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net5.0 | 9.5KB | 288.0KB | +278.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net6.0 | 10.0KB | 230.0KB | +220.0KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | +| net7.0 | 10.0KB | 196.0KB | +186.0KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | +| net8.0 | 9.5KB | 166.0KB | +156.5KB | +8.5KB | +512bytes | +512bytes | +3.5KB | | net9.0 | 9.5KB | 115.5KB | +106.0KB | +8.5KB | | +512bytes | +3.0KB | | net10.0 | 10.0KB | 93.5KB | +83.5KB | +8.5KB | | +512bytes | +3.0KB | | net11.0 | 10.0KB | 21.0KB | +11.0KB | +9.0KB | | +512bytes | +3.5KB | @@ -29,24 +29,24 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 543.3KB | +535.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 472.2KB | +463.7KB | +16.2KB | +8.2KB | +13.4KB | +18.9KB | -| net461 | 8.5KB | 542.8KB | +534.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 547.8KB | +540.8KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net47 | 7.0KB | 547.6KB | +540.6KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net471 | 8.5KB | 544.7KB | +536.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net48 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net481 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| netcoreapp2.0 | 9.0KB | 511.2KB | +502.2KB | +15.7KB | +8.7KB | +14.4KB | +19.4KB | -| netcoreapp2.1 | 9.0KB | 479.4KB | +470.4KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 479.4KB | +470.4KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 468.7KB | +459.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 466.7KB | +457.2KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 412.6KB | +403.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net6.0 | 10.0KB | 334.1KB | +324.1KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 282.8KB | +272.8KB | +17.1KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 239.0KB | +229.5KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 544.1KB | +536.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netstandard2.1 | 8.5KB | 473.0KB | +464.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 544.1KB | +535.6KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | +| net462 | 7.0KB | 549.1KB | +542.1KB | +14.7KB | +8.2KB | +12.4KB | +16.9KB | +| net47 | 7.0KB | 548.4KB | +541.4KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | +| net471 | 8.5KB | 546.0KB | +537.5KB | +16.2KB | +7.7KB | +13.4KB | +18.4KB | +| net472 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 512.5KB | +503.5KB | +15.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 480.2KB | +471.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 480.2KB | +471.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 469.5KB | +460.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 467.5KB | +458.0KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 413.4KB | +403.9KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net6.0 | 10.0KB | 335.4KB | +325.4KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | +| net7.0 | 10.0KB | 283.6KB | +273.6KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | +| net8.0 | 9.5KB | 239.8KB | +230.3KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 166.8KB | +157.3KB | +16.0KB | | +1.1KB | +3.7KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | | net11.0 | 10.0KB | 30.9KB | +20.9KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/readme.md b/readme.md index 45d6bcde..e40fa0f2 100644 --- a/readme.md +++ b/readme.md @@ -13,34 +13,34 @@ The package targets `netstandard2.0` and is designed to support the following ru * `uap10` -**API count: 1105** +**API count: 1109** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 1032 | -| `net462` | 1032 | -| `net47` | 1031 | -| `net471` | 1030 | -| `net472` | 1026 | -| `net48` | 1026 | -| `net481` | 1026 | -| `netstandard2.0` | 1028 | -| `netstandard2.1` | 881 | -| `netcoreapp2.0` | 951 | -| `netcoreapp2.1` | 892 | -| `netcoreapp2.2` | 892 | -| `netcoreapp3.0` | 853 | -| `netcoreapp3.1` | 852 | -| `net5.0` | 722 | -| `net6.0` | 623 | -| `net7.0` | 474 | -| `net8.0` | 353 | +| `net461` | 1036 | +| `net462` | 1036 | +| `net47` | 1035 | +| `net471` | 1034 | +| `net472` | 1030 | +| `net48` | 1030 | +| `net481` | 1030 | +| `netstandard2.0` | 1032 | +| `netstandard2.1` | 885 | +| `netcoreapp2.0` | 955 | +| `netcoreapp2.1` | 896 | +| `netcoreapp2.2` | 896 | +| `netcoreapp3.0` | 857 | +| `netcoreapp3.1` | 856 | +| `net5.0` | 726 | +| `net6.0` | 627 | +| `net7.0` | 478 | +| `net8.0` | 357 | | `net9.0` | 236 | | `net10.0` | 182 | | `net11.0` | 58 | -| `uap10.0` | 1018 | +| `uap10.0` | 1022 | @@ -96,24 +96,24 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 370.5KB | +362.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 326.0KB | +317.5KB | +8.5KB | +6.5KB | +8.5KB | +13.5KB | -| net461 | 8.5KB | 369.0KB | +360.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 374.0KB | +367.0KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | -| net47 | 7.0KB | 374.0KB | +367.0KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | -| net471 | 8.5KB | 371.5KB | +363.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net48 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net481 | 8.5KB | 370.5KB | +362.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| netcoreapp2.0 | 9.0KB | 349.0KB | +340.0KB | +8.0KB | +7.0KB | +9.5KB | +14.0KB | -| netcoreapp2.1 | 9.0KB | 329.5KB | +320.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 329.5KB | +320.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 325.5KB | +316.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 323.5KB | +314.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net5.0 | 9.5KB | 287.5KB | +278.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net6.0 | 10.0KB | 229.0KB | +219.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 195.5KB | +185.5KB | +9.5KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 165.5KB | +156.0KB | +8.5KB | | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 371.0KB | +363.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netstandard2.1 | 8.5KB | 326.5KB | +318.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 370.0KB | +361.5KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | +| net462 | 7.0KB | 375.0KB | +368.0KB | +7.0KB | +6.5KB | +7.5KB | +11.5KB | +| net47 | 7.0KB | 374.5KB | +367.5KB | +7.5KB | +6.5KB | +7.5KB | +12.0KB | +| net471 | 8.5KB | 372.5KB | +364.0KB | +8.5KB | +6.0KB | +8.5KB | +13.0KB | +| net472 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 371.0KB | +362.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 350.0KB | +341.0KB | +7.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 330.0KB | +321.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 330.0KB | +321.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 326.0KB | +316.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 324.0KB | +314.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net5.0 | 9.5KB | 288.0KB | +278.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net6.0 | 10.0KB | 230.0KB | +220.0KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | +| net7.0 | 10.0KB | 196.0KB | +186.0KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | +| net8.0 | 9.5KB | 166.0KB | +156.5KB | +8.5KB | +512bytes | +512bytes | +3.5KB | | net9.0 | 9.5KB | 115.5KB | +106.0KB | +8.5KB | | +512bytes | +3.0KB | | net10.0 | 10.0KB | 93.5KB | +83.5KB | +8.5KB | | +512bytes | +3.0KB | | net11.0 | 10.0KB | 21.0KB | +11.0KB | +9.0KB | | +512bytes | +3.5KB | @@ -123,24 +123,24 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 543.3KB | +535.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 472.2KB | +463.7KB | +16.2KB | +8.2KB | +13.4KB | +18.9KB | -| net461 | 8.5KB | 542.8KB | +534.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 547.8KB | +540.8KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net47 | 7.0KB | 547.6KB | +540.6KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net471 | 8.5KB | 544.7KB | +536.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net48 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net481 | 8.5KB | 542.7KB | +534.2KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| netcoreapp2.0 | 9.0KB | 511.2KB | +502.2KB | +15.7KB | +8.7KB | +14.4KB | +19.4KB | -| netcoreapp2.1 | 9.0KB | 479.4KB | +470.4KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 479.4KB | +470.4KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 468.7KB | +459.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 466.7KB | +457.2KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 412.6KB | +403.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net6.0 | 10.0KB | 334.1KB | +324.1KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 282.8KB | +272.8KB | +17.1KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 239.0KB | +229.5KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 544.1KB | +536.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netstandard2.1 | 8.5KB | 473.0KB | +464.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 544.1KB | +535.6KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | +| net462 | 7.0KB | 549.1KB | +542.1KB | +14.7KB | +8.2KB | +12.4KB | +16.9KB | +| net47 | 7.0KB | 548.4KB | +541.4KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | +| net471 | 8.5KB | 546.0KB | +537.5KB | +16.2KB | +7.7KB | +13.4KB | +18.4KB | +| net472 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 543.4KB | +534.9KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 512.5KB | +503.5KB | +15.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 480.2KB | +471.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 480.2KB | +471.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 469.5KB | +460.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 467.5KB | +458.0KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 413.4KB | +403.9KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net6.0 | 10.0KB | 335.4KB | +325.4KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | +| net7.0 | 10.0KB | 283.6KB | +273.6KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | +| net8.0 | 9.5KB | 239.8KB | +230.3KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 166.8KB | +157.3KB | +16.0KB | | +1.1KB | +3.7KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | | net11.0 | 10.0KB | 30.9KB | +20.9KB | +16.5KB | | +1.1KB | +4.2KB | @@ -2088,6 +2088,14 @@ The class `Polyfill` includes the following extension methods: #### Uri + * `string EscapeDataString(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.escapedatastring?view=net-11.0#system-uri-escapedatastring(system-readonlyspan((system-char)))) + * Note: Copies the span to a string first, so this allocates where the BCL escapes straight from the span. + * `bool TryEscapeDataString(ReadOnlySpan, Span, int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryescapedatastring?view=net-11.0) + * Note: Escapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + * `bool TryUnescapeDataString(ReadOnlySpan, Span, int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryunescapedatastring?view=net-11.0) + * Note: Unescapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + * `string UnescapeDataString(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.unescapedatastring?view=net-11.0#system-uri-unescapedatastring(system-readonlyspan((system-char)))) + * Note: Copies the span to a string first, so this allocates where the BCL unescapes straight from the span. * `string UriSchemeData` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uri.urischemedata?view=net-11.0) @@ -2489,7 +2497,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2508,7 +2516,7 @@ void EnsureExample(Order order, Customer customer, string customerId, string ema this.quantity = Ensure.NotNegativeOrZero(quantity); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 9a29574e..f9df0ab2 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -331,6 +331,17 @@ void UriSchemeDataUsage() _ = Uri.UriSchemeData; } +#if FeatureMemory + void UriEscapingUsage() + { + var escaped = Uri.EscapeDataString("a b".AsSpan()); + var unescaped = Uri.UnescapeDataString("a%20b".AsSpan()); + Span destination = stackalloc char[10]; + var escapedOk = Uri.TryEscapeDataString("a b".AsSpan(), destination, out var escapedWritten); + var unescapedOk = Uri.TryUnescapeDataString("a%20b".AsSpan(), destination, out var unescapedWritten); + } +#endif + #if FeatureMemory void Utf16Usage() { diff --git a/src/Polyfill/UriPolyfill.cs b/src/Polyfill/UriPolyfill.cs index 3802c397..7793a334 100644 --- a/src/Polyfill/UriPolyfill.cs +++ b/src/Polyfill/UriPolyfill.cs @@ -13,7 +13,60 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.uri.urischemedata?view=net-11.0 public static string UriSchemeData => "data"; + +#if FeatureMemory && !NET9_0_OR_GREATER + + /// + /// Converts a span of characters to its escaped representation. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.uri.escapedatastring?view=net-11.0#system-uri-escapedatastring(system-readonlyspan((system-char))) + //Note: Copies the span to a string first, so this allocates where the BCL escapes straight from the span. + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + + /// + /// Converts a span of characters to its unescaped representation. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.uri.unescapedatastring?view=net-11.0#system-uri-unescapedatastring(system-readonlyspan((system-char))) + //Note: Copies the span to a string first, so this allocates where the BCL unescapes straight from the span. + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryescapedatastring?view=net-11.0 + //Note: Escapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.uri.tryunescapedatastring?view=net-11.0 + //Note: Unescapes through an intermediate string, so this allocates where the BCL writes straight to the destination. + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); + +#endif } + +#if FeatureMemory && !NET9_0_OR_GREATER + + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; + } + +#endif } #endif diff --git a/src/Split/net461/UriPolyfill.cs b/src/Split/net461/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net461/UriPolyfill.cs +++ b/src/Split/net461/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net462/UriPolyfill.cs b/src/Split/net462/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net462/UriPolyfill.cs +++ b/src/Split/net462/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net47/UriPolyfill.cs b/src/Split/net47/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net47/UriPolyfill.cs +++ b/src/Split/net47/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net471/UriPolyfill.cs b/src/Split/net471/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net471/UriPolyfill.cs +++ b/src/Split/net471/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net472/UriPolyfill.cs b/src/Split/net472/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net472/UriPolyfill.cs +++ b/src/Split/net472/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net48/UriPolyfill.cs b/src/Split/net48/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net48/UriPolyfill.cs +++ b/src/Split/net48/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net481/UriPolyfill.cs b/src/Split/net481/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net481/UriPolyfill.cs +++ b/src/Split/net481/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net5.0/UriPolyfill.cs b/src/Split/net5.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net5.0/UriPolyfill.cs +++ b/src/Split/net5.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net6.0/UriPolyfill.cs b/src/Split/net6.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net6.0/UriPolyfill.cs +++ b/src/Split/net6.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net7.0/UriPolyfill.cs b/src/Split/net7.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net7.0/UriPolyfill.cs +++ b/src/Split/net7.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/net8.0/UriPolyfill.cs b/src/Split/net8.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/net8.0/UriPolyfill.cs +++ b/src/Split/net8.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netcoreapp2.0/UriPolyfill.cs b/src/Split/netcoreapp2.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netcoreapp2.0/UriPolyfill.cs +++ b/src/Split/netcoreapp2.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netcoreapp2.1/UriPolyfill.cs b/src/Split/netcoreapp2.1/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netcoreapp2.1/UriPolyfill.cs +++ b/src/Split/netcoreapp2.1/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netcoreapp2.2/UriPolyfill.cs b/src/Split/netcoreapp2.2/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netcoreapp2.2/UriPolyfill.cs +++ b/src/Split/netcoreapp2.2/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netcoreapp3.0/UriPolyfill.cs b/src/Split/netcoreapp3.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netcoreapp3.0/UriPolyfill.cs +++ b/src/Split/netcoreapp3.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netcoreapp3.1/UriPolyfill.cs b/src/Split/netcoreapp3.1/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netcoreapp3.1/UriPolyfill.cs +++ b/src/Split/netcoreapp3.1/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netstandard2.0/UriPolyfill.cs b/src/Split/netstandard2.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netstandard2.0/UriPolyfill.cs +++ b/src/Split/netstandard2.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/netstandard2.1/UriPolyfill.cs b/src/Split/netstandard2.1/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/netstandard2.1/UriPolyfill.cs +++ b/src/Split/netstandard2.1/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Split/uap10.0/UriPolyfill.cs b/src/Split/uap10.0/UriPolyfill.cs index 81679bd1..88311079 100644 --- a/src/Split/uap10.0/UriPolyfill.cs +++ b/src/Split/uap10.0/UriPolyfill.cs @@ -10,5 +10,40 @@ static partial class Polyfill /// Provides the scheme name for the data URI scheme (RFC 2397). This field is read-only. /// public static string UriSchemeData => "data"; +#if FeatureMemory + /// + /// Converts a span of characters to its escaped representation. + /// + public static string EscapeDataString(ReadOnlySpan charsToEscape) => + Uri.EscapeDataString(charsToEscape.ToString()); + /// + /// Converts a span of characters to its unescaped representation. + /// + public static string UnescapeDataString(ReadOnlySpan charsToUnescape) => + Uri.UnescapeDataString(charsToUnescape.ToString()); + /// + /// Attempts to convert a span of characters to its escaped representation. + /// + public static bool TryEscapeDataString(ReadOnlySpan charsToEscape, Span destination, out int charsWritten) => + TryWriteTo(Uri.EscapeDataString(charsToEscape.ToString()), destination, out charsWritten); + /// + /// Attempts to convert a span of characters to its unescaped representation. + /// + public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Span destination, out int charsWritten) => + TryWriteTo(Uri.UnescapeDataString(charsToUnescape.ToString()), destination, out charsWritten); +#endif + } +#if FeatureMemory + static bool TryWriteTo(string value, Span destination, out int charsWritten) + { + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + value.AsSpan().CopyTo(destination); + charsWritten = value.Length; + return true; } +#endif } diff --git a/src/Tests/UriPolyfillTests.cs b/src/Tests/UriPolyfillTests.cs new file mode 100644 index 00000000..8d04f9c5 --- /dev/null +++ b/src/Tests/UriPolyfillTests.cs @@ -0,0 +1,179 @@ +#if FeatureMemory + +// The span overloads must agree with the string overloads on the framework they run on. +// Uri.EscapeDataString itself escapes a different set of characters on .NET Framework than +// on .NET, so comparing the two overloads is the invariant that holds everywhere, rather +// than any fixed expected output. +public class UriPolyfillTests +{ + static string[] samples = + [ + "", + "abc", + "a b", + "a/b?c=d&e=f", + "%", + "%%", + "%4", + "%41", + "%zz", + "%25", + "-._~", + "!*'()", + "+", + "é", + "你好", + "%E4%BD%A0%E5%A5%BD", + "abc%20def" + ]; + + [Test] + public async Task EscapeDataString_MatchesStringOverload() + { + foreach (var sample in samples) + { + var fromSpan = Capture(() => Uri.EscapeDataString(sample.AsSpan())); + var fromString = Capture(() => Uri.EscapeDataString(sample)); + await Assert.That(fromSpan).IsEqualTo(fromString); + } + } + + [Test] + public async Task UnescapeDataString_MatchesStringOverload() + { + foreach (var sample in samples) + { + var fromSpan = Capture(() => Uri.UnescapeDataString(sample.AsSpan())); + var fromString = Capture(() => Uri.UnescapeDataString(sample)); + await Assert.That(fromSpan).IsEqualTo(fromString); + } + } + + [Test] + public async Task EscapeDataString_KnownValues() + { + await Assert.That(Uri.EscapeDataString("a b".AsSpan())).IsEqualTo("a%20b"); + await Assert.That(Uri.EscapeDataString("abc".AsSpan())).IsEqualTo("abc"); + await Assert.That(Uri.EscapeDataString(default(ReadOnlySpan))).IsEqualTo(string.Empty); + await Assert.That(Uri.UnescapeDataString("a%20b".AsSpan())).IsEqualTo("a b"); + await Assert.That(Uri.UnescapeDataString(default(ReadOnlySpan))).IsEqualTo(string.Empty); + } + + [Test] + public async Task TryEscapeDataString_Sizing() + { + foreach (var sample in samples) + { + var expected = Uri.EscapeDataString(sample); + for (var size = 0; size <= expected.Length + 1; size++) + { + var result = TryEscape(sample, size); + + await Assert.That(result.Succeeded).IsEqualTo(size >= expected.Length); + if (result.Succeeded) + { + await Assert.That(result.Written).IsEqualTo(expected.Length); + await Assert.That(result.Content).IsEqualTo(expected); + } + else + { + // a failed attempt reports nothing written + await Assert.That(result.Written).IsEqualTo(0); + } + } + } + } + + [Test] + public async Task TryUnescapeDataString_Sizing() + { + foreach (var sample in samples) + { + var expected = Uri.UnescapeDataString(sample); + for (var size = SmallestTestableSize(sample); size <= expected.Length + 1; size++) + { + var result = TryUnescape(sample, size); + + await Assert.That(result.Succeeded).IsEqualTo(size >= expected.Length); + if (result.Succeeded) + { + await Assert.That(result.Written).IsEqualTo(expected.Length); + await Assert.That(result.Content).IsEqualTo(expected); + } + else + { + await Assert.That(result.Written).IsEqualTo(0); + } + } + } + } + + // 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 + 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 + [Test] + public async Task TryUnescapeDataString_InPlace() + { + var buffer = "a%20b".ToCharArray(); + var succeeded = Uri.TryUnescapeDataString(buffer, buffer, out var written); + var writtenCount = written; + var content = new string(buffer, 0, written); + + await Assert.That(succeeded).IsTrue(); + await Assert.That(writtenCount).IsEqualTo(3); + await Assert.That(content).IsEqualTo("a b"); + } + + static int SmallestTestableSize(string sample) + { +#if NET9_0_OR_GREATER && !NET11_0_OR_GREATER + // skip the sizes covered by TryUnescapeDataString_DestinationSmallerThanLiteralPrefix + var index = sample.IndexOf('%'); + return index < 0 ? 0 : index; +#else + return 0; +#endif + } + + // the spans stay inside these helpers, so nothing ref struct shaped has to live across an await + static (bool Succeeded, int Written, string? Content) TryEscape(string value, int destinationSize) + { + var buffer = new char[destinationSize]; + var succeeded = Uri.TryEscapeDataString(value.AsSpan(), buffer, out var written); + return (succeeded, written, succeeded ? new string(buffer, 0, written) : null); + } + + static (bool Succeeded, int Written, string? Content) TryUnescape(string value, int destinationSize) + { + var buffer = new char[destinationSize]; + var succeeded = Uri.TryUnescapeDataString(value.AsSpan(), buffer, out var written); + return (succeeded, written, succeeded ? new string(buffer, 0, written) : null); + } + + static string Capture(Func func) + { + try + { + return func(); + } + catch (Exception exception) + { + return exception.GetType().Name; + } + } +} + +#endif