File: Patterns\RoutePatternParser.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.
 
#nullable disable
 
using System.Buffers;
using System.Diagnostics;
#if COMPONENTS
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Routing.Patterns;
#endif
 
namespace Microsoft.AspNetCore.Routing.Patterns;
 
internal static class RoutePatternParser
{
    private const char Separator = '/';
    private const char OpenBrace = '{';
    private const char CloseBrace = '}';
    private const char QuestionMark = '?';
    private const string PeriodString = ".";
 
    internal static readonly SearchValues<char> InvalidParameterNameChars = SearchValues.Create("/{}?*");
 
    public static RoutePattern Parse(string pattern)
    {
        ArgumentNullException.ThrowIfNull(pattern);
 
        var trimmedPattern = TrimPrefix(pattern);
 
        var context = new Context(trimmedPattern);
        var segments = new List<RoutePatternPathSegment>();
 
        while (context.MoveNext())
        {
            var i = context.Index;
 
            if (context.Current == Separator)
            {
                // If we get here is means that there's a consecutive '/' character.
                // Templates don't start with a '/' and parsing a segment consumes the separator.
                throw new RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators);
            }
 
            if (!ParseSegment(context, segments))
            {
                throw new RoutePatternException(pattern, context.Error);
            }
 
            // A successful parse should always result in us being at the end or at a separator.
            Debug.Assert(context.AtEnd() || context.Current == Separator);
 
            if (context.Index <= i)
            {
                // This shouldn't happen, but we want to crash if it does.
                var message = "Infinite loop detected in the parser. Please open an issue.";
                throw new InvalidProgramException(message);
            }
        }
 
        if (IsAllValid(context, segments))
        {
            return RoutePatternFactory.Pattern(pattern, segments);
        }
        else
        {
            throw new RoutePatternException(pattern, context.Error);
        }
    }
 
    private static bool ParseSegment(Context context, List<RoutePatternPathSegment> segments)
    {
        Debug.Assert(context != null);
        Debug.Assert(segments != null);
 
        var parts = new List<RoutePatternPart>();
 
        while (true)
        {
            var i = context.Index;
 
            if (context.Current == OpenBrace)
            {
                if (!context.MoveNext())
                {
                    // This is a dangling open-brace, which is not allowed
                    context.Error = Resources.TemplateRoute_MismatchedParameter;
                    return false;
                }
 
                if (context.Current == OpenBrace)
                {
                    // This is an 'escaped' brace in a literal, like "{{foo"
                    context.Back();
                    if (!ParseLiteral(context, parts))
                    {
                        return false;
                    }
                }
                else
                {
                    // This is a parameter
                    context.Back();
                    if (!ParseParameter(context, parts))
                    {
                        return false;
                    }
                }
            }
            else
            {
                if (!ParseLiteral(context, parts))
                {
                    return false;
                }
            }
 
            if (context.Current == Separator || context.AtEnd())
            {
                // We've reached the end of the segment
                break;
            }
 
            if (context.Index <= i)
            {
                // This shouldn't happen, but we want to crash if it does.
                var message = "Infinite loop detected in the parser. Please open an issue.";
                throw new InvalidProgramException(message);
            }
        }
 
        if (IsSegmentValid(context, parts))
        {
            segments.Add(new RoutePatternPathSegment(parts));
            return true;
        }
        else
        {
            return false;
        }
    }
 
    private static bool ParseParameter(Context context, List<RoutePatternPart> parts)
    {
        Debug.Assert(context.Current == OpenBrace);
        context.Mark();
 
        context.MoveNext();
 
        while (true)
        {
            if (context.Current == OpenBrace)
            {
                // This is an open brace inside of a parameter, it has to be escaped
                if (context.MoveNext())
                {
                    if (context.Current != OpenBrace)
                    {
                        // If we see something like "{p1:regex(^\d{3", we will come here.
                        context.Error = Resources.TemplateRoute_UnescapedBrace;
                        return false;
                    }
                }
                else
                {
                    // This is a dangling open-brace, which is not allowed
                    // Example: "{p1:regex(^\d{"
                    context.Error = Resources.TemplateRoute_MismatchedParameter;
                    return false;
                }
            }
            else if (context.Current == CloseBrace)
            {
                // When we encounter Closed brace here, it either means end of the parameter or it is a closed
                // brace in the parameter, in that case it needs to be escaped.
                // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
                if (!context.MoveNext())
                {
                    // This is the end of the string -and we have a valid parameter
                    break;
                }
 
                if (context.Current == CloseBrace)
                {
                    // This is an 'escaped' brace in a parameter name
                }
                else
                {
                    // This is the end of the parameter
                    break;
                }
            }
 
            if (!context.MoveNext())
            {
                // This is a dangling open-brace, which is not allowed
                context.Error = Resources.TemplateRoute_MismatchedParameter;
                return false;
            }
        }
 
        var text = context.Capture();
        if (text == "{}")
        {
            context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty);
            return false;
        }
 
        var inside = text.Substring(1, text.Length - 2);
        var decoded = inside.Replace("}}", "}").Replace("{{", "{");
 
        // At this point, we need to parse the raw name for inline constraint,
        // default values and optional parameters.
        var templatePart = RouteParameterParser.ParseRouteParameter(decoded);
 
        // See #475 - this is here because InlineRouteParameterParser can't return errors
        if (decoded.StartsWith('*') && decoded.EndsWith('?'))
        {
            context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
            return false;
        }
 
        if (templatePart.IsOptional && templatePart.Default != null)
        {
            // Cannot be optional and have a default value.
            // The only way to declare an optional parameter is to have a ? at the end,
            // hence we cannot have both default value and optional parameter within the template.
            // A workaround is to add it as a separate entry in the defaults argument.
            context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
            return false;
        }
 
        var parameterName = templatePart.Name;
        if (IsValidParameterName(context, parameterName))
        {
            parts.Add(templatePart);
            return true;
        }
        else
        {
            return false;
        }
    }
 
    private static bool ParseLiteral(Context context, List<RoutePatternPart> parts)
    {
        context.Mark();
 
        while (true)
        {
            if (context.Current == Separator)
            {
                // End of the segment
                break;
            }
            else if (context.Current == OpenBrace)
            {
                if (!context.MoveNext())
                {
                    // This is a dangling open-brace, which is not allowed
                    context.Error = Resources.TemplateRoute_MismatchedParameter;
                    return false;
                }
 
                if (context.Current == OpenBrace)
                {
                    // This is an 'escaped' brace in a literal, like "{{foo" - keep going.
                }
                else
                {
                    // We've just seen the start of a parameter, so back up.
                    context.Back();
                    break;
                }
            }
            else if (context.Current == CloseBrace)
            {
                if (!context.MoveNext())
                {
                    // This is a dangling close-brace, which is not allowed
                    context.Error = Resources.TemplateRoute_MismatchedParameter;
                    return false;
                }
 
                if (context.Current == CloseBrace)
                {
                    // This is an 'escaped' brace in a literal, like "{{foo" - keep going.
                }
                else
                {
                    // This is an unbalanced close-brace, which is not allowed
                    context.Error = Resources.TemplateRoute_MismatchedParameter;
                    return false;
                }
            }
 
            if (!context.MoveNext())
            {
                break;
            }
        }
 
        var encoded = context.Capture();
        var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
        if (IsValidLiteral(context, decoded))
        {
            parts.Add(RoutePatternFactory.LiteralPart(decoded));
            return true;
        }
        else
        {
            return false;
        }
    }
 
    private static bool IsAllValid(Context context, List<RoutePatternPathSegment> segments)
    {
        // A catch-all parameter must be the last part of the last segment
        for (var i = 0; i < segments.Count; i++)
        {
            var segment = segments[i];
            for (var j = 0; j < segment.Parts.Count; j++)
            {
                var part = segment.Parts[j];
                if (part is RoutePatternParameterPart parameter
                    && parameter.IsCatchAll &&
                    (i != segments.Count - 1 || j != segment.Parts.Count - 1))
                {
                    context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
                    return false;
                }
            }
        }
 
        return true;
    }
 
    private static bool IsSegmentValid(Context context, List<RoutePatternPart> parts)
    {
        // If a segment has multiple parts, then it can't contain a catch all.
        for (var i = 0; i < parts.Count; i++)
        {
            var part = parts[i];
            if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1)
            {
                context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
                return false;
            }
        }
 
        // if a segment has multiple parts, then only the last one parameter can be optional
        // if it is following a optional separator.
        for (var i = 0; i < parts.Count; i++)
        {
            var part = parts[i];
 
            if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1)
            {
                // This optional parameter is the last part in the segment
                if (i == parts.Count - 1)
                {
                    var previousPart = parts[i - 1];
 
                    if (!previousPart.IsLiteral && !previousPart.IsSeparator)
                    {
                        // The optional parameter is preceded by something that is not a literal or separator
                        // Example of error message:
                        // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
                        // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
                        context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(
                            RoutePatternPathSegment.DebuggerToString(parts),
                            parameter.Name,
                            parts[i - 1].DebuggerToString());
 
                        return false;
                    }
                    else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString)
                    {
                        // The optional parameter is preceded by a literal other than period.
                        // Example of error message:
                        // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
                        // by an invalid segment '-'. Only a period (.) can precede an optional parameter.
                        context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(
                            RoutePatternPathSegment.DebuggerToString(parts),
                            parameter.Name,
                            parts[i - 1].DebuggerToString());
 
                        return false;
                    }
 
                    parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content);
                }
                else
                {
                    // This optional parameter is not the last one in the segment
                    // Example:
                    // An optional parameter must be at the end of the segment. In the segment '{RouteValue?})',
                    // optional parameter 'RouteValue' is followed by ')'
                    context.Error = Resources.FormatTemplateRoute_OptionalParameterHasTobeTheLast(
                        RoutePatternPathSegment.DebuggerToString(parts),
                        parameter.Name,
                        parts[i + 1].DebuggerToString());
 
                    return false;
                }
            }
        }
 
        // A segment cannot contain two consecutive parameters
        var isLastSegmentParameter = false;
        for (var i = 0; i < parts.Count; i++)
        {
            var part = parts[i];
            if (part.IsParameter && isLastSegmentParameter)
            {
                context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
                return false;
            }
 
            isLastSegmentParameter = part.IsParameter;
        }
 
        return true;
    }
 
    private static bool IsValidParameterName(Context context, string parameterName)
    {
        if (parameterName.Length == 0 || parameterName.AsSpan().IndexOfAny(InvalidParameterNameChars) >= 0)
        {
            context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName);
            return false;
        }
 
        if (!context.ParameterNames.Add(parameterName))
        {
            context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName);
            return false;
        }
 
        return true;
    }
 
    private static bool IsValidLiteral(Context context, string literal)
    {
        Debug.Assert(context != null);
        Debug.Assert(literal != null);
 
        if (literal.Contains(QuestionMark))
        {
            context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal);
            return false;
        }
 
        return true;
    }
 
    private static string TrimPrefix(string routePattern)
    {
        if (routePattern.StartsWith("~/", StringComparison.Ordinal))
        {
            return routePattern.Substring(2);
        }
        else if (routePattern.StartsWith('/'))
        {
            return routePattern.Substring(1);
        }
        else if (routePattern.StartsWith('~'))
        {
            throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate);
        }
        return routePattern;
    }
 
    [DebuggerDisplay("{DebuggerToString()}")]
    private sealed class Context
    {
        private readonly string _template;
        private int _index;
        private int? _mark;
 
        private readonly HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        public Context(string template)
        {
            Debug.Assert(template != null);
            _template = template;
 
            _index = -1;
        }
 
        public char Current
        {
            get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
        }
 
        public int Index => _index;
 
        public string Error
        {
            get;
            set;
        }
 
        public HashSet<string> ParameterNames
        {
            get { return _parameterNames; }
        }
 
        public bool Back()
        {
            return --_index >= 0;
        }
 
        public bool AtEnd()
        {
            return _index >= _template.Length;
        }
 
        public bool MoveNext()
        {
            return ++_index < _template.Length;
        }
 
        public void Mark()
        {
            Debug.Assert(_index >= 0);
 
            // Index is always the index of the character *past* Current - we want to 'mark' Current.
            _mark = _index;
        }
 
        public string Capture()
        {
            if (_mark.HasValue)
            {
                var value = _template.Substring(_mark.Value, _index - _mark.Value);
                _mark = null;
                return value;
            }
            else
            {
                return null;
            }
        }
 
        private string DebuggerToString()
        {
            if (_index == -1)
            {
                return _template;
            }
            else if (_mark.HasValue)
            {
                return _template.Substring(0, _mark.Value) +
                    "|" +
                    _template.Substring(_mark.Value, _index - _mark.Value) +
                    "|" +
                    _template.Substring(_index);
            }
            else
            {
                return string.Concat(_template.Substring(0, _index), "|", _template.Substring(_index));
            }
        }
    }
}