Simplify KnownHeaders lookup in SocketsHttpHandler (#34974)
authorStephen Toub <stoub@microsoft.com>
Wed, 15 Apr 2020 17:13:37 +0000 (13:13 -0400)
committerGitHub <noreply@github.com>
Wed, 15 Apr 2020 17:13:37 +0000 (13:13 -0400)
* Simplify KnownHeaders lookup

* Update src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs

Co-Authored-By: David Shulman <david.shulman@microsoft.com>
Co-authored-by: David Shulman <david.shulman@microsoft.com>
src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs
src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj

index 654765e..0708e8e 100644 (file)
@@ -141,13 +141,19 @@ namespace System.Net.Http.Headers
             public char this[int index] => (char)_p[index];
         }
 
-        // Find possible known header match via lookup on length and a distinguishing char for that length.
-        // Matching is case-insenstive.
-        // NOTE: Because of this, we do not preserve the case of the original header,
-        // whether from the wire or from the user explicitly setting a known header using a header name string.
+        /// <summary>
+        /// Find possible known header match via lookup on length and a distinguishing char for that length.
+        /// </summary>
+        /// <remarks>
+        /// Matching is case-insensitive. Because of this, we do not preserve the case of the original header,
+        /// whether from the wire or from the user explicitly setting a known header using a header name string.
+        /// </remarks>
         private static KnownHeader? GetCandidate<T>(T key)
             where T : struct, IHeaderNameAccessor     // Enforce struct for performance
         {
+            // Lookup is performed by first switching on the header name's length, and then switching
+            // on the most unique position in that length's string.
+
             int length = key.Length;
             switch (length)
             {
@@ -155,190 +161,187 @@ namespace System.Net.Http.Headers
                     return TE; // TE
 
                 case 3:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'A': case 'a': return Age; // [A]ge
-                        case 'P': case 'p': return P3P; // [P]3P
-                        case 'T': case 't': return TSV; // [T]SV
-                        case 'V': case 'v': return Via; // [V]ia
+                        case 'a': return Age; // [A]ge
+                        case 'p': return P3P; // [P]3P
+                        case 't': return TSV; // [T]SV
+                        case 'v': return Via; // [V]ia
                     }
                     break;
 
                 case 4:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'D': case 'd': return Date; // [D]ate
-                        case 'E': case 'e': return ETag; // [E]Tag
-                        case 'F': case 'f': return From; // [F]rom
-                        case 'H': case 'h': return Host; // [H]ost
-                        case 'L': case 'l': return Link; // [L]ink
-                        case 'V': case 'v': return Vary; // [V]ary
+                        case 'd': return Date; // [D]ate
+                        case 'e': return ETag; // [E]Tag
+                        case 'f': return From; // [F]rom
+                        case 'h': return Host; // [H]ost
+                        case 'l': return Link; // [L]ink
+                        case 'v': return Vary; // [V]ary
                     }
                     break;
 
                 case 5:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'A': case 'a': return Allow; // [A]llow
-                        case 'R': case 'r': return Range; // [R]ange
+                        case 'a': return Allow; // [A]llow
+                        case 'r': return Range; // [R]ange
                     }
                     break;
 
                 case 6:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'A': case 'a': return Accept; // [A]ccept
-                        case 'C': case 'c': return Cookie; // [C]ookie
-                        case 'E': case 'e': return Expect; // [E]xpect
-                        case 'O': case 'o': return Origin; // [O]rigin
-                        case 'P': case 'p': return Pragma; // [P]ragma
-                        case 'S': case 's': return Server; // [S]erver
+                        case 'a': return Accept; // [A]ccept
+                        case 'c': return Cookie; // [C]ookie
+                        case 'e': return Expect; // [E]xpect
+                        case 'o': return Origin; // [O]rigin
+                        case 'p': return Pragma; // [P]ragma
+                        case 's': return Server; // [S]erver
                     }
                     break;
 
                 case 7:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
                         case ':': return PseudoStatus; // [:]status
-                        case 'A': case 'a': return AltSvc;  // [A]lt-Svc
-                        case 'C': case 'c': return Cookie2; // [C]ookie2
-                        case 'E': case 'e': return Expires; // [E]xpires
-                        case 'R': case 'r':
-                            switch (key[3])
+                        case 'a': return AltSvc;  // [A]lt-Svc
+                        case 'c': return Cookie2; // [C]ookie2
+                        case 'e': return Expires; // [E]xpires
+                        case 'r':
+                            switch (key[3] | 0x20)
                             {
-                                case 'E': case 'e': return Referer; // [R]ef[e]rer
-                                case 'R': case 'r': return Refresh; // [R]ef[r]esh
+                                case 'e': return Referer; // [R]ef[e]rer
+                                case 'r': return Refresh; // [R]ef[r]esh
                             }
                             break;
-                        case 'T': case 't': return Trailer; // [T]railer
-                        case 'U': case 'u': return Upgrade; // [U]pgrade
-                        case 'W': case 'w': return Warning; // [W]arning
-                        case 'X': case 'x': return XCache;  // [X]-Cache
+                        case 't': return Trailer; // [T]railer
+                        case 'u': return Upgrade; // [U]pgrade
+                        case 'w': return Warning; // [W]arning
+                        case 'x': return XCache;  // [X]-Cache
                     }
                     break;
 
                 case 8:
-                    switch (key[3])
+                    switch (key[3] | 0x20)
                     {
-                        case 'M': case 'm': return IfMatch;  // If-[M]atch
-                        case 'R': case 'r': return IfRange;  // If-[R]ange
-                        case 'A': case 'a': return Location; // Loc[a]tion
+                        case '-': return AltUsed;  // Alt[-]Used
+                        case 'a': return Location; // Loc[a]tion
+                        case 'm': return IfMatch;  // If-[M]atch
+                        case 'r': return IfRange;  // If-[R]ange
                     }
                     break;
 
                 case 9:
-                    switch (key[0])
-                    {
-                        case 'E': case 'e': return ExpectCT; // [E]xpect-CT
-                    }
-                    break;
+                    return ExpectCT; // Expect-CT
 
                 case 10:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'C': case 'c': return Connection; // [C]onnection
-                        case 'K': case 'k': return KeepAlive;  // [K]eep-Alive
-                        case 'S': case 's': return SetCookie;  // [S]et-Cookie
-                        case 'U': case 'u': return UserAgent;  // [U]ser-Agent
+                        case 'c': return Connection; // [C]onnection
+                        case 'k': return KeepAlive;  // [K]eep-Alive
+                        case 's': return SetCookie;  // [S]et-Cookie
+                        case 'u': return UserAgent;  // [U]ser-Agent
                     }
                     break;
 
                 case 11:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'C': case 'c': return ContentMD5; // [C]ontent-MD5
-                        case 'R': case 'r': return RetryAfter; // [R]etry-After
-                        case 'S': case 's': return SetCookie2; // [S]et-Cookie2
+                        case 'c': return ContentMD5; // [C]ontent-MD5
+                        case 'r': return RetryAfter; // [R]etry-After
+                        case 's': return SetCookie2; // [S]et-Cookie2
                     }
                     break;
 
                 case 12:
-                    switch (key[2])
+                    switch (key[2] | 0x20)
                     {
-                        case 'C': case 'c': return AcceptPatch; // Ac[c]ept-Patch
-                        case 'N': case 'n': return ContentType; // Co[n]tent-Type
-                        case 'X': case 'x': return MaxForwards; // Ma[x]-Forwards
-                        case 'M': case 'm': return XMSEdgeRef;  // X-[M]SEdge-Ref
-                        case 'P': case 'p': return XPoweredBy;  // X-[P]owered-By
-                        case 'R': case 'r': return XRequestID;  // X-[R]equest-ID
+                        case 'c': return AcceptPatch; // Ac[c]ept-Patch
+                        case 'm': return XMSEdgeRef;  // X-[M]SEdge-Ref
+                        case 'n': return ContentType; // Co[n]tent-Type
+                        case 'p': return XPoweredBy;  // X-[P]owered-By
+                        case 'r': return XRequestID;  // X-[R]equest-ID
+                        case 'x': return MaxForwards; // Ma[x]-Forwards
                     }
                     break;
 
                 case 13:
-                    switch (key[12])
+                    switch (key[12] | 0x20)
                     {
-                        case 'S': case 's': return AcceptRanges;  // Accept-Range[s]
-                        case 'N': case 'n': return Authorization; // Authorizatio[n]
-                        case 'L': case 'l': return CacheControl;  // Cache-Contro[l]
-                        case 'E': case 'e': return ContentRange;  // Content-Rang[e]
-                        case 'H': case 'h': return IfNoneMatch;   // If-None-Matc[h]
-                        case 'D': case 'd': return LastModified;  // Last-Modifie[d]
-                        case 'T': case 't': return ProxySupport;  // Proxy-Suppor[t]
-                        case 'G': case 'g': return ServerTiming;  // Server-Timin[g]
+                        case 'd': return LastModified;  // Last-Modifie[d]
+                        case 'e': return ContentRange;  // Content-Rang[e]
+                        case 'g': return ServerTiming;  // Server-Timin[g]
+                        case 'h': return IfNoneMatch;   // If-None-Matc[h]
+                        case 'l': return CacheControl;  // Cache-Contro[l]
+                        case 'n': return Authorization; // Authorizatio[n]
+                        case 's': return AcceptRanges;  // Accept-Range[s]
+                        case 't': return ProxySupport;  // Proxy-Suppor[t]
                     }
                     break;
 
                 case 14:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'A': case 'a': return AcceptCharset; // [A]ccept-Charset
-                        case 'C': case 'c': return ContentLength; // [C]ontent-Length
+                        case 'a': return AcceptCharset; // [A]ccept-Charset
+                        case 'c': return ContentLength; // [C]ontent-Length
                     }
                     break;
 
                 case 15:
-                    switch (key[7])
+                    switch (key[7] | 0x20)
                     {
                         case '-': return XFrameOptions;  // X-Frame[-]Options
-                        case 'M': case 'm': return XUACompatible;  // X-UA-Co[m]patible
-                        case 'E': case 'e': return AcceptEncoding; // Accept-[E]ncoding
-                        case 'K': case 'k': return PublicKeyPins;  // Public-[K]ey-Pins
-                        case 'L': case 'l': return AcceptLanguage; // Accept-[L]anguage
-                        case 'R': case 'r': return ReferrerPolicy; // Referre[r]-Policy
+                        case 'e': return AcceptEncoding; // Accept-[E]ncoding
+                        case 'k': return PublicKeyPins;  // Public-[K]ey-Pins
+                        case 'l': return AcceptLanguage; // Accept-[L]anguage
+                        case 'm': return XUACompatible;  // X-UA-Co[m]patible
+                        case 'r': return ReferrerPolicy; // Referre[r]-Policy
                     }
                     break;
 
                 case 16:
-                    switch (key[11])
+                    switch (key[11] | 0x20)
                     {
-                        case 'O': case 'o': return ContentEncoding; // Content-Enc[o]ding
-                        case 'G': case 'g': return ContentLanguage; // Content-Lan[g]uage
-                        case 'A': case 'a': return ContentLocation; // Content-Loc[a]tion
-                        case 'C': case 'c':
-                            switch (key[0])
+                        case 'a': return ContentLocation; // Content-Loc[a]tion
+                        case 'c':
+                            switch (key[0] | 0x20)
                             {
-                                case 'P': case 'p': return ProxyConnection; // [P]roxy-Conne[c]tion
-                                case 'X': case 'x': return XXssProtection;  // [X]-XSS-Prote[c]tion
+                                case 'p': return ProxyConnection; // [P]roxy-Conne[c]tion
+                                case 'x': return XXssProtection;  // [X]-XSS-Prote[c]tion
                             }
                             break;
-                        case 'I': case 'i': return WWWAuthenticate; // WWW-Authent[i]cate
-                        case 'R': case 'r': return XAspNetVersion;  // X-AspNet-Ve[r]sion
+                        case 'g': return ContentLanguage; // Content-Lan[g]uage
+                        case 'i': return WWWAuthenticate; // WWW-Authent[i]cate
+                        case 'o': return ContentEncoding; // Content-Enc[o]ding
+                        case 'r': return XAspNetVersion;  // X-AspNet-Ve[r]sion
                     }
                     break;
 
                 case 17:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'I': case 'i': return IfModifiedSince;  // [I]f-Modified-Since
-                        case 'S': case 's': return SecWebSocketKey;  // [S]ec-WebSocket-Key
-                        case 'T': case 't': return TransferEncoding; // [T]ransfer-Encoding
+                        case 'i': return IfModifiedSince;  // [I]f-Modified-Since
+                        case 's': return SecWebSocketKey;  // [S]ec-WebSocket-Key
+                        case 't': return TransferEncoding; // [T]ransfer-Encoding
                     }
                     break;
 
                 case 18:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'P': case 'p': return ProxyAuthenticate; // [P]roxy-Authenticate
-                        case 'X': case 'x': return XContentDuration;  // [X]-Content-Duration
+                        case 'p': return ProxyAuthenticate; // [P]roxy-Authenticate
+                        case 'x': return XContentDuration;  // [X]-Content-Duration
                     }
                     break;
 
                 case 19:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'C': case 'c': return ContentDisposition; // [C]ontent-Disposition
-                        case 'I': case 'i': return IfUnmodifiedSince;  // [I]f-Unmodified-Since
-                        case 'P': case 'p': return ProxyAuthorization; // [P]roxy-Authorization
+                        case 'c': return ContentDisposition; // [C]ontent-Disposition
+                        case 'i': return IfUnmodifiedSince;  // [I]f-Unmodified-Since
+                        case 'p': return ProxyAuthorization; // [P]roxy-Authorization
                     }
                     break;
 
@@ -349,11 +352,11 @@ namespace System.Net.Http.Headers
                     return SecWebSocketVersion; // Sec-WebSocket-Version
 
                 case 22:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'A': case 'a': return AccessControlMaxAge;  // [A]ccess-Control-Max-Age
-                        case 'S': case 's': return SecWebSocketProtocol; // [S]ec-WebSocket-Protocol
-                        case 'X': case 'x': return XContentTypeOptions;  // [X]-Content-Type-Options
+                        case 'a': return AccessControlMaxAge;  // [A]ccess-Control-Max-Age
+                        case 's': return SecWebSocketProtocol; // [S]ec-WebSocket-Protocol
+                        case 'x': return XContentTypeOptions;  // [X]-Content-Type-Options
                     }
                     break;
 
@@ -364,10 +367,10 @@ namespace System.Net.Http.Headers
                     return SecWebSocketExtensions; // Sec-WebSocket-Extensions
 
                 case 25:
-                    switch (key[0])
+                    switch (key[0] | 0x20)
                     {
-                        case 'S': case 's': return StrictTransportSecurity; // [S]trict-Transport-Security
-                        case 'U': case 'u': return UpgradeInsecureRequests; // [U]pgrade-Insecure-Requests
+                        case 's': return StrictTransportSecurity; // [S]trict-Transport-Security
+                        case 'u': return UpgradeInsecureRequests; // [U]pgrade-Insecure-Requests
                     }
                     break;
 
@@ -375,10 +378,10 @@ namespace System.Net.Http.Headers
                     return AccessControlAllowOrigin; // Access-Control-Allow-Origin
 
                 case 28:
-                    switch (key[21])
+                    switch (key[21] | 0x20)
                     {
-                        case 'H': case 'h': return AccessControlAllowHeaders; // Access-Control-Allow-[H]eaders
-                        case 'M': case 'm': return AccessControlAllowMethods; // Access-Control-Allow-[M]ethods
+                        case 'h': return AccessControlAllowHeaders; // Access-Control-Allow-[H]eaders
+                        case 'm': return AccessControlAllowMethods; // Access-Control-Allow-[M]ethods
                     }
                     break;
 
diff --git a/src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs
new file mode 100644 (file)
index 0000000..b2bd2be
--- /dev/null
@@ -0,0 +1,139 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using System.Net.Http.Headers;
+using Xunit;
+
+namespace System.Net.Http.Tests
+{
+    public class KnownHeadersTest
+    {
+        [Theory]
+        [InlineData(":status")]
+        [InlineData("Accept")]
+        [InlineData("Accept-Charset")]
+        [InlineData("Accept-Encoding")]
+        [InlineData("Accept-Language")]
+        [InlineData("Accept-Patch")]
+        [InlineData("Accept-Ranges")]
+        [InlineData("Access-Control-Allow-Credentials")]
+        [InlineData("Access-Control-Allow-Headers")]
+        [InlineData("Access-Control-Allow-Methods")]
+        [InlineData("Access-Control-Allow-Origin")]
+        [InlineData("Access-Control-Expose-Headers")]
+        [InlineData("Access-Control-Max-Age")]
+        [InlineData("Age")]
+        [InlineData("Allow")]
+        [InlineData("Alt-Svc")]
+        [InlineData("Alt-Used")]
+        [InlineData("Authorization")]
+        [InlineData("Cache-Control")]
+        [InlineData("Connection")]
+        [InlineData("Content-Disposition")]
+        [InlineData("Content-Encoding")]
+        [InlineData("Content-Language")]
+        [InlineData("Content-Length")]
+        [InlineData("Content-Location")]
+        [InlineData("Content-MD5")]
+        [InlineData("Content-Range")]
+        [InlineData("Content-Security-Policy")]
+        [InlineData("Content-Type")]
+        [InlineData("Cookie")]
+        [InlineData("Cookie2")]
+        [InlineData("Date")]
+        [InlineData("ETag")]
+        [InlineData("Expect")]
+        [InlineData("Expect-CT")]
+        [InlineData("Expires")]
+        [InlineData("From")]
+        [InlineData("Host")]
+        [InlineData("If-Match")]
+        [InlineData("If-Modified-Since")]
+        [InlineData("If-None-Match")]
+        [InlineData("If-Range")]
+        [InlineData("If-Unmodified-Since")]
+        [InlineData("Keep-Alive")]
+        [InlineData("Last-Modified")]
+        [InlineData("Link")]
+        [InlineData("Location")]
+        [InlineData("Max-Forwards")]
+        [InlineData("Origin")]
+        [InlineData("P3P")]
+        [InlineData("Pragma")]
+        [InlineData("Proxy-Authenticate")]
+        [InlineData("Proxy-Authorization")]
+        [InlineData("Proxy-Connection")]
+        [InlineData("Proxy-Support")]
+        [InlineData("Public-Key-Pins")]
+        [InlineData("Range")]
+        [InlineData("Referer")]
+        [InlineData("Referrer-Policy")]
+        [InlineData("Refresh")]
+        [InlineData("Retry-After")]
+        [InlineData("Sec-WebSocket-Accept")]
+        [InlineData("Sec-WebSocket-Extensions")]
+        [InlineData("Sec-WebSocket-Key")]
+        [InlineData("Sec-WebSocket-Protocol")]
+        [InlineData("Sec-WebSocket-Version")]
+        [InlineData("Server")]
+        [InlineData("Server-Timing")]
+        [InlineData("Set-Cookie")]
+        [InlineData("Set-Cookie2")]
+        [InlineData("Strict-Transport-Security")]
+        [InlineData("TE")]
+        [InlineData("Trailer")]
+        [InlineData("Transfer-Encoding")]
+        [InlineData("TSV")]
+        [InlineData("Upgrade")]
+        [InlineData("Upgrade-Insecure-Requests")]
+        [InlineData("User-Agent")]
+        [InlineData("Vary")]
+        [InlineData("Via")]
+        [InlineData("Warning")]
+        [InlineData("WWW-Authenticate")]
+        [InlineData("X-AspNet-Version")]
+        [InlineData("X-Cache")]
+        [InlineData("X-Content-Duration")]
+        [InlineData("X-Content-Type-Options")]
+        [InlineData("X-Frame-Options")]
+        [InlineData("X-MSEdge-Ref")]
+        [InlineData("X-Powered-By")]
+        [InlineData("X-Request-ID")]
+        [InlineData("X-UA-Compatible")]
+        [InlineData("X-XSS-Protection")]
+        public void TryGetKnownHeader_Known_Found(string name)
+        {
+            foreach (string casedName in new[] { name, name.ToUpperInvariant(), name.ToLowerInvariant() })
+            {
+                Validate(casedName, KnownHeaders.TryGetKnownHeader(casedName));
+                Validate(casedName, KnownHeaders.TryGetKnownHeader(casedName.Select(c => (byte)c).ToArray()));
+            }
+
+            static void Validate(string name, KnownHeader h)
+            {
+                Assert.NotNull(h);
+                Assert.Same(h, KnownHeaders.TryGetKnownHeader(name));
+
+                Assert.Same(h, h.Descriptor.KnownHeader);
+                Assert.Equal(name, h.Name, StringComparer.OrdinalIgnoreCase);
+                Assert.Equal(name, h.Descriptor.Name, StringComparer.OrdinalIgnoreCase);
+            }
+        }
+
+        [Theory]
+        [InlineData("")]
+        [InlineData(" \t ")]
+        [InlineData("Something")]
+        [InlineData("X-Unknown")]
+        public void TryGetKnownHeader_Unknown_NotFound(string name)
+        {
+            foreach (string casedName in new[] { name, name.ToUpperInvariant(), name.ToLowerInvariant() })
+            {
+                Assert.Null(KnownHeaders.TryGetKnownHeader(casedName));
+                Assert.Null(KnownHeaders.TryGetKnownHeader(casedName.Select(c => (byte)c).ToArray()));
+            }
+        }
+    }
+}
index 33d33d6..f8dc22a 100644 (file)
     <Compile Include="Headers\HttpResponseHeadersTest.cs" />
     <Compile Include="Headers\Int32NumberHeaderParserTest.cs" />
     <Compile Include="Headers\Int64NumberHeaderParserTest.cs" />
+    <Compile Include="Headers\KnownHeadersTest.cs" />
     <Compile Include="Headers\MediaTypeHeaderParserTest.cs" />
     <Compile Include="Headers\MediaTypeHeaderValueTest.cs" />
     <Compile Include="Headers\MediaTypeWithQualityHeaderValueTest.cs" />