// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; namespace Microsoft.AspNetCore.Dispatcher.Patterns { internal static class RoutePatternParser { private const char Separator = '/'; private const char OpenBrace = '{'; private const char CloseBrace = '}'; private const char EqualsSign = '='; private const char QuestionMark = '?'; private const char Asterisk = '*'; private const string PeriodString = "."; internal static readonly char[] InvalidParameterNameChars = new char[] { Separator, OpenBrace, CloseBrace, QuestionMark, Asterisk }; public static RoutePattern Parse(string pattern) { if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } var trimmedPattern = TrimPrefix(pattern); var context = new TemplateParserContext(trimmedPattern); var segments = new List(); 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) { throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); } } if (IsAllValid(context, segments)) { var builder = RoutePatternBuilder.Create(pattern); for (var i = 0; i < segments.Count; i++) { builder.PathSegments.Add(segments[i]); } return builder.Build(); } else { throw new RoutePatternException(pattern, context.Error); } } private static bool ParseSegment(TemplateParserContext context, List segments) { Debug.Assert(context != null); Debug.Assert(segments != null); var parts = new List(); 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) { throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); } } if (IsSegmentValid(context, parts)) { segments.Add(new RoutePatternPathSegment(null, parts.ToArray())); return true; } else { return false; } } private static bool ParseParameter(TemplateParserContext context, List 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 = InlineRouteParameterParser.ParseRouteParameter(text, 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.DefaultValue != 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(TemplateParserContext context, List 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(RoutePatternPart.CreateLiteralFromText(encoded, decoded)); return true; } else { return false; } } private static bool IsAllValid(TemplateParserContext context, List 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.IsParameter && ((RoutePatternParameter)part).IsCatchAll && (i != segments.Count - 1 || j != segment.Parts.Count - 1)) { context.Error = Resources.TemplateRoute_CatchAllMustBeLast; return false; } } } return true; } private static bool IsSegmentValid(TemplateParserContext context, List 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.IsParameter && ((RoutePatternParameter)part).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 seperator. for (var i = 0; i < parts.Count; i++) { var part = parts[i]; if (part.IsParameter && ((RoutePatternParameter)part).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 = string.Format( Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, RoutePatternPathSegment.DebuggerToString(parts), ((RoutePatternParameter)part).Name, parts[i - 1].DebuggerToString()); return false; } else if (previousPart is RoutePatternLiteral 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 = string.Format( Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, RoutePatternPathSegment.DebuggerToString(parts), ((RoutePatternParameter)part).Name, parts[i - 1].DebuggerToString()); return false; } parts[i - 1] = RoutePatternPart.CreateSeparatorFromText(previousPart.RawText, ((RoutePatternLiteral)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 = string.Format( Resources.TemplateRoute_OptionalParameterHasTobeTheLast, RoutePatternPathSegment.DebuggerToString(parts), ((RoutePatternParameter)part).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(TemplateParserContext context, string parameterName) { if (parameterName.Length == 0 || parameterName.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(TemplateParserContext context, string literal) { Debug.Assert(context != null); Debug.Assert(literal != null); if (literal.IndexOf(QuestionMark) != -1) { 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("/", StringComparison.Ordinal)) { return routePattern.Substring(1); } else if (routePattern.StartsWith("~", StringComparison.Ordinal)) { throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate); } return routePattern; } [DebuggerDisplay("{DebuggerToString()}")] private class TemplateParserContext { private readonly string _template; private int _index; private int? _mark; private HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); public TemplateParserContext(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 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 _template.Substring(0, _index) + "|" + _template.Substring(_index); } } } } }