diff --git a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs deleted file mode 100644 index 7ef353255b16..000000000000 --- a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Microsoft.AspNetCore.Http; - -internal static class PathStringHelper -{ - // uint[] bits uses 1 cache line (Array info + 16 bytes) - // bool[] would use 3 cache lines (Array info + 128 bytes) - // So we use 128 bits rather than 128 bytes/bools - private static readonly uint[] ValidPathChars = { - 0b_0000_0000__0000_0000__0000_0000__0000_0000, // 0x00 - 0x1F - 0b_0010_1111__1111_1111__1111_1111__1101_0010, // 0x20 - 0x3F - 0b_1000_0111__1111_1111__1111_1111__1111_1111, // 0x40 - 0x5F - 0b_0100_0111__1111_1111__1111_1111__1111_1110, // 0x60 - 0x7F - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidPathChar(char c) - { - // Use local array and uint .Length compare to elide the bounds check on array access - var validChars = ValidPathChars; - var i = (int)c; - - // Array is in chunks of 32 bits, so get offset by dividing by 32 - var offset = i >> 5; // i / 32; - // Significant bit position is the remainder of the above calc; i % 32 => i & 31 - var significantBit = 1u << (i & 31); - - // Check offset in bounds and check if significant bit set - return (uint)offset < (uint)validChars.Length && - ((validChars[offset] & significantBit) != 0); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPercentEncodedChar(string str, int index) - { - var len = (uint)str.Length; - if (str[index] == '%' && index < len - 2) - { - return AreFollowingTwoCharsHex(str, index); - } - - return false; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool AreFollowingTwoCharsHex(string str, int index) - { - Debug.Assert(index < str.Length - 2); - - var c1 = str[index + 1]; - var c2 = str[index + 2]; - return IsHexadecimalChar(c1) && IsHexadecimalChar(c2); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHexadecimalChar(char c) - { - // Between 0 - 9 or uppercased between A - F - return (uint)(c - '0') <= 9 || (uint)((c & ~0x20) - 'A') <= ('F' - 'A'); - } -} diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs index 8a1dedc71d55..485f99b4cda8 100644 --- a/src/Http/Http.Abstractions/src/PathString.cs +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -18,6 +19,9 @@ namespace Microsoft.AspNetCore.Http; [DebuggerDisplay("{Value}")] public readonly struct PathString : IEquatable { + private static readonly SearchValues s_validPathChars = + SearchValues.Create("!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"); + internal const int StackAllocThreshold = 128; /// @@ -68,27 +72,18 @@ public override string ToString() /// The escaped path value public string ToUriComponent() { - if (!HasValue) - { - return string.Empty; - } - var value = Value; - var i = 0; - for (; i < value.Length; i++) - { - if (!PathStringHelper.IsValidPathChar(value[i]) || PathStringHelper.IsPercentEncodedChar(value, i)) - { - break; - } - } - if (i < value.Length) + if (string.IsNullOrEmpty(value)) { - return ToEscapedUriComponent(value, i); + return string.Empty; } - return value; + var indexOfInvalidChar = value.AsSpan().IndexOfAnyExcept(s_validPathChars); + + return indexOfInvalidChar < 0 + ? value + : ToEscapedUriComponent(value, indexOfInvalidChar); } private static string ToEscapedUriComponent(string value, int i) @@ -99,10 +94,10 @@ private static string ToEscapedUriComponent(string value, int i) var count = i; var requiresEscaping = false; - while (i < value.Length) + while ((uint)i < (uint)value.Length) { - var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(value, i); - if (PathStringHelper.IsValidPathChar(value[i]) || isPercentEncodedChar) + var isPercentEncodedChar = false; + if (s_validPathChars.Contains(value[i]) || (isPercentEncodedChar = Uri.IsHexEncoding(value, i))) { if (requiresEscaping) { @@ -122,8 +117,18 @@ private static string ToEscapedUriComponent(string value, int i) } else { - count++; - i++; + // We just saw a character we don't want to escape. It's likely there are more, do a vectorized search. + var charsToSkip = value.AsSpan(i).IndexOfAnyExcept(s_validPathChars); + + if (charsToSkip < 0) + { + // Only valid characters remain + count += value.Length - i; + break; + } + + count += charsToSkip; + i += charsToSkip; } } else @@ -150,21 +155,19 @@ private static string ToEscapedUriComponent(string value, int i) } else { - if (count > 0) - { - buffer ??= new StringBuilder(value.Length * 3); + Debug.Assert(count > 0); + Debug.Assert(buffer is not null); - if (requiresEscaping) - { - buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - } - else - { - buffer.Append(value, start, count); - } + if (requiresEscaping) + { + buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); + } + else + { + buffer.Append(value, start, count); } - return buffer?.ToString() ?? string.Empty; + return buffer.ToString(); } }