Remove the use of Regex in the TemplateRouteParser

Fixes #164
This commit is contained in:
Pranav K 2015-07-30 07:43:25 -07:00
parent ae27f7d321
commit fe9bf8bcbf
4 changed files with 352 additions and 80 deletions

View File

@ -1,10 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.AspNet.Routing.Template; using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
@ -12,82 +9,230 @@ namespace Microsoft.AspNet.Routing
{ {
public static class InlineRouteParameterParser public static class InlineRouteParameterParser
{ {
// One or more characters, matches "id"
private const string ParameterNamePattern = @"(?<parameterName>.+?)";
// Zero or more inline constraints that start with a colon followed by zero or more characters
// Optionally the constraint can have arguments within parentheses
// - necessary to capture characters like ":" and "}"
// Matches ":int", ":length(2)", ":regex(\})", ":regex(:)" zero or more times
private const string ConstraintPattern = @"(:(?<constraint>.*?(\(.*?\))?))*";
// A default value with an equal sign followed by zero or more characters
// Matches "=", "=abc"
private const string DefaultValueParameter = @"(?<defaultValue>(=.*?))?";
private static readonly Regex _parameterRegex = new Regex(
"^" + ParameterNamePattern + ConstraintPattern + DefaultValueParameter + "$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
public static TemplatePart ParseRouteParameter([NotNull] string routeParameter) public static TemplatePart ParseRouteParameter([NotNull] string routeParameter)
{ {
var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal); if (routeParameter.Length == 0)
var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal);
routeParameter = isCatchAll ? routeParameter.Substring(1) : routeParameter;
routeParameter = isOptional ? routeParameter.Substring(0, routeParameter.Length - 1) : routeParameter;
var parameterMatch = _parameterRegex.Match(routeParameter);
if (!parameterMatch.Success)
{ {
return TemplatePart.CreateParameter(name: string.Empty, return TemplatePart.CreateParameter(
isCatchAll: isCatchAll, name: string.Empty,
isOptional: isOptional, isCatchAll: false,
defaultValue: null, isOptional: false,
inlineConstraints: null); defaultValue: null,
inlineConstraints: null);
} }
var parameterName = parameterMatch.Groups["parameterName"].Value; var startIndex = 0;
var endIndex = routeParameter.Length - 1;
// Add the default value if present var isCatchAll = false;
var defaultValueGroup = parameterMatch.Groups["defaultValue"]; var isOptional = false;
var defaultValue = GetDefaultValue(defaultValueGroup);
// Register inline constraints if present if (routeParameter[0] == '*')
var constraintGroup = parameterMatch.Groups["constraint"]; {
var inlineConstraints = GetInlineConstraints(constraintGroup); 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, return TemplatePart.CreateParameter(parameterName,
isCatchAll, isCatchAll,
isOptional, isOptional,
defaultValue, defaultValue,
inlineConstraints); parseResults.Constraints);
} }
private static string GetDefaultValue(Group defaultValueGroup) private static ConstraintParseResults ParseConstraints(
string routeParameter,
int currentIndex,
int endIndex)
{ {
if (defaultValueGroup.Success) var inlineConstraints = new List<InlineConstraint>();
var state = ParseState.Start;
var startIndex = currentIndex;
do
{ {
var defaultValueMatch = defaultValueGroup.Value; 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.
// Strip out the equal sign at the beginning var nextChar = currentIndex + 1 > endIndex ? null : (char?)routeParameter[currentIndex + 1];
Debug.Assert(defaultValueMatch.StartsWith("=", StringComparison.Ordinal)); switch (nextChar)
return defaultValueMatch.Substring(1); {
} 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 immediatiely) 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));
return null; 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 = currentIndex,
Constraints = inlineConstraints
};
} }
private static IEnumerable<InlineConstraint> GetInlineConstraints(Group constraintGroup) private enum ParseState
{ {
var constraints = new List<InlineConstraint>(); Start,
ParsingName,
InsideParenthesis,
End
}
foreach (Capture capture in constraintGroup.Captures) private struct ConstraintParseResults
{ {
constraints.Add(new InlineConstraint(capture.Value)); public int CurrentIndex;
}
return constraints; public IEnumerable<InlineConstraint> Constraints;
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {

View File

@ -1,26 +1,28 @@
{ {
"description": "ASP.NET 5 middleware and abstractions for routing requests to application logic and for generating links.", "description": "ASP.NET 5 middleware and abstractions for routing requests to application logic and for generating links.",
"version": "1.0.0-*", "version": "1.0.0-*",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/aspnet/routing" "url": "git://github.com/aspnet/routing"
}, },
"compilationOptions": { "compilationOptions": {
"warningsAsErrors": "true" "warningsAsErrors": "true"
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*", "Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
"Microsoft.Framework.OptionsModel": "1.0.0-*", "Microsoft.Framework.OptionsModel": "1.0.0-*",
"Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } "Microsoft.Framework.NotNullAttribute.Sources": {
}, "type": "build",
"frameworks": { "version": "1.0.0-*"
"dnx451": {}, }
"dnxcore50": { },
"dependencies": { "frameworks": {
"System.Reflection.Extensions": "4.0.0-beta-*", "dnx451": { },
"System.Text.RegularExpressions": "4.0.10-beta-*" "dnxcore50": {
} "dependencies": {
"System.Text.RegularExpressions": "4.0.10-beta-*"
}
}
} }
}
} }

View File

@ -13,6 +13,70 @@ namespace Microsoft.AspNet.Routing.Tests
{ {
public class InlineRouteParameterParserTests public class InlineRouteParameterParserTests
{ {
[Theory]
[InlineData("=")]
[InlineData(":")]
public void ParseRouteParameter_WithoutADefaultValue(string parameterName)
{
// Arrange & Act
var templatePart = ParseParameter(parameterName);
// Assert
Assert.Equal(parameterName, templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact]
public void ParseRouteParameter_WithoutADefaultValue()
{
// Arrange & Act
var templatePart = ParseParameter("param=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintName()
{
// Arrange & Act
var templatePart = ParseParameter("param:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Empty(constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintNameOrParameterName()
{
// Arrange & Act
var templatePart = ParseParameter("param:=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Empty(constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator()
{
// Arrange & Act
var templatePart = ParseParameter("param=:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal(":", templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact] [Fact]
public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly()
{ {
@ -192,6 +256,23 @@ namespace Microsoft.AspNet.Routing.Tests
constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); constraint => Assert.Equal(@"test(\w+)", constraint.Constraint));
} }
[Theory]
[InlineData("=")]
[InlineData("+=")]
[InlineData(">= || <= || ==")]
public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue)
{
// Arrange & Act
var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}");
// Assert
Assert.Equal("comparison-operator", templatePart.Name);
Assert.Equal(defaultValue, templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("length(6)", constraint.Constraint);
}
[Fact] [Fact]
public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly()
{ {
@ -646,6 +727,36 @@ namespace Microsoft.AspNet.Routing.Tests
constraint => Assert.Equal(@"test1", constraint.Constraint)); constraint => Assert.Equal(@"test1", constraint.Constraint));
} }
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("default-value", templatePart.DefaultValue);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint),
constraint => Assert.Equal(@"name(test1", constraint.Constraint),
constraint => Assert.Equal(@"differentname", constraint.Constraint));
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(constraintvalue=test1");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("test1", templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(constraintvalue", constraint.Constraint);
}
[Fact] [Fact]
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly()
{ {
@ -767,6 +878,21 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal(@"test(#:)$)", constraint.Constraint); Assert.Equal(@"test(#:)$)", constraint.Constraint);
} }
[Fact]
public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint()
{
// Arrange & Act
var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()");
// Assert
Assert.Equal("foo", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint);
}
[Fact] [Fact]
public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
{ {