File: Patterns\RouteParameterParser.cs
Web Access
Project: src\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.AspNetCore.Routing.Patterns;
 
#if COMPONENTS
using Microsoft.AspNetCore.Components.Routing.Patterns;
#endif
 
internal static class RouteParameterParser
{
    // This code parses the inside of the route parameter
    //
    // Ex: {hello} - this method is responsible for parsing 'hello'
    // The factoring between this class and RoutePatternParser is due to legacy.
    public static RoutePatternParameterPart ParseRouteParameter(string parameter)
    {
        ArgumentNullException.ThrowIfNull(parameter);
 
        if (parameter.Length == 0)
        {
            return new RoutePatternParameterPart(string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty<RoutePatternParameterPolicyReference>());
        }
 
        var startIndex = 0;
        var endIndex = parameter.Length - 1;
        var encodeSlashes = true;
 
        var parameterKind = RoutePatternParameterKind.Standard;
 
        if (parameter.StartsWith("**", StringComparison.Ordinal))
        {
            encodeSlashes = false;
            parameterKind = RoutePatternParameterKind.CatchAll;
            startIndex += 2;
        }
        else if (parameter[0] == '*')
        {
            parameterKind = RoutePatternParameterKind.CatchAll;
            startIndex++;
        }
 
        if (parameter[endIndex] == '?')
        {
            parameterKind = RoutePatternParameterKind.Optional;
            endIndex--;
        }
 
        var currentIndex = startIndex;
 
        // Parse parameter name
        var parameterName = string.Empty;
 
        while (currentIndex <= endIndex)
        {
            var currentChar = parameter[currentIndex];
 
            if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex)
            {
                // Parameter names are allowed to start with delimiters used to denote constraints or default values.
                // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint
                // specifications.
                parameterName = parameter.Substring(startIndex, currentIndex - startIndex);
 
                // Roll the index back and move to the constraint parsing stage.
                currentIndex--;
                break;
            }
            else if (currentIndex == endIndex)
            {
                parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1);
            }
 
            currentIndex++;
        }
 
        var parseResults = ParseConstraints(parameter, currentIndex, endIndex);
        currentIndex = parseResults.CurrentIndex;
 
        string? defaultValue = null;
        if (currentIndex <= endIndex &&
            parameter[currentIndex] == '=')
        {
            defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex);
        }
 
        return new RoutePatternParameterPart(
            parameterName,
            defaultValue,
            parameterKind,
            parseResults.ParameterPolicies,
            encodeSlashes);
    }
 
    private static ParameterPolicyParseResults ParseConstraints(
        string text,
        int currentIndex,
        int endIndex)
    {
#if !COMPONENTS
        var constraints = new ArrayBuilder<RoutePatternParameterPolicyReference>(0);
#else
        var constraints = new List<RoutePatternParameterPolicyReference>();
#endif
        var state = ParseState.Start;
        var startIndex = currentIndex;
        do
        {
            var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex];
            switch (state)
            {
                case ParseState.Start:
                    switch (currentChar)
                    {
                        case null:
                            state = ParseState.End;
                            break;
                        case ':':
                            state = ParseState.ParsingName;
                            startIndex = currentIndex + 1;
                            break;
                        case '(':
                            state = ParseState.InsideParenthesis;
                            break;
                        case '=':
                            state = ParseState.End;
                            currentIndex--;
                            break;
                    }
                    break;
                case ParseState.InsideParenthesis:
                    switch (currentChar)
                    {
                        case null:
                            state = ParseState.End;
                            var constraintText = text.Substring(startIndex, currentIndex - startIndex);
                            constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                            break;
                        case ')':
                            // Only consume a ')' token if
                            // (a) it is the last token
                            // (b) the next character is the start of the new constraint ':'
                            // (c) the next character is the start of the default value.
 
                            var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1];
                            switch (nextChar)
                            {
                                case null:
                                    state = ParseState.End;
                                    constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
                                    constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                                    break;
                                case ':':
                                    state = ParseState.Start;
                                    constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
                                    constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                                    startIndex = currentIndex + 1;
                                    break;
                                case '=':
                                    state = ParseState.End;
                                    constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
                                    constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                                    break;
                            }
                            break;
                        case ':':
                        case '=':
                            // In the original implementation, the Regex would've backtracked if it encountered an
                            // unbalanced opening bracket followed by (not necessarily immediately) a delimiter.
                            // Simply verifying that the parentheses will eventually be closed should suffice to
                            // determine if the terminator needs to be consumed as part of the current constraint
                            // specification.
                            var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1);
                            if (indexOfClosingParantheses == -1)
                            {
                                constraintText = text.Substring(startIndex, currentIndex - startIndex);
                                constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
 
                                if (currentChar == ':')
                                {
                                    state = ParseState.ParsingName;
                                    startIndex = currentIndex + 1;
                                }
                                else
                                {
                                    state = ParseState.End;
                                    currentIndex--;
                                }
                            }
                            else
                            {
                                currentIndex = indexOfClosingParantheses;
                            }
 
                            break;
                    }
                    break;
                case ParseState.ParsingName:
                    switch (currentChar)
                    {
                        case null:
                            state = ParseState.End;
                            var constraintText = text.Substring(startIndex, currentIndex - startIndex);
                            if (constraintText.Length > 0)
                            {
                                constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                            }
                            break;
                        case ':':
                            constraintText = text.Substring(startIndex, currentIndex - startIndex);
                            if (constraintText.Length > 0)
                            {
                                constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                            }
                            startIndex = currentIndex + 1;
                            break;
                        case '(':
                            state = ParseState.InsideParenthesis;
                            break;
                        case '=':
                            state = ParseState.End;
                            constraintText = text.Substring(startIndex, currentIndex - startIndex);
                            if (constraintText.Length > 0)
                            {
                                constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText));
                            }
                            currentIndex--;
                            break;
                    }
                    break;
            }
 
            currentIndex++;
 
        } while (state != ParseState.End);
 
        return new ParameterPolicyParseResults(currentIndex, constraints.ToArray());
    }
 
    private enum ParseState
    {
        Start,
        ParsingName,
        InsideParenthesis,
        End
    }
 
    private readonly struct ParameterPolicyParseResults
    {
        public readonly int CurrentIndex;
 
        public readonly RoutePatternParameterPolicyReference[] ParameterPolicies;
 
        public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies)
        {
            CurrentIndex = currentIndex;
            ParameterPolicies = parameterPolicies;
        }
    }
}