| File: InlineRouteParameterParser.cs | Web Access |
| Project: src\aspnetcore\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. using Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing; /// <summary> /// Contains methods for parsing processing constraints from a route definition. /// </summary> public static class InlineRouteParameterParser { /// <summary> /// Parses a string representing the provided <paramref name="routeParameter"/> into a <see cref="TemplatePart"/>. /// </summary> /// <param name="routeParameter">A string representation of the route parameter.</param> /// <returns>A <see cref="TemplatePart"/> instance.</returns> public static TemplatePart ParseRouteParameter(string routeParameter) { ArgumentNullException.ThrowIfNull(routeParameter); if (routeParameter.Length == 0) { return TemplatePart.CreateParameter( name: string.Empty, isCatchAll: false, isOptional: false, defaultValue: null, inlineConstraints: null); } var startIndex = 0; var endIndex = routeParameter.Length - 1; var isCatchAll = false; var isOptional = false; if (routeParameter[0] == '*') { isCatchAll = true; startIndex++; } if (routeParameter[endIndex] == '?') { isOptional = true; endIndex--; } var currentIndex = startIndex; // Parse parameter name var parameterName = string.Empty; while (currentIndex <= endIndex) { var currentChar = routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); // Roll the index back and move to the constraint parsing stage. currentIndex--; break; } else if (currentIndex == endIndex) { parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); } currentIndex++; } var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); currentIndex = parseResults.CurrentIndex; string? defaultValue = null; if (currentIndex <= endIndex && routeParameter[currentIndex] == '=') { defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); } return TemplatePart.CreateParameter(parameterName, isCatchAll, isOptional, defaultValue, parseResults.Constraints); } private static ConstraintParseResults ParseConstraints( string routeParameter, int currentIndex, int endIndex) { var inlineConstraints = new List<InlineConstraint>(); var state = ParseState.Start; var startIndex = currentIndex; do { var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); inlineConstraints.Add(new InlineConstraint(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?)routeParameter[currentIndex + 1]; switch (nextChar) { case null: state = ParseState.End; constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); inlineConstraints.Add(new InlineConstraint(constraintText)); break; case ':': state = ParseState.Start; constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); inlineConstraints.Add(new InlineConstraint(constraintText)); startIndex = currentIndex + 1; break; case '=': state = ParseState.End; constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); inlineConstraints.Add(new InlineConstraint(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 = routeParameter.IndexOf(')', currentIndex + 1); if (indexOfClosingParantheses == -1) { constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); inlineConstraints.Add(new InlineConstraint(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 = routeParameter.Substring(startIndex, currentIndex - startIndex); inlineConstraints.Add(new InlineConstraint(constraintText)); break; case ':': constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); inlineConstraints.Add(new InlineConstraint(constraintText)); startIndex = currentIndex + 1; break; case '(': state = ParseState.InsideParenthesis; break; case '=': state = ParseState.End; constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); inlineConstraints.Add(new InlineConstraint(constraintText)); currentIndex--; break; } break; } currentIndex++; } while (state != ParseState.End); return new ConstraintParseResults(currentIndex, inlineConstraints); } private enum ParseState { Start, ParsingName, InsideParenthesis, End } private readonly struct ConstraintParseResults { public readonly int CurrentIndex; public readonly IEnumerable<InlineConstraint> Constraints; public ConstraintParseResults(int currentIndex, IEnumerable<InlineConstraint> constraints) { CurrentIndex = currentIndex; Constraints = constraints; } } }