|
// 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 parantheses 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;
}
}
}
|