diff --git a/src/csharp/Grpc.Core.Tests/Internal/WellKnownStringsTest.cs b/src/csharp/Grpc.Core.Tests/Internal/WellKnownStringsTest.cs new file mode 100644 index 00000000000..f5c8e01a141 --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/Internal/WellKnownStringsTest.cs @@ -0,0 +1,46 @@ +using System.Text; +using Grpc.Core.Internal; +using NUnit.Framework; + +namespace Grpc.Core.Tests.Internal +{ + public class WellKnownStringsTest + { + [Test] + [TestCase("", true)] + [TestCase("u", false)] + [TestCase("us", false)] + [TestCase("use", false)] + [TestCase("user", false)] + [TestCase("user-", false)] + [TestCase("user-a", false)] + [TestCase("user-ag", false)] + [TestCase("user-age", false)] + [TestCase("user-agent", true)] + [TestCase("user-agent ", false)] + [TestCase("useragent ", false)] + [TestCase("User-Agent", false)] + [TestCase("sdlkfjlskjfdlkjs;lfdksflsdfkh skjdfh sdkfhskdhf skjfhk sdhjkjh", false)] + + // test for endianness snafus (reversed in segments) + [TestCase("ega-resutn", false)] + public unsafe void TestWellKnownStrings(string input, bool expected) + { + // create a copy of the data; no cheating! + byte[] bytes = Encoding.ASCII.GetBytes(input); + fixed(byte* ptr = bytes) + { + string result = WellKnownStrings.TryIdentify(ptr, bytes.Length); + if (expected) Assert.AreEqual(input, result); + else Assert.IsNull(result); + + if (expected) + { + // try again, and check we get the same instance + string again = WellKnownStrings.TryIdentify(ptr, bytes.Length); + Assert.AreSame(result, again); + } + } + } + } +} diff --git a/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs b/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs index 1663a9d44ac..cc306891119 100644 --- a/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs @@ -76,39 +76,6 @@ namespace Grpc.Core.Internal return metadata; } - private static class WellKnownStrings - { - // turn a string of ASCII-length 8 into a ulong using the CPUs current - // endianness; this allows us to do the same thing in TryIdentify, - // testing string prefixes (of length >= 8) in a single load/compare - private static unsafe ulong Thunk8(string value) - { - int byteCount = Encoding.ASCII.GetByteCount(value); - if (byteCount != 8) throw new ArgumentException(nameof(value)); - ulong result = 0; - fixed (char* cPtr = value) - { - Encoding.ASCII.GetBytes(cPtr, value.Length, (byte*)&result, byteCount); - } - return result; - } - private static readonly ulong UserAgent = Thunk8("user-age"); - public static unsafe string TryIdentify(byte* source, int length) - { - switch (length) - { - case 10: - // test the first 8 bytes via evilness - ulong first8 = *(ulong*)source; - if (first8 == UserAgent & source[8] == (byte)'n' & source[9] == (byte)'t') - return "user-agent"; - - break; - } - return null; - } - } - internal IntPtr Handle { get diff --git a/src/csharp/Grpc.Core/Internal/WellKnownStrings.cs b/src/csharp/Grpc.Core/Internal/WellKnownStrings.cs new file mode 100644 index 00000000000..1e78ffdab43 --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/WellKnownStrings.cs @@ -0,0 +1,66 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Grpc.Core.Internal +{ + /// + /// Utility type for identifying "well-known" strings (i.e. headers/keys etc that + /// we expect to see frequently, and don't want to allocate lots of copies of) + /// + internal static class WellKnownStrings + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong Coerce64(byte* value) + { + return *(ulong*)value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe uint Coerce32(byte* value) + { + return *(uint*)value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ushort Coerce16(byte* value) + { + return *(ushort*)value; + } + + /// + /// Test whether the provided byte sequence is recognized as a well-known string; if + /// so, return a shared instance of that string; otherwise, return null + /// + public static unsafe string TryIdentify(byte* source, int length) + { + // note: the logic here is hard-coded to constants for optimal processing; + // refer to an ASCII/hex converter (and remember to reverse **segments** for little-endian) + if (BitConverter.IsLittleEndian) // this is a JIT intrinsic; branch removal happens on modern runtimes + { + switch (length) + { + case 0: return ""; + case 10: + switch(Coerce64(source)) + { + case 0x6567612d72657375: return Coerce16(source + 8) == 0x746e ? "user-agent" : null; + } + break; + } + } + else + { + switch (length) + { + case 0: return ""; + case 10: + switch (Coerce64(source)) + { + case 0x757365722d616765: return Coerce16(source + 8) == 0x6e74 ? "user-agent" : null; + } + break; + } + } + return null; + } + } +}