From 358062bfa2a5738340d09427d3312611d8492caa Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 21 Nov 2022 23:11:50 +0100 Subject: [PATCH] Use IndexOfAnyValues in System.Net.Http --- .../Net/Http/Headers/AltSvcHeaderParser.cs | 11 +- .../Net/Http/Headers/HeaderUtilities.cs | 42 +++-- .../System/Net/Http/Headers/KnownHeader.cs | 4 +- .../src/System/Net/Http/HttpMethod.cs | 2 +- .../src/System/Net/Http/HttpRuleParser.cs | 143 +++++------------- .../tests/UnitTests/HttpRuleParserTest.cs | 10 +- .../src/System/Net/Cookie.cs | 7 +- 7 files changed, 80 insertions(+), 139 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs index beef88449c336f..22935964ebc9e3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs @@ -406,12 +406,10 @@ private static bool TryReadTokenOrQuotedInt32(string value, int startIndex, out return false; } - if (HttpRuleParser.IsTokenChar(value[startIndex])) + int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex); + if (tokenLength > 0) { // No reason for integers to be quoted, so this should be the hot path. - - int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex); - readLength = tokenLength; return HeaderUtilities.TryParseInt32(value, startIndex, tokenLength, out result); } @@ -471,9 +469,10 @@ private static bool TrySkipTokenOrQuoted(string value, int startIndex, out int r return false; } - if (HttpRuleParser.IsTokenChar(value[startIndex])) + int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex); + if (tokenLength > 0) { - readLength = HttpRuleParser.GetTokenLength(value, startIndex); + readLength = tokenLength; return true; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs index 14b1075ec1f775..711c8a23225fc0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs @@ -21,6 +21,11 @@ internal static class HeaderUtilities internal const string BytesUnit = "bytes"; + // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // ; token except ( "*" / "'" / "%" ) + private static readonly IndexOfAnyValues s_rfc5987AttrBytes = + IndexOfAnyValues.Create("!#$&+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"u8); + internal static void SetQuality(UnvalidatedObjectCollection parameters, double? value) { Debug.Assert(parameters != null); @@ -76,36 +81,45 @@ internal static bool ContainsNonAscii(string input) // encoding'lang'PercentEncodedSpecials internal static string Encode5987(string input) { - // Encode a string using RFC 5987 encoding. - // encoding'lang'PercentEncodedSpecials var builder = new ValueStringBuilder(stackalloc char[256]); byte[] utf8bytes = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(input.Length)); int utf8length = Encoding.UTF8.GetBytes(input, 0, input.Length, utf8bytes, 0); builder.Append("utf-8\'\'"); - for (int i = 0; i < utf8length; i++) + + ReadOnlySpan utf8 = utf8bytes.AsSpan(0, utf8length); + do { - byte utf8byte = utf8bytes[i]; + int length = utf8.IndexOfAnyExcept(s_rfc5987AttrBytes); + if (length < 0) + { + length = utf8.Length; + } + + Encoding.ASCII.GetChars(utf8.Slice(0, length), builder.AppendSpan(length)); - // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - // ; token except ( "*" / "'" / "%" ) - if (utf8byte > 0x7F) // Encodes as multiple utf-8 bytes + utf8 = utf8.Slice(length); + + if (utf8.IsEmpty) { - AddHexEscaped(utf8byte, ref builder); + break; } - else if (!HttpRuleParser.IsTokenChar((char)utf8byte) || utf8byte == '*' || utf8byte == '\'' || utf8byte == '%') + + length = utf8.IndexOfAny(s_rfc5987AttrBytes); + if (length < 0) { - // ASCII - Only one encoded byte. - AddHexEscaped(utf8byte, ref builder); + length = utf8.Length; } - else + + foreach (byte b in utf8.Slice(0, length)) { - builder.Append((char)utf8byte); + AddHexEscaped(b, ref builder); } + utf8 = utf8.Slice(length); } + while (!utf8.IsEmpty); - Array.Clear(utf8bytes, 0, utf8length); ArrayPool.Shared.Return(utf8bytes); return builder.ToString(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeader.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeader.cs index 0163db073a852e..ab78688af1e0c1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeader.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeader.cs @@ -12,13 +12,13 @@ public KnownHeader(string name, int? http2StaticTableIndex = null, int? http3Sta this(name, HttpHeaderType.Custom, parser: null, knownValues: null, http2StaticTableIndex, http3StaticTableIndex) { Debug.Assert(!string.IsNullOrEmpty(name)); - Debug.Assert(name[0] == ':' || HttpRuleParser.GetTokenLength(name, 0) == name.Length); + Debug.Assert(name[0] == ':' || HttpRuleParser.IsToken(name)); } public KnownHeader(string name, HttpHeaderType headerType, HttpHeaderParser? parser, string[]? knownValues = null, int? http2StaticTableIndex = null, int? http3StaticTableIndex = null) { Debug.Assert(!string.IsNullOrEmpty(name)); - Debug.Assert(name[0] == ':' || HttpRuleParser.GetTokenLength(name, 0) == name.Length); + Debug.Assert(name[0] == ':' || HttpRuleParser.IsToken(name)); Name = name; HeaderType = headerType; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs index 1ef905c4ac50cd..cda7a9f228a028 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs @@ -82,7 +82,7 @@ public HttpMethod(string method) { throw new ArgumentException(SR.net_http_argument_empty_string, nameof(method)); } - if (HttpRuleParser.GetTokenLength(method, 0) != method.Length) + if (!HttpRuleParser.IsToken(method)) { throw new FormatException(SR.net_http_httpmethod_format_error); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRuleParser.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRuleParser.cs index f7c5564edf71f1..ab0a01afa78b32 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRuleParser.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRuleParser.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.Diagnostics; using System.Text; @@ -8,7 +9,17 @@ namespace System.Net.Http { internal static class HttpRuleParser { - private static readonly bool[] s_tokenChars = CreateTokenChars(); + // token = 1* + // CTL = + private static readonly IndexOfAnyValues s_tokenChars = + IndexOfAnyValues.Create("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"); + + private static readonly IndexOfAnyValues s_tokenBytes = + IndexOfAnyValues.Create("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"u8); + + private static readonly IndexOfAnyValues s_hostDelimiterChars = + IndexOfAnyValues.Create("/ \t\r,"); + private const int MaxNestedCount = 5; internal const char CR = (char)13; @@ -18,98 +29,22 @@ internal static class HttpRuleParser internal static Encoding DefaultHttpEncoding => Encoding.Latin1; - private static bool[] CreateTokenChars() - { - // token = 1* - // CTL = - - var tokenChars = new bool[128]; // All elements default to "false". - - for (int i = 33; i < 127; i++) // Skip Space (32) & DEL (127). - { - tokenChars[i] = true; - } - - // Remove separators: these are not valid token characters. - tokenChars[(byte)'('] = false; - tokenChars[(byte)')'] = false; - tokenChars[(byte)'<'] = false; - tokenChars[(byte)'>'] = false; - tokenChars[(byte)'@'] = false; - tokenChars[(byte)','] = false; - tokenChars[(byte)';'] = false; - tokenChars[(byte)':'] = false; - tokenChars[(byte)'\\'] = false; - tokenChars[(byte)'"'] = false; - tokenChars[(byte)'/'] = false; - tokenChars[(byte)'['] = false; - tokenChars[(byte)']'] = false; - tokenChars[(byte)'?'] = false; - tokenChars[(byte)'='] = false; - tokenChars[(byte)'{'] = false; - tokenChars[(byte)'}'] = false; - - return tokenChars; - } - - internal static bool IsTokenChar(char character) - { - // Must be between 'space' (32) and 'DEL' (127). - if (character > 127) - { - return false; - } - - return s_tokenChars[character]; - } - internal static int GetTokenLength(string input, int startIndex) { - Debug.Assert(input != null); + Debug.Assert(input is not null); - if (startIndex >= input.Length) - { - return 0; - } - - int current = startIndex; - - while (current < input.Length) - { - if (!IsTokenChar(input[current])) - { - return current - startIndex; - } - current++; - } - return input.Length - startIndex; - } + ReadOnlySpan slice = input.AsSpan(startIndex); - internal static bool IsToken(string input) - { - for (int i = 0; i < input.Length; i++) - { - if (!IsTokenChar(input[i])) - { - return false; - } - } + int index = slice.IndexOfAnyExcept(s_tokenChars); - return true; + return index < 0 ? slice.Length : index; } - internal static bool IsToken(ReadOnlySpan input) - { - for (int i = 0; i < input.Length; i++) - { - if (!IsTokenChar((char)input[i])) - { - return false; - } - } + internal static bool IsToken(ReadOnlySpan input) => + input.IndexOfAnyExcept(s_tokenChars) < 0; - return true; - } + internal static bool IsToken(ReadOnlySpan input) => + input.IndexOfAnyExcept(s_tokenBytes) < 0; internal static string GetTokenString(ReadOnlySpan input) { @@ -147,10 +82,8 @@ internal static int GetWhitespaceLength(string input, int startIndex) return input.Length - startIndex; } - internal static bool ContainsNewLine(string value, int startIndex = 0) - { - return value.AsSpan(startIndex).IndexOfAny('\r', '\n') != -1; - } + internal static bool ContainsNewLine(string value, int startIndex = 0) => + value.AsSpan(startIndex).IndexOfAny('\r', '\n') >= 0; internal static int GetNumberLength(string input, int startIndex, bool allowDecimal) { @@ -206,41 +139,33 @@ internal static int GetHostLength(string input, int startIndex, bool allowToken) return 0; } + ReadOnlySpan slice = input.AsSpan(startIndex); + // A 'host' is either a token (if 'allowToken' == true) or a valid host name as defined by the URI RFC. // So we first iterate through the string and search for path delimiters and whitespace. When found, stop // and try to use the substring as token or URI host name. If it works, we have a host name, otherwise not. - int current = startIndex; - bool isToken = true; - while (current < input.Length) + int index = slice.IndexOfAny(s_hostDelimiterChars); + if (index >= 0) { - char c = input[current]; - if (c == '/') + if (index == 0) { - return 0; // Host header must not contain paths. + return 0; } - if ((c == ' ') || (c == '\t') || (c == '\r') || (c == ',')) + if (slice[index] == '/') { - break; // We hit a delimiter (',' or whitespace). Stop here. + return 0; // Host header must not contain paths. } - isToken = isToken && IsTokenChar(c); - - current++; + slice = slice.Slice(0, index); } - int length = current - startIndex; - if (length == 0) + if ((allowToken && IsToken(slice)) || IsValidHostName(slice)) { - return 0; - } - - if ((!allowToken || !isToken) && !IsValidHostName(input.AsSpan(startIndex, length))) - { - return 0; + return slice.Length; } - return length; + return 0; } internal static HttpParseResult GetCommentLength(string input, int startIndex, out int length) diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HttpRuleParserTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HttpRuleParserTest.cs index 35b3f607680874..29e49940251073 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/HttpRuleParserTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/HttpRuleParserTest.cs @@ -41,16 +41,18 @@ public static IEnumerable InvalidTokenCharsArguments [Theory] [MemberData(nameof(ValidTokenCharsArguments))] - public void IsTokenChar_ValidTokenChars_ConsideredValid(char token) + public void IsToken_ValidTokenChars_ConsideredValid(char token) { - Assert.True(HttpRuleParser.IsTokenChar(token)); + Assert.True(HttpRuleParser.IsToken(stackalloc[] { token })); + Assert.True(HttpRuleParser.IsToken(new ReadOnlySpan((byte)token))); } [Theory] [MemberData(nameof(InvalidTokenCharsArguments))] - public void IsTokenChar_InvalidTokenChars_ConsideredInvalid(char token) + public void IsToken_InvalidTokenChars_ConsideredInvalid(char token) { - Assert.False(HttpRuleParser.IsTokenChar(token)); + Assert.False(HttpRuleParser.IsToken(stackalloc[] { token })); + Assert.False(HttpRuleParser.IsToken(new ReadOnlySpan((byte)token))); } [Fact] diff --git a/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs b/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs index ee319ca338d5f1..12ca4d8ef80cc2 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/Cookie.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.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -41,7 +42,7 @@ public sealed class Cookie internal static readonly char[] PortSplitDelimiters = new char[] { ' ', ',', '\"' }; // Space (' ') should be reserved as well per RFCs, but major web browsers support it and some web sites use it - so we support it too - internal const string ReservedToName = "\t\r\n=;,"; + private static readonly IndexOfAnyValues s_reservedToNameChars = IndexOfAnyValues.Create("\t\r\n=;,"); private string m_comment = string.Empty; // Do not rename (binary serialization) private Uri? m_commentUri; // Do not rename (binary serialization) @@ -238,7 +239,7 @@ internal bool InternalSetName(string? value) || value.StartsWith('$') || value.StartsWith(' ') || value.EndsWith(' ') - || value.AsSpan().IndexOfAny(ReservedToName) >= 0) + || value.AsSpan().IndexOfAny(s_reservedToNameChars) >= 0) { m_name = string.Empty; return false; @@ -346,7 +347,7 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma m_name.StartsWith('$') || m_name.StartsWith(' ') || m_name.EndsWith(' ') || - m_name.AsSpan().IndexOfAny(ReservedToName) >= 0) + m_name.AsSpan().IndexOfAny(s_reservedToNameChars) >= 0) { if (shouldThrow) {