| File: Patterns\RouteParameterParser.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. 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; } } }