< Summary

Line coverage
81%
Covered lines: 176
Uncovered lines: 40
Coverable lines: 216
Total lines: 442
Line coverage: 81.4%
Branch coverage
90%
Covered branches: 90
Total branches: 100
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParserHelpers.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Text;
 9
 10namespace System.Reflection.Metadata
 11{
 12    internal static class TypeNameParserHelpers
 13    {
 14        internal const int SZArray = -1;
 15        internal const int Pointer = -2;
 16        internal const int ByRef = -3;
 17        private const char EscapeCharacter = '\\';
 18
 119        private static readonly SearchValues<char> s_endOfFullTypeNameDelimiterChars = SearchValues.Create("[]&*,+\\");
 20
 21        /// <returns>Positive length or negative value for invalid name</returns>
 22        internal static int GetFullTypeNameLength(ReadOnlySpan<char> input, out bool isNestedType)
 5617223        {
 5617224            isNestedType = false;
 25
 26            // NET 6+ guarantees that MemoryExtensions.IndexOfAny has worst-case complexity
 27            // O(m * i) if a match is found, or O(m * n) if a match is not found, where:
 28            //   i := index of match position
 29            //   m := number of needles
 30            //   n := length of search space (haystack)
 31            //
 32            // Downlevel versions of .NET do not make this guarantee, instead having a
 33            // worst-case complexity of O(m * n) even if a match occurs at the beginning of
 34            // the search space. Since we're running this in a loop over untrusted user
 35            // input, that makes the total loop complexity potentially O(m * n^2), where
 36            // 'n' is adversary-controlled. To avoid DoS issues here, we'll loop manually.
 5617237            int offset = input.IndexOfAny(s_endOfFullTypeNameDelimiterChars);
 5617238            if (offset < 0)
 129239            {
 129240                return input.Length; // no type name end chars were found, the whole input is the type name
 41            }
 42
 5488043            if (input[offset] == EscapeCharacter) // this is very rare (IL Emit or pure IL)
 408444            {
 408445                offset = GetUnescapedOffset(input, startOffset: offset); // this is slower, but very rare so acceptable
 408446            }
 47
 5488048            isNestedType = offset > 0 && offset < input.Length && input[offset] == '+';
 5488049            return offset;
 50
 51            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startOffset)
 408452            {
 408453                int offset = startOffset;
 3947654                for (; offset < input.Length; offset++)
 2137255                {
 2137256                    char c = input[offset];
 2137257                    if (c == EscapeCharacter)
 816058                    {
 816059                        offset++; // skip the escaped char
 60
 816061                        if (offset == input.Length || // invalid name that ends with escape character
 816062                            !s_endOfFullTypeNameDelimiterChars.Contains(input[offset])) // invalid name, escapes a char 
 7663                        {
 7664                            return -1;
 65                        }
 808466                    }
 1321267                    else if (s_endOfFullTypeNameDelimiterChars.Contains(c))
 360068                    {
 360069                        break;
 70                    }
 1769671                }
 400872                return offset;
 408473            }
 5617274        }
 75
 76        internal static int IndexOfNamespaceDelimiter(ReadOnlySpan<char> fullName)
 718077        {
 78            // Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp
 79            // This could result in the type name beginning with a '.' character.
 718080            int index = fullName.LastIndexOf('.');
 81
 718082            if (index > 0 && fullName[index - 1] == '.')
 483            {
 484                index--;
 485            }
 86
 718087            return index;
 718088        }
 89
 90        internal static string Unescape(string input)
 091        {
 092            int indexOfEscapeCharacter = input.IndexOf(EscapeCharacter);
 093            if (indexOfEscapeCharacter < 0)
 094            {
 95                // Nothing to escape, just return the original value.
 096                return input;
 97            }
 98
 099            return UnescapeToBuilder(input, indexOfEscapeCharacter);
 100
 101            static string UnescapeToBuilder(string name, int indexOfEscapeCharacter)
 0102            {
 103                // This code path is executed very rarely (IL Emit or pure IL with chars not allowed in C# or F#).
 0104                var sb = new ValueStringBuilder(stackalloc char[64]);
 0105                sb.EnsureCapacity(name.Length);
 0106                sb.Append(name.AsSpan(0, indexOfEscapeCharacter));
 107
 0108                for (int i = indexOfEscapeCharacter; i < name.Length;)
 0109                {
 0110                    char c = name[i++];
 111
 0112                    if (c != EscapeCharacter || i == name.Length)
 0113                    {
 0114                        sb.Append(c);
 0115                    }
 0116                    else if (name[i] == EscapeCharacter) // escaped escape character ;)
 0117                    {
 0118                        sb.Append(c);
 119                        // Consume the escaped escape character, it's important for edge cases
 120                        // like escaped escape character followed by another escaped char (example: "\\\\\\+")
 0121                        i++;
 0122                    }
 0123                }
 124
 0125                return sb.ToString();
 0126            }
 0127        }
 128
 129        // this method handles escaping of the ] just to let the AssemblyNameParser fail for the right input
 130        internal static ReadOnlySpan<char> GetAssemblyNameCandidate(ReadOnlySpan<char> input)
 11164131        {
 132            // The only delimiter which can terminate an assembly name is ']'.
 133            // Otherwise EOL serves as the terminator.
 11164134            int offset = input.IndexOf(']');
 135
 11164136            if (offset > 0 && input[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL)
 672137            {
 672138                offset = GetUnescapedOffset(input, startIndex: offset);
 672139            }
 140
 11164141            return offset < 0 ? input : input.Slice(0, offset);
 142
 143            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startIndex)
 672144            {
 672145                int offset = startIndex;
 71144146                for (; offset < input.Length; offset++)
 35348147                {
 35348148                    if (input[offset] is ']')
 5480149                    {
 5480150                        if (input[offset - 1] != EscapeCharacter)
 112151                        {
 112152                            break;
 153                        }
 5368154                    }
 35236155                }
 672156                return offset;
 672157            }
 11164158        }
 159
 160        internal static void AppendRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder
 9024161        {
 9024162            if (rankOrModifier == ByRef)
 1112163            {
 1112164                builder.Append('&');
 1112165            }
 7912166            else if (rankOrModifier == Pointer)
 3092167            {
 3092168                builder.Append('*');
 3092169            }
 4820170            else if (rankOrModifier == SZArray)
 3912171            {
 3912172                builder.Append("[]");
 3912173            }
 908174            else if (rankOrModifier == 1)
 332175            {
 332176                builder.Append("[*]");
 332177            }
 178            else
 576179            {
 576180                Debug.Assert(rankOrModifier >= 2);
 181
 182                // O(rank) work, so we have to assume the rank is trusted. We don't put a hard cap on this,
 183                // but within the TypeName parser, we do require the input string to contain the correct number
 184                // of commas. This forces the input string to have at least O(rank) length, so there's no
 185                // alg. complexity attack possible here. Callers can of course pass any arbitrary value to
 186                // TypeName.MakeArrayTypeName, but per first sentence in this comment, we have to assume any
 187                // such arbitrary value which is programmatically fed in originates from a trustworthy source.
 188
 576189                builder.Append('[');
 576190                builder.Append(',', rankOrModifier - 1);
 576191                builder.Append(']');
 576192            }
 9024193        }
 194
 195        /// <summary>
 196        /// Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",".
 197        /// </summary>
 198        internal static bool IsBeginningOfGenericArgs(ref ReadOnlySpan<char> span, out bool doubleBrackets)
 49704199        {
 49704200            doubleBrackets = false;
 201
 49704202            if (!span.IsEmpty && span[0] == '[')
 21420203            {
 204                // There are no spaces allowed before the first '[', but spaces are allowed after that.
 21420205                ReadOnlySpan<char> trimmed = span.Slice(1).TrimStart();
 21420206                if (!trimmed.IsEmpty)
 21364207                {
 21364208                    if (trimmed[0] == '[')
 3992209                    {
 3992210                        doubleBrackets = true;
 3992211                        span = trimmed.Slice(1).TrimStart();
 3992212                        return true;
 213                    }
 17372214                    if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...]
 12716215                    {
 12716216                        span = trimmed;
 12716217                        return true;
 218                    }
 4656219                }
 4712220            }
 221
 32996222            return false;
 49704223        }
 224
 225        internal static bool TryGetTypeNameInfo(TypeNameParseOptions options, ref ReadOnlySpan<char> input,
 226            ref List<int>? nestedNameLengths, ref int recursiveDepth, out int totalLength)
 50364227        {
 228            bool isNestedType;
 50364229            totalLength = 0;
 230            do
 56172231            {
 56172232                int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType);
 56172233                if (length <= 0)
 564234                {
 235                    // invalid type names:
 236                    // -1: invalid escaping
 237                    // 0: pair of unescaped "++" characters
 564238                    return false;
 239                }
 240
 241#if SYSTEM_PRIVATE_CORELIB
 242                // Compat: Ignore leading '.' for type names without namespace. .NET Framework historically ignored lead
 243                // that code out there depends on this behavior. For example, type names formed by concatenating namespa
 244                // empty namespace (bug), are going to have superfluous leading '.'.
 245                // This behavior means that types that start with '.' are not round-trippable via type name.
 246                if (length > 1 && input[0] == '.' && input.Slice(0, length).LastIndexOf('.') == 0)
 247                {
 248                    input = input.Slice(1);
 249                    length--;
 250                }
 251#endif
 55608252                if (isNestedType)
 5904253                {
 5904254                    if (!TryDive(options, ref recursiveDepth))
 96255                    {
 96256                        return false;
 257                    }
 258
 5808259                    (nestedNameLengths ??= new()).Add(length);
 5808260                    totalLength += 1; // skip the '+' sign in next search
 5808261                }
 55512262                totalLength += length;
 111024263            } while (isNestedType);
 264
 49704265            return true;
 50364266        }
 267
 268        internal static bool TryParseNextDecorator(ref ReadOnlySpan<char> input, out int rankOrModifier)
 79580269        {
 270            // Then try pulling a single decorator.
 271            // Whitespace cannot precede the decorator, but it can follow the decorator.
 272
 79580273            ReadOnlySpan<char> originalInput = input; // so we can restore on 'false' return
 274
 79580275            if (TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 8664276            {
 8664277                rankOrModifier = Pointer;
 8664278                return true;
 279            }
 280
 70916281            if (TryStripFirstCharAndTrailingSpaces(ref input, '&'))
 7760282            {
 7760283                rankOrModifier = ByRef;
 7760284                return true;
 285            }
 286
 63156287            if (TryStripFirstCharAndTrailingSpaces(ref input, '['))
 14932288            {
 289                // SZArray := []
 290                // MDArray := [*] or [,] or [,,,, ...]
 291
 14932292                int rank = 1;
 14932293                bool hasSeenAsterisk = false;
 294
 31456295            ReadNextArrayToken:
 296
 31456297                if (TryStripFirstCharAndTrailingSpaces(ref input, ']'))
 13964298                {
 299                    // End of array marker
 13964300                    rankOrModifier = rank == 1 && !hasSeenAsterisk ? SZArray : rank;
 13964301                    return true;
 302                }
 303
 17492304                if (!hasSeenAsterisk)
 17216305                {
 17216306                    if (rank == 1 && TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 2800307                    {
 308                        // [*]
 2800309                        hasSeenAsterisk = true;
 2800310                        goto ReadNextArrayToken;
 311                    }
 14416312                    else if (TryStripFirstCharAndTrailingSpaces(ref input, ','))
 13724313                    {
 314                        // [,,, ...]
 315                        // The runtime restricts arrays to rank 32, but we don't enforce that here.
 316                        // Instead, the max rank is controlled by the total number of commas present
 317                        // in the array decorator.
 41172318                        checked { rank++; }
 13724319                        goto ReadNextArrayToken;
 320                    }
 692321                }
 322
 323                // Don't know what this token is.
 324                // Fall through to 'return false' statement.
 968325            }
 326
 49192327            input = originalInput; // ensure 'ref input' not mutated
 49192328            rankOrModifier = 0;
 49192329            return false;
 79580330        }
 331
 332        internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan<char> span, char value)
 347888333        {
 347888334            if (!span.IsEmpty && span[0] == value)
 106428335            {
 106428336                span = span.Slice(1).TrimStart();
 106428337                return true;
 338            }
 241460339            return false;
 347888340        }
 341
 342        [DoesNotReturn]
 343        internal static void ThrowArgumentNullException(string paramName)
 0344        {
 0345            throw new ArgumentNullException(paramName);
 346        }
 347
 348        [DoesNotReturn]
 349        internal static void ThrowArgumentException_InvalidTypeName(int errorIndex)
 3796350        {
 3796351            throw new ArgumentException(SR.Argument_InvalidTypeName, $"typeName@{errorIndex}");
 352        }
 353
 354        [DoesNotReturn]
 355        internal static void ThrowInvalidOperation_MaxNodesExceeded(int limit)
 254356        {
 357#if SYSTEM_REFLECTION_METADATA
 254358            throw new InvalidOperationException(SR.Format(SR.InvalidOperation_MaxNodesExceeded, limit));
 359#else
 360            Debug.Fail("Expected to be unreachable");
 361            throw new InvalidOperationException();
 362#endif
 363        }
 364
 365        [DoesNotReturn]
 366        internal static void ThrowInvalidOperation_NotGenericType()
 0367        {
 368#if SYSTEM_REFLECTION_METADATA
 0369            throw new InvalidOperationException(SR.InvalidOperation_NotGenericType);
 370#else
 371            Debug.Fail("Expected to be unreachable");
 372            throw new InvalidOperationException();
 373#endif
 374        }
 375
 376        [DoesNotReturn]
 377        internal static void ThrowInvalidOperation_NotNestedType()
 0378        {
 379#if SYSTEM_REFLECTION_METADATA
 0380            throw new InvalidOperationException(SR.InvalidOperation_NotNestedType);
 381#else
 382            Debug.Fail("Expected to be unreachable");
 383            throw new InvalidOperationException();
 384#endif
 385        }
 386
 387        [DoesNotReturn]
 388        internal static void ThrowInvalidOperation_NoElement()
 0389        {
 390#if SYSTEM_REFLECTION_METADATA
 0391            throw new InvalidOperationException(SR.InvalidOperation_NoElement);
 392#else
 393            Debug.Fail("Expected to be unreachable");
 394            throw new InvalidOperationException();
 395#endif
 396        }
 397
 398        [DoesNotReturn]
 399        internal static void ThrowInvalidOperation_HasToBeArrayClass()
 0400        {
 401#if SYSTEM_REFLECTION_METADATA
 0402            throw new InvalidOperationException(SR.Argument_HasToBeArrayClass);
 403#else
 404            Debug.Fail("Expected to be unreachable");
 405            throw new InvalidOperationException();
 406#endif
 407        }
 408
 409        [DoesNotReturn]
 410        internal static void ThrowInvalidOperation_NestedTypeNamespace()
 0411        {
 412#if SYSTEM_REFLECTION_METADATA
 0413            throw new InvalidOperationException(SR.InvalidOperation_NestedTypeNamespace);
 414#else
 415            Debug.Fail("Expected to be unreachable");
 416            throw new InvalidOperationException();
 417#endif
 418        }
 419
 420        internal static bool IsMaxDepthExceeded(TypeNameParseOptions options, int depth)
 421#if SYSTEM_PRIVATE_CORELIB
 422            => false; // CoreLib does not enforce any limits
 423#else
 85956424            => depth > options.MaxNodes;
 425#endif
 426
 427        internal static bool TryDive(TypeNameParseOptions options, ref int depth)
 81908428        {
 81908429            depth++;
 81908430            return !IsMaxDepthExceeded(options, depth);
 81908431        }
 432
 433#if SYSTEM_REFLECTION_METADATA
 434        [DoesNotReturn]
 435        internal static void ThrowInvalidOperation_NotSimpleName(string fullName)
 0436        {
 0437            throw new InvalidOperationException(SR.Format(SR.Arg_NotSimpleTypeName, fullName));
 438        }
 439#endif
 440    }
 441}
 442