< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 216
Coverable lines: 216
Total lines: 442
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 100
Branch coverage: 0%
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
 019        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)
 023        {
 024            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.
 037            int offset = input.IndexOfAny(s_endOfFullTypeNameDelimiterChars);
 038            if (offset < 0)
 039            {
 040                return input.Length; // no type name end chars were found, the whole input is the type name
 41            }
 42
 043            if (input[offset] == EscapeCharacter) // this is very rare (IL Emit or pure IL)
 044            {
 045                offset = GetUnescapedOffset(input, startOffset: offset); // this is slower, but very rare so acceptable
 046            }
 47
 048            isNestedType = offset > 0 && offset < input.Length && input[offset] == '+';
 049            return offset;
 50
 51            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startOffset)
 052            {
 053                int offset = startOffset;
 054                for (; offset < input.Length; offset++)
 055                {
 056                    char c = input[offset];
 057                    if (c == EscapeCharacter)
 058                    {
 059                        offset++; // skip the escaped char
 60
 061                        if (offset == input.Length || // invalid name that ends with escape character
 062                            !s_endOfFullTypeNameDelimiterChars.Contains(input[offset])) // invalid name, escapes a char 
 063                        {
 064                            return -1;
 65                        }
 066                    }
 067                    else if (s_endOfFullTypeNameDelimiterChars.Contains(c))
 068                    {
 069                        break;
 70                    }
 071                }
 072                return offset;
 073            }
 074        }
 75
 76        internal static int IndexOfNamespaceDelimiter(ReadOnlySpan<char> fullName)
 077        {
 78            // Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp
 79            // This could result in the type name beginning with a '.' character.
 080            int index = fullName.LastIndexOf('.');
 81
 082            if (index > 0 && fullName[index - 1] == '.')
 083            {
 084                index--;
 085            }
 86
 087            return index;
 088        }
 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)
 0131        {
 132            // The only delimiter which can terminate an assembly name is ']'.
 133            // Otherwise EOL serves as the terminator.
 0134            int offset = input.IndexOf(']');
 135
 0136            if (offset > 0 && input[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL)
 0137            {
 0138                offset = GetUnescapedOffset(input, startIndex: offset);
 0139            }
 140
 0141            return offset < 0 ? input : input.Slice(0, offset);
 142
 143            static int GetUnescapedOffset(ReadOnlySpan<char> input, int startIndex)
 0144            {
 0145                int offset = startIndex;
 0146                for (; offset < input.Length; offset++)
 0147                {
 0148                    if (input[offset] is ']')
 0149                    {
 0150                        if (input[offset - 1] != EscapeCharacter)
 0151                        {
 0152                            break;
 153                        }
 0154                    }
 0155                }
 0156                return offset;
 0157            }
 0158        }
 159
 160        internal static void AppendRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder
 0161        {
 0162            if (rankOrModifier == ByRef)
 0163            {
 0164                builder.Append('&');
 0165            }
 0166            else if (rankOrModifier == Pointer)
 0167            {
 0168                builder.Append('*');
 0169            }
 0170            else if (rankOrModifier == SZArray)
 0171            {
 0172                builder.Append("[]");
 0173            }
 0174            else if (rankOrModifier == 1)
 0175            {
 0176                builder.Append("[*]");
 0177            }
 178            else
 0179            {
 0180                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
 0189                builder.Append('[');
 0190                builder.Append(',', rankOrModifier - 1);
 0191                builder.Append(']');
 0192            }
 0193        }
 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)
 0199        {
 0200            doubleBrackets = false;
 201
 0202            if (!span.IsEmpty && span[0] == '[')
 0203            {
 204                // There are no spaces allowed before the first '[', but spaces are allowed after that.
 0205                ReadOnlySpan<char> trimmed = span.Slice(1).TrimStart();
 0206                if (!trimmed.IsEmpty)
 0207                {
 0208                    if (trimmed[0] == '[')
 0209                    {
 0210                        doubleBrackets = true;
 0211                        span = trimmed.Slice(1).TrimStart();
 0212                        return true;
 213                    }
 0214                    if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...]
 0215                    {
 0216                        span = trimmed;
 0217                        return true;
 218                    }
 0219                }
 0220            }
 221
 0222            return false;
 0223        }
 224
 225        internal static bool TryGetTypeNameInfo(TypeNameParseOptions options, ref ReadOnlySpan<char> input,
 226            ref List<int>? nestedNameLengths, ref int recursiveDepth, out int totalLength)
 0227        {
 228            bool isNestedType;
 0229            totalLength = 0;
 230            do
 0231            {
 0232                int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType);
 0233                if (length <= 0)
 0234                {
 235                    // invalid type names:
 236                    // -1: invalid escaping
 237                    // 0: pair of unescaped "++" characters
 0238                    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
 0252                if (isNestedType)
 0253                {
 0254                    if (!TryDive(options, ref recursiveDepth))
 0255                    {
 0256                        return false;
 257                    }
 258
 0259                    (nestedNameLengths ??= new()).Add(length);
 0260                    totalLength += 1; // skip the '+' sign in next search
 0261                }
 0262                totalLength += length;
 0263            } while (isNestedType);
 264
 0265            return true;
 0266        }
 267
 268        internal static bool TryParseNextDecorator(ref ReadOnlySpan<char> input, out int rankOrModifier)
 0269        {
 270            // Then try pulling a single decorator.
 271            // Whitespace cannot precede the decorator, but it can follow the decorator.
 272
 0273            ReadOnlySpan<char> originalInput = input; // so we can restore on 'false' return
 274
 0275            if (TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 0276            {
 0277                rankOrModifier = Pointer;
 0278                return true;
 279            }
 280
 0281            if (TryStripFirstCharAndTrailingSpaces(ref input, '&'))
 0282            {
 0283                rankOrModifier = ByRef;
 0284                return true;
 285            }
 286
 0287            if (TryStripFirstCharAndTrailingSpaces(ref input, '['))
 0288            {
 289                // SZArray := []
 290                // MDArray := [*] or [,] or [,,,, ...]
 291
 0292                int rank = 1;
 0293                bool hasSeenAsterisk = false;
 294
 0295            ReadNextArrayToken:
 296
 0297                if (TryStripFirstCharAndTrailingSpaces(ref input, ']'))
 0298                {
 299                    // End of array marker
 0300                    rankOrModifier = rank == 1 && !hasSeenAsterisk ? SZArray : rank;
 0301                    return true;
 302                }
 303
 0304                if (!hasSeenAsterisk)
 0305                {
 0306                    if (rank == 1 && TryStripFirstCharAndTrailingSpaces(ref input, '*'))
 0307                    {
 308                        // [*]
 0309                        hasSeenAsterisk = true;
 0310                        goto ReadNextArrayToken;
 311                    }
 0312                    else if (TryStripFirstCharAndTrailingSpaces(ref input, ','))
 0313                    {
 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.
 0318                        checked { rank++; }
 0319                        goto ReadNextArrayToken;
 320                    }
 0321                }
 322
 323                // Don't know what this token is.
 324                // Fall through to 'return false' statement.
 0325            }
 326
 0327            input = originalInput; // ensure 'ref input' not mutated
 0328            rankOrModifier = 0;
 0329            return false;
 0330        }
 331
 332        internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan<char> span, char value)
 0333        {
 0334            if (!span.IsEmpty && span[0] == value)
 0335            {
 0336                span = span.Slice(1).TrimStart();
 0337                return true;
 338            }
 0339            return false;
 0340        }
 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)
 0350        {
 0351            throw new ArgumentException(SR.Argument_InvalidTypeName, $"typeName@{errorIndex}");
 352        }
 353
 354        [DoesNotReturn]
 355        internal static void ThrowInvalidOperation_MaxNodesExceeded(int limit)
 0356        {
 357#if SYSTEM_REFLECTION_METADATA
 0358            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
 0424            => depth > options.MaxNodes;
 425#endif
 426
 427        internal static bool TryDive(TypeNameParseOptions options, ref int depth)
 0428        {
 0429            depth++;
 0430            return !IsMaxDepthExceeded(options, depth);
 0431        }
 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