< Summary

Information
Class: System.Reflection.Metadata.TypeNameParserHelpers
Assembly: System.Reflection.Metadata
File(s): D:\runner\runtime\src\libraries\System.Reflection.Metadata\src\System\Reflection\Metadata\TypeNameParserHelpers.cs
Line coverage
81%
Covered lines: 176
Uncovered lines: 40
Coverable lines: 216
Total lines: 441
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)

D:\runner\runtime\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)
 4797623        {
 4797624            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.
 4797637            int offset = input.IndexOfAny(s_endOfFullTypeNameDelimiterChars);
 4797638            if (offset < 0)
 117239            {
 117240                return input.Length; // no type name end chars were found, the whole input is the type name
 41            }
 42
 4680443            if (input[offset] == EscapeCharacter) // this is very rare (IL Emit or pure IL)
 188844            {
 188845                offset = GetUnescapedOffset(input, startOffset: offset); // this is slower, but very rare so acceptable
 188846            }
 47
 4680448            isNestedType = offset > 0 && offset < input.Length && input[offset] == '+';
 4680449            return offset;
 50
 51            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startOffset)
 188852            {
 188853                int offset = startOffset;
 1541654                for (; offset < input.Length; offset++)
 852055                {
 852056                    char c = input[offset];
 852057                    if (c == EscapeCharacter)
 294858                    {
 294859                        offset++; // skip the escaped char
 60
 294861                        if (offset == input.Length || // invalid name that ends with escape character
 294862                            !s_endOfFullTypeNameDelimiterChars.Contains(input[offset])) // invalid name, escapes a char 
 8463                        {
 8464                            return -1;
 65                        }
 286466                    }
 557267                    else if (s_endOfFullTypeNameDelimiterChars.Contains(c))
 167268                    {
 167269                        break;
 70                    }
 676471                }
 180472                return offset;
 188873            }
 4797674        }
 75
 76        internal static int IndexOfNamespaceDelimiter(ReadOnlySpan<char> fullName)
 680477        {
 78            // Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp
 79            // This could result in the type name beginning with a '.' character.
 680480            int index = fullName.LastIndexOf('.');
 81
 680482            if (index > 0 && fullName[index - 1] == '.')
 483            {
 484                index--;
 485            }
 86
 680487            return index;
 680488        }
 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)
 11180131        {
 132            // The only delimiter which can terminate an assembly name is ']'.
 133            // Otherwise EOL serves as the terminator.
 11180134            int offset = input.IndexOf(']');
 135
 11180136            if (offset > 0 && input[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL)
 884137            {
 884138                offset = GetUnescapedOffset(input, startIndex: offset);
 884139            }
 140
 11180141            return offset < 0 ? input : input.Slice(0, offset);
 142
 143            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startIndex)
 884144            {
 884145                int offset = startIndex;
 13572146                for (; offset < input.Length; offset++)
 6408147                {
 6408148                    if (input[offset] is ']')
 1804149                    {
 1804150                        if (input[offset - 1] != EscapeCharacter)
 64151                        {
 64152                            break;
 153                        }
 1740154                    }
 6344155                }
 884156                return offset;
 884157            }
 11180158        }
 159
 160        internal static void AppendRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder
 12728161        {
 12728162            if (rankOrModifier == ByRef)
 1420163            {
 1420164                builder.Append('&');
 1420165            }
 11308166            else if (rankOrModifier == Pointer)
 4076167            {
 4076168                builder.Append('*');
 4076169            }
 7232170            else if (rankOrModifier == SZArray)
 5172171            {
 5172172                builder.Append("[]");
 5172173            }
 2060174            else if (rankOrModifier == 1)
 284175            {
 284176                builder.Append("[*]");
 284177            }
 178            else
 1776179            {
 1776180                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
 1776189                builder.Append('[');
 1776190                builder.Append(',', rankOrModifier - 1);
 1776191                builder.Append(']');
 1776192            }
 12728193        }
 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)
 42980199        {
 42980200            doubleBrackets = false;
 201
 42980202            if (!span.IsEmpty && span[0] == '[')
 17940203            {
 204                // There are no spaces allowed before the first '[', but spaces are allowed after that.
 17940205                ReadOnlySpan<char> trimmed = span.Slice(1).TrimStart();
 17940206                if (!trimmed.IsEmpty)
 17836207                {
 17836208                    if (trimmed[0] == '[')
 4684209                    {
 4684210                        doubleBrackets = true;
 4684211                        span = trimmed.Slice(1).TrimStart();
 4684212                        return true;
 213                    }
 13152214                    if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...]
 10184215                    {
 10184216                        span = trimmed;
 10184217                        return true;
 218                    }
 2968219                }
 3072220            }
 221
 28112222            return false;
 42980223        }
 224
 225        internal static bool TryGetTypeNameInfo(TypeNameParseOptions options, ref ReadOnlySpan<char> input,
 226            ref List<int>? nestedNameLengths, ref int recursiveDepth, out int totalLength)
 43516227        {
 228            bool isNestedType;
 43516229            totalLength = 0;
 230            do
 47976231            {
 47976232                int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType);
 47976233                if (length <= 0)
 484234                {
 235                    // invalid type names:
 236                    // -1: invalid escaping
 237                    // 0: pair of unescaped "++" characters
 484238                    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
 47492252                if (isNestedType)
 4512253                {
 4512254                    if (!TryDive(options, ref recursiveDepth))
 52255                    {
 52256                        return false;
 257                    }
 258
 4460259                    (nestedNameLengths ??= new()).Add(length);
 4460260                    totalLength += 1; // skip the '+' sign in next search
 4460261                }
 47440262                totalLength += length;
 94880263            } while (isNestedType);
 264
 42980265            return true;
 43516266        }
 267
 268        internal static bool TryParseNextDecorator(ref ReadOnlySpan<char> input, out int rankOrModifier)
 69048269        {
 270            // Then try pulling a single decorator.
 271            // Whitespace cannot precede the decorator, but it can follow the decorator.
 272
 69048273            ReadOnlySpan<char> originalInput = input; // so we can restore on 'false' return
 274
 69048275            if (TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 10568276            {
 10568277                rankOrModifier = Pointer;
 10568278                return true;
 279            }
 280
 58480281            if (TryStripFirstCharAndTrailingSpaces(ref input, '&'))
 2116282            {
 2116283                rankOrModifier = ByRef;
 2116284                return true;
 285            }
 286
 56364287            if (TryStripFirstCharAndTrailingSpaces(ref input, '['))
 14336288            {
 289                // SZArray := []
 290                // MDArray := [*] or [,] or [,,,, ...]
 291
 14336292                int rank = 1;
 14336293                bool hasSeenAsterisk = false;
 294
 30112295            ReadNextArrayToken:
 296
 30112297                if (TryStripFirstCharAndTrailingSpaces(ref input, ']'))
 13344298                {
 299                    // End of array marker
 13344300                    rankOrModifier = rank == 1 && !hasSeenAsterisk ? SZArray : rank;
 13344301                    return true;
 302                }
 303
 16768304                if (!hasSeenAsterisk)
 16664305                {
 16664306                    if (rank == 1 && TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 720307                    {
 308                        // [*]
 720309                        hasSeenAsterisk = true;
 720310                        goto ReadNextArrayToken;
 311                    }
 15944312                    else if (TryStripFirstCharAndTrailingSpaces(ref input, ','))
 15056313                    {
 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.
 45168318                        checked { rank++; }
 15056319                        goto ReadNextArrayToken;
 320                    }
 888321                }
 322
 323                // Don't know what this token is.
 324                // Fall through to 'return false' statement.
 992325            }
 326
 43020327            input = originalInput; // ensure 'ref input' not mutated
 43020328            rankOrModifier = 0;
 43020329            return false;
 69048330        }
 331
 332        internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan<char> span, char value)
 310556333        {
 310556334            if (!span.IsEmpty && span[0] == value)
 99592335            {
 99592336                span = span.Slice(1).TrimStart();
 99592337                return true;
 338            }
 210964339            return false;
 310556340        }
 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)
 3586350        {
 3586351            throw new ArgumentException(SR.Argument_InvalidTypeName, $"typeName@{errorIndex}");
 352        }
 353
 354        [DoesNotReturn]
 355        internal static void ThrowInvalidOperation_MaxNodesExceeded(int limit)
 162356        {
 357#if SYSTEM_REFLECTION_METADATA
 162358            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
 75290424            => depth > options.MaxNodes;
 425#endif
 426
 427        internal static bool TryDive(TypeNameParseOptions options, ref int depth)
 71544428        {
 71544429            depth++;
 71544430            return !IsMaxDepthExceeded(options, depth);
 71544431        }
 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}