Skip to content

Commit d7c107b

Browse files
authored
Add static char helpers like IsAsciiHexDigit (#8417)
## Summary of changes Vendors standard .NET 7+ `char`/ASCII helpers, and uses them where appropriate ## Reason for change Was duplicating these standard methods in various places, I was going to be adding more, seemed a good time to explicitly vendor these properly. ## Implementation details - Based on static `char` helpers methods that are available in .NET 7+ - Created as static extension methods, so they act as true pollyfills - Replace the handrolled versions - Replace similar methods - this isn't exhaustive, and there are some potential alternative places, but they're used in places where we pattern match explicitly, and didn't want to get too much into refactoring. We can address them later as we see fit. - _Didn't_ mark these for agressive inlining, because they're not marked as such in the runtime, and I trust the .NET team more than my own gut 😄 ## Test coverage Added a few simple "exhaustive" unit tests for the behaviour. ## Other details Pre-requisite for client-side-stats improvements. Part of a stack
1 parent f9e8d09 commit d7c107b

File tree

7 files changed

+181
-35
lines changed

7 files changed

+181
-35
lines changed

tracer/src/Datadog.Trace/Ci/CiEnvironment/CIEnvironmentValues.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,7 @@ protected static bool IsHex(IEnumerable<char> chars)
191191
{
192192
foreach (var c in chars)
193193
{
194-
var isHex = (c is >= '0' and <= '9' ||
195-
c is >= 'a' and <= 'f' ||
196-
c is >= 'A' and <= 'F');
197-
198-
if (!isHex)
194+
if (!char.IsAsciiHexDigit(c))
199195
{
200196
return false;
201197
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/IbmMq/IbmMqHeadersAdapter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#nullable enable
77

8+
using System;
89
using System.Collections.Generic;
910
using System.Text;
1011
using Datadog.Trace.Headers;
@@ -32,7 +33,7 @@ private static string NormalizeName(string name)
3233
var sb = StringBuilderCache.Acquire(name.Length);
3334
foreach (var c in name)
3435
{
35-
sb.Append(c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9') ? c : '_');
36+
sb.Append(char.IsAsciiLetterOrDigit(c) ? c : '_');
3637
}
3738

3839
return StringBuilderCache.GetStringAndRelease(sb);

tracer/src/Datadog.Trace/Processors/TraceUtil.cs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6+
using System;
67
using System.Collections.Generic;
78
using System.Runtime.CompilerServices;
89
using System.Text;
@@ -210,20 +211,6 @@ public static string NormalizeTag(string value)
210211
return new string(segment.Array, segment.Offset, segment.Count);
211212
}
212213

213-
// https://github.com/DataDog/datadog-agent/blob/eac2327c5574da7f225f9ef0f89eaeb05ed10382/pkg/trace/traceutil/normalize.go#L213-L216
214-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
215-
public static bool IsAlpha(char c)
216-
{
217-
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
218-
}
219-
220-
// https://github.com/DataDog/datadog-agent/blob/eac2327c5574da7f225f9ef0f89eaeb05ed10382/pkg/trace/traceutil/normalize.go#L218-L221
221-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
222-
public static bool IsAlphaNumeric(char c)
223-
{
224-
return IsAlpha(c) || (c >= '0' && c <= '9');
225-
}
226-
227214
// https://github.com/DataDog/datadog-agent/blob/eac2327c5574da7f225f9ef0f89eaeb05ed10382/pkg/trace/traceutil/normalize.go#L223-L274
228215
public static string NormalizeMetricName(string name, int limit)
229216
{
@@ -236,7 +223,7 @@ public static string NormalizeMetricName(string name, int limit)
236223
int i = 0;
237224

238225
// skip non-alphabetic characters
239-
for (; i < name.Length && !IsAlpha(name[i]); i++) { }
226+
for (; i < name.Length && !char.IsAsciiLetter(name[i]); i++) { }
240227

241228
// if there were no alphabetic characters it wasn't valid
242229
if (i == name.Length)
@@ -248,7 +235,7 @@ public static string NormalizeMetricName(string name, int limit)
248235
{
249236
char c = name[i];
250237

251-
if (IsAlphaNumeric(c))
238+
if (char.IsAsciiLetterOrDigit(c))
252239
{
253240
sb.Append(c);
254241
}

tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationPath.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static RemoteConfigurationPath FromPath(string path)
5252
// Verify all characters between are digits
5353
for (var i = digitStart; i < secondSlash; i++)
5454
{
55-
if (path[i] is < '0' or > '9')
55+
if (!char.IsAsciiDigit(path[i]))
5656
{
5757
ThrowHelper.ThrowException($"Error parsing path: {path}");
5858
}

tracer/src/Datadog.Trace/Telemetry/Collectors/DependencyTelemetryCollector.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ private static bool IsHexString(string assemblyName, int start)
169169
{
170170
for (int i = assemblyName.Length - 1; i >= start; i--)
171171
{
172-
if (!IsHexChar(assemblyName[i]))
172+
if (!char.IsAsciiHexDigit(assemblyName[i]))
173173
{
174174
return false;
175175
}
@@ -194,16 +194,6 @@ private static bool IsGuid(string assemblyName)
194194
}
195195
}
196196

197-
private static bool IsHexChar(char c)
198-
{
199-
return c switch
200-
{
201-
>= '0' and <= '9' => true,
202-
>= 'a' and <= 'f' => true,
203-
_ => false
204-
};
205-
}
206-
207197
private static bool IsZeroVersionAssemblyPattern(string assemblyName)
208198
{
209199
// e.g.
@@ -255,7 +245,7 @@ private static bool IsDynamicClassesPattern(string assemblyName)
255245
// Verify remaining characters are all digits
256246
for (int i = 14; i < assemblyName.Length; i++)
257247
{
258-
if (assemblyName[i] is < '0' or > '9')
248+
if (!char.IsAsciiDigit(assemblyName[i]))
259249
{
260250
return false;
261251
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// <copyright file="CharHelpers.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
// Based on https://github.com/dotnet/runtime/blob/v10.0.105/src/libraries/System.Private.CoreLib/src/System/Char.cs
7+
8+
using Datadog.Trace.Util;
9+
10+
#if !NET7_0_OR_GREATER
11+
12+
#nullable enable
13+
14+
// Putting these in System, because they are polyfills for methods available directly on char in newer frameworks
15+
16+
// ReSharper disable once CheckNamespace
17+
namespace System;
18+
19+
internal static class CharHelpers
20+
{
21+
extension(char)
22+
{
23+
/// <summary>Indicates whether a character is categorized as an ASCII letter.</summary>
24+
/// <param name="c">The character to evaluate.</param>
25+
/// <returns>true if <paramref name="c"/> is an ASCII letter; otherwise, false.</returns>
26+
/// <remarks>
27+
/// This determines whether the character is in the range 'A' through 'Z', inclusive,
28+
/// or 'a' through 'z', inclusive.
29+
/// </remarks>
30+
public static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a';
31+
32+
/// <summary>Indicates whether a character is categorized as an ASCII digit.</summary>
33+
/// <param name="c">The character to evaluate.</param>
34+
/// <returns>true if <paramref name="c"/> is an ASCII digit; otherwise, false.</returns>
35+
/// <remarks>
36+
/// This determines whether the character is in the range '0' through '9', inclusive.
37+
/// </remarks>
38+
public static bool IsAsciiDigit(char c) => IsBetween(c, '0', '9');
39+
40+
/// <summary>Indicates whether a character is categorized as an ASCII letter or digit.</summary>
41+
/// <param name="c">The character to evaluate.</param>
42+
/// <returns>true if <paramref name="c"/> is an ASCII letter or digit; otherwise, false.</returns>
43+
/// <remarks>
44+
/// This determines whether the character is in the range 'A' through 'Z', inclusive,
45+
/// 'a' through 'z', inclusive, or '0' through '9', inclusive.
46+
/// </remarks>
47+
public static bool IsAsciiLetterOrDigit(char c) => IsAsciiLetter(c) | IsBetween(c, '0', '9');
48+
49+
/// <summary>Indicates whether a character is categorized as an ASCII hexadecimal digit.</summary>
50+
/// <param name="c">The character to evaluate.</param>
51+
/// <returns>true if <paramref name="c"/> is a hexadecimal digit; otherwise, false.</returns>
52+
/// <remarks>
53+
/// This determines whether the character is in the range '0' through '9', inclusive,
54+
/// 'A' through 'F', inclusive, or 'a' through 'f', inclusive.
55+
/// </remarks>
56+
public static bool IsAsciiHexDigit(char c) => HexConverter.IsHexChar(c);
57+
58+
/// <summary>Indicates whether a character is within the specified inclusive range.</summary>
59+
/// <param name="c">The character to evaluate.</param>
60+
/// <param name="minInclusive">The lower bound, inclusive.</param>
61+
/// <param name="maxInclusive">The upper bound, inclusive.</param>
62+
/// <returns>true if <paramref name="c"/> is within the specified range; otherwise, false.</returns>
63+
/// <remarks>
64+
/// The method does not validate that <paramref name="maxInclusive"/> is greater than or equal
65+
/// to <paramref name="minInclusive"/>. If <paramref name="maxInclusive"/> is less than
66+
/// <paramref name="minInclusive"/>, the behavior is undefined.
67+
/// </remarks>
68+
public static bool IsBetween(char c, char minInclusive, char maxInclusive) =>
69+
(uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive);
70+
}
71+
}
72+
#endif
73+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// <copyright file="CharHelpersTests.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
// Based on https://github.com/dotnet/runtime/blob/v10.0.105/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/CharTests.cs
7+
8+
using System;
9+
using System.Linq;
10+
using FluentAssertions;
11+
using Xunit;
12+
13+
namespace Datadog.Trace.Tests.Util;
14+
15+
public class CharHelpersTests
16+
{
17+
private static readonly char[] UppercaseLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
18+
private static readonly char[] LowercaseLetters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
19+
private static readonly char[] Digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
20+
private static readonly char[] HexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'];
21+
22+
[Fact]
23+
public static void IsAsciiLetter()
24+
{
25+
for (int i = char.MinValue; i <= char.MaxValue; i++)
26+
{
27+
var c = (char)i;
28+
29+
var expected = UppercaseLetters.Contains(c) || LowercaseLetters.Contains(c);
30+
31+
CharHelpers.IsAsciiLetter(c).Should().Be(expected);
32+
}
33+
}
34+
35+
[Fact]
36+
public static void IsAsciiLetterOrDigit()
37+
{
38+
for (int i = char.MinValue; i <= char.MaxValue; i++)
39+
{
40+
var c = (char)i;
41+
42+
var expected = UppercaseLetters.Contains(c) || LowercaseLetters.Contains(c) || Digits.Contains(c);
43+
44+
CharHelpers.IsAsciiLetterOrDigit(c).Should().Be(expected);
45+
}
46+
}
47+
48+
[Fact]
49+
public static void IsAsciiDigit()
50+
{
51+
for (int i = char.MinValue; i <= char.MaxValue; i++)
52+
{
53+
var c = (char)i;
54+
55+
var expected = Digits.Contains(c);
56+
57+
CharHelpers.IsAsciiDigit(c).Should().Be(expected);
58+
}
59+
}
60+
61+
[Fact]
62+
public static void IsAsciiHexDigit()
63+
{
64+
for (int i = char.MinValue; i <= char.MaxValue; i++)
65+
{
66+
var c = (char)i;
67+
68+
var expected = HexDigits.Contains(c);
69+
70+
CharHelpers.IsAsciiHexDigit(c).Should().Be(expected);
71+
}
72+
}
73+
74+
[Theory]
75+
[InlineData('a', 'a', 'a', true)]
76+
[InlineData((char)('a' - 1), 'a', 'a', false)]
77+
[InlineData((char)('a' + 1), 'a', 'a', false)]
78+
[InlineData('a', 'a', 'b', true)]
79+
[InlineData('b', 'a', 'b', true)]
80+
[InlineData((char)('a' - 1), 'a', 'b', false)]
81+
[InlineData((char)('b' + 1), 'a', 'b', false)]
82+
[InlineData('a', 'a', 'z', true)]
83+
[InlineData('m', 'a', 'z', true)]
84+
[InlineData('z', 'a', 'z', true)]
85+
[InlineData((char)('a' - 1), 'a', 'z', false)]
86+
[InlineData((char)('z' + 1), 'a', 'z', false)]
87+
[InlineData('\0', '\0', '\uFFFF', true)]
88+
[InlineData('\u1234', '\0', '\uFFFF', true)]
89+
[InlineData('\uFFFF', '\0', '\uFFFF', true)]
90+
[InlineData('\u1234', '\u0123', '\u2345', true)]
91+
[InlineData('\u1234', '\u2345', '\uFFFF', false)]
92+
[InlineData('\u1234', '\u0123', '\u1233', false)]
93+
[InlineData('\u1234', '\u0123', '\u1234', true)]
94+
[InlineData('b', 'c', 'd', false)]
95+
public static void IsBetween_Char(char c, char minInclusive, char maxInclusive, bool expected)
96+
{
97+
CharHelpers.IsBetween(c, minInclusive, maxInclusive).Should().Be(expected);
98+
}
99+
}

0 commit comments

Comments
 (0)