Skip to content

Remove unsafe code from Http KnownHeaders #114757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 19, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

using System.Net.Http.HPack;
using System.Net.Http.QPack;
using System.Runtime.InteropServices;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;

namespace System.Net.Http.Headers
Expand Down Expand Up @@ -114,63 +115,29 @@ internal static class KnownHeaders
private static AltSvcHeaderParser? GetAltSvcHeaderParser() => AltSvcHeaderParser.Parser;
#endif

// Helper interface for making GetCandidate generic over strings, utf8, etc
private interface IHeaderNameAccessor
{
int Length { get; }
char this[int index] { get; }
}

private readonly struct StringAccessor : IHeaderNameAccessor
{
private readonly string _string;

public StringAccessor(string s)
{
_string = s;
}

public int Length => _string.Length;
public char this[int index] => _string[index];
}

// Can't use Span here as it's unsupported.
private readonly unsafe struct BytePtrAccessor : IHeaderNameAccessor
{
private readonly byte* _p;
private readonly int _length;

public BytePtrAccessor(byte* p, int length)
{
_p = p;
_length = length;
}

public int Length => _length;
public char this[int index] => (char)_p[index];
}

/// <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
private static KnownHeader? GetCandidate<T>(ReadOnlySpan<T> key)
where T : struct, INumberBase<T>
{
// 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)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int GetLower(T value) => int.CreateTruncating(value) | 0x20;

switch (key.Length)
{
case 2:
return TE; // TE

case 3:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'a': return Age; // [A]ge
case 'p': return P3P; // [P]3P
Expand All @@ -180,7 +147,7 @@ public BytePtrAccessor(byte* p, int length)
break;

case 4:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'd': return Date; // [D]ate
case 'e': return ETag; // [E]Tag
Expand All @@ -192,15 +159,15 @@ public BytePtrAccessor(byte* p, int length)
break;

case 5:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'a': return Allow; // [A]llow
case 'r': return Range; // [R]ange
}
break;

case 6:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'a': return Accept; // [A]ccept
case 'c': return Cookie; // [C]ookie
Expand All @@ -212,14 +179,14 @@ public BytePtrAccessor(byte* p, int length)
break;

case 7:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case ':': return PseudoStatus; // [:]status
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)
switch (GetLower(key[3]))
{
case 'e': return Referer; // [R]ef[e]rer
case 'r': return Refresh; // [R]ef[r]esh
Expand All @@ -233,7 +200,7 @@ public BytePtrAccessor(byte* p, int length)
break;

case 8:
switch (key[3] | 0x20)
switch (GetLower(key[3]))
{
case '-': return AltUsed; // Alt[-]Used
case 'a': return Location; // Loc[a]tion
Expand All @@ -246,7 +213,7 @@ public BytePtrAccessor(byte* p, int length)
return ExpectCT; // Expect-CT

case 10:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'c': return Connection; // [C]onnection
case 'k': return KeepAlive; // [K]eep-Alive
Expand All @@ -256,7 +223,7 @@ public BytePtrAccessor(byte* p, int length)
break;

case 11:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'c': return ContentMD5; // [C]ontent-MD5
case 'g': return GrpcStatus; // [g]rpc-status
Expand All @@ -266,7 +233,7 @@ public BytePtrAccessor(byte* p, int length)
break;

case 12:
switch (key[5] | 0x20)
switch (GetLower(key[5]))
{
case 'd': return XMSEdgeRef; // X-MSE[d]ge-Ref
case 'e': return XPoweredBy; // X-Pow[e]red-By
Expand All @@ -279,12 +246,12 @@ public BytePtrAccessor(byte* p, int length)
break;

case 13:
switch (key[12] | 0x20)
switch (GetLower(key[12]))
{
case 'd': return LastModified; // Last-Modifie[d]
case 'e': return ContentRange; // Content-Rang[e]
case 'g':
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 's': return ServerTiming; // [S]erver-Timin[g]
case 'g': return GrpcEncoding; // [g]rpc-encodin[g]
Expand All @@ -299,15 +266,15 @@ public BytePtrAccessor(byte* p, int length)
break;

case 14:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'a': return AcceptCharset; // [A]ccept-Charset
case 'c': return ContentLength; // [C]ontent-Length
}
break;

case 15:
switch (key[7] | 0x20)
switch (GetLower(key[7]))
{
case '-': return XFrameOptions; // X-Frame[-]Options
case 'e': return AcceptEncoding; // Accept-[E]ncoding
Expand All @@ -319,11 +286,11 @@ public BytePtrAccessor(byte* p, int length)
break;

case 16:
switch (key[11] | 0x20)
switch (GetLower(key[11]))
{
case 'a': return ContentLocation; // Content-Loc[a]tion
case 'c':
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'p': return ProxyConnection; // [P]roxy-Conne[c]tion
case 'x': return XXssProtection; // [X]-XSS-Prote[c]tion
Expand All @@ -337,7 +304,7 @@ public BytePtrAccessor(byte* p, int length)
break;

case 17:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'i': return IfModifiedSince; // [I]f-Modified-Since
case 's': return SecWebSocketKey; // [S]ec-WebSocket-Key
Expand All @@ -346,15 +313,15 @@ public BytePtrAccessor(byte* p, int length)
break;

case 18:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'p': return ProxyAuthenticate; // [P]roxy-Authenticate
case 'x': return XContentDuration; // [X]-Content-Duration
}
break;

case 19:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'c': return ContentDisposition; // [C]ontent-Disposition
case 'i': return IfUnmodifiedSince; // [I]f-Unmodified-Since
Expand All @@ -369,7 +336,7 @@ public BytePtrAccessor(byte* p, int length)
return SecWebSocketVersion; // Sec-WebSocket-Version

case 22:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 'a': return AccessControlMaxAge; // [A]ccess-Control-Max-Age
case 's': return SecWebSocketProtocol; // [S]ec-WebSocket-Protocol
Expand All @@ -384,7 +351,7 @@ public BytePtrAccessor(byte* p, int length)
return SecWebSocketExtensions; // Sec-WebSocket-Extensions

case 25:
switch (key[0] | 0x20)
switch (GetLower(key[0]))
{
case 's': return StrictTransportSecurity; // [S]trict-Transport-Security
case 'u': return UpgradeInsecureRequests; // [U]pgrade-Insecure-Requests
Expand All @@ -395,7 +362,7 @@ public BytePtrAccessor(byte* p, int length)
return AccessControlAllowOrigin; // Access-Control-Allow-Origin

case 28:
switch (key[21] | 0x20)
switch (GetLower(key[21]))
{
case 'h': return AccessControlAllowHeaders; // Access-Control-Allow-[H]eaders
case 'm': return AccessControlAllowMethods; // Access-Control-Allow-[M]ethods
Expand All @@ -412,9 +379,9 @@ public BytePtrAccessor(byte* p, int length)
return null;
}

internal static KnownHeader? TryGetKnownHeader(string name)
public static KnownHeader? TryGetKnownHeader(string name)
{
KnownHeader? candidate = GetCandidate(new StringAccessor(name));
KnownHeader? candidate = GetCandidate<char>(name);
if (candidate != null && StringComparer.OrdinalIgnoreCase.Equals(name, candidate.Name))
{
return candidate;
Expand All @@ -423,15 +390,12 @@ public BytePtrAccessor(byte* p, int length)
return null;
}

internal static unsafe KnownHeader? TryGetKnownHeader(ReadOnlySpan<byte> name)
public static KnownHeader? TryGetKnownHeader(ReadOnlySpan<byte> name)
{
fixed (byte* p = &MemoryMarshal.GetReference(name))
KnownHeader? candidate = GetCandidate(name);
if (candidate != null && Ascii.EqualsIgnoreCase(name, candidate.Name))
{
KnownHeader? candidate = GetCandidate(new BytePtrAccessor(p, name.Length));
if (candidate != null && Ascii.EqualsIgnoreCase(name, candidate.Name))
{
return candidate;
}
return candidate;
}

return null;
Expand Down
Loading