Skip to content
This repository was archived by the owner on Nov 27, 2018. It is now read-only.

Commit 54e96bd

Browse files
authored
Tolerate leading "~/" or "/" (#499)
Addresses #441
1 parent e450e1f commit 54e96bd

File tree

9 files changed

+70
-38
lines changed

9 files changed

+70
-38
lines changed

src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ public RoutePattern Build()
5858
var segment = PathSegments[i];
5959
for (var j = 0; j < segment.Parts.Count; j++)
6060
{
61-
var parameter = segment.Parts[j] as RoutePatternParameter;
62-
if (parameter != null)
61+
if (segment.Parts[j] is RoutePatternParameter parameter)
6362
{
6463
parameters.Add(parameter);
6564
}

src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,9 @@ public static RoutePattern Parse(string pattern)
3333
throw new ArgumentNullException(nameof(pattern));
3434
}
3535

36-
if (IsInvalidPattern(pattern))
37-
{
38-
throw new RoutePatternException(pattern, Resources.TemplateRoute_InvalidRouteTemplate);
39-
}
36+
var trimmedPattern = TrimPrefix(pattern);
4037

41-
var context = new TemplateParserContext(pattern);
42-
var builder = RoutePatternBuilder.Create(pattern);
38+
var context = new TemplateParserContext(trimmedPattern);
4339
var segments = new List<RoutePatternPathSegment>();
4440

4541
while (context.MoveNext())
@@ -69,6 +65,7 @@ public static RoutePattern Parse(string pattern)
6965

7066
if (IsAllValid(context, segments))
7167
{
68+
var builder = RoutePatternBuilder.Create(pattern);
7269
for (var i = 0; i < segments.Count; i++)
7370
{
7471
builder.PathSegments.Add(segments[i]);
@@ -257,7 +254,7 @@ private static bool ParseParameter(TemplateParserContext context, List<RoutePatt
257254
private static bool ParseLiteral(TemplateParserContext context, List<RoutePatternPart> parts)
258255
{
259256
context.Mark();
260-
257+
261258
while (true)
262259
{
263260
if (context.Current == Separator)
@@ -469,10 +466,21 @@ private static bool IsValidLiteral(TemplateParserContext context, string literal
469466
return true;
470467
}
471468

472-
private static bool IsInvalidPattern(string routeTemplate)
469+
private static string TrimPrefix(string routePattern)
473470
{
474-
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
475-
routeTemplate.StartsWith("/", StringComparison.Ordinal);
471+
if (routePattern.StartsWith("~/", StringComparison.Ordinal))
472+
{
473+
return routePattern.Substring(2);
474+
}
475+
else if (routePattern.StartsWith("/", StringComparison.Ordinal))
476+
{
477+
return routePattern.Substring(1);
478+
}
479+
else if (routePattern.StartsWith("~", StringComparison.Ordinal))
480+
{
481+
throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate);
482+
}
483+
return routePattern;
476484
}
477485

478486
[DebuggerDisplay("{DebuggerToString()}")]
@@ -555,8 +563,8 @@ private string DebuggerToString()
555563
}
556564
else if (_mark.HasValue)
557565
{
558-
return _template.Substring(0, _mark.Value) +
559-
"|" +
566+
return _template.Substring(0, _mark.Value) +
567+
"|" +
560568
_template.Substring(_mark.Value, _index - _mark.Value) +
561569
"|" +
562570
_template.Substring(_index);

src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNetCore.Dispatcher/Resources.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
<value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.</value>
165165
</data>
166166
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
167-
<value>The route template cannot start with a '/' or '~' character.</value>
167+
<value>The route template cannot start with a '~' character unless followed by a '/'.</value>
168168
</data>
169169
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
170170
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>

src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ internal RoutePatternBinder(
7474
// If it's a parameter subsegment, examine the current value to see if it matches the new value
7575
var parameterName = parameter.Name;
7676

77-
object newParameterValue;
78-
var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
77+
var hasNewParameterValue = values.TryGetValue(parameterName, out var newParameterValue);
7978

8079
object currentParameterValue = null;
8180
var hasCurrentParameterValue = ambientValues != null &&
@@ -179,8 +178,7 @@ internal RoutePatternBinder(
179178
continue;
180179
}
181180

182-
object value;
183-
if (values.TryGetValue(filter.Key, out value))
181+
if (values.TryGetValue(filter.Key, out var value))
184182
{
185183
if (!RoutePartsEqual(value, filter.Value))
186184
{

src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static RouteTemplate Parse(string routeTemplate)
1717

1818
try
1919
{
20-
var inner = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern.Parse(routeTemplate);
20+
var inner = RoutePattern.Parse(routeTemplate);
2121
return new RouteTemplate(inner);
2222
}
2323
catch (ArgumentException ex) when (ex.InnerException is RoutePatternException)

test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -622,20 +622,24 @@ public void InvalidTemplate_RepeatedParametersThrows()
622622
"a literal string.");
623623
}
624624

625-
[Fact]
626-
public void InvalidTemplate_CannotStartWithSlash()
625+
[Theory]
626+
[InlineData("/foo")]
627+
[InlineData("~/foo")]
628+
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern)
627629
{
628-
ExceptionAssert.Throws<RoutePatternException>(
629-
() => RoutePatternParser.Parse("/foo"),
630-
"The route template cannot start with a '/' or '~' character.");
630+
// Arrange & Act
631+
var pattern = RoutePatternParser.Parse(routePattern);
632+
633+
// Assert
634+
Assert.Equal(routePattern, pattern.RawText);
631635
}
632636

633637
[Fact]
634638
public void InvalidTemplate_CannotStartWithTilde()
635639
{
636640
ExceptionAssert.Throws<RoutePatternException>(
637641
() => RoutePatternParser.Parse("~foo"),
638-
"The route template cannot start with a '/' or '~' character.");
642+
"The route template cannot start with a '~' character unless followed by a '/'.");
639643
}
640644

641645
[Fact]

test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Text.Encodings.Web;
8-
using Microsoft.AspNetCore.Dispatcher.Patterns;
98
using Microsoft.Extensions.ObjectPool;
109
using Microsoft.Extensions.WebEncoders.Testing;
1110
using Xunit;
@@ -651,6 +650,28 @@ public void GetVirtualDoesNotEncodeLeadingSlashes()
651650
UrlEncoder.Default);
652651
}
653652

653+
[Fact]
654+
public void GetUrlWithLeadingTildeSlash()
655+
{
656+
RunTest(
657+
"~/foo",
658+
null,
659+
null,
660+
new DispatcherValueCollection(new { }),
661+
"/UrlEncode[[foo]]");
662+
}
663+
664+
[Fact]
665+
public void GetUrlWithLeadingSlash()
666+
{
667+
RunTest(
668+
"/foo",
669+
null,
670+
null,
671+
new DispatcherValueCollection(new { }),
672+
"/UrlEncode[[foo]]");
673+
}
674+
654675
[Fact]
655676
public void GetUrlWithCatchAllWithValue()
656677
{
@@ -1192,8 +1213,7 @@ private void RunTest(
11921213

11931214
foreach (var kvp in expectedParts.Parameters)
11941215
{
1195-
string value;
1196-
Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value));
1216+
Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out var value));
11971217
Assert.Equal(kvp.Value, value);
11981218
}
11991219
}

test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -765,21 +765,24 @@ public void InvalidTemplate_RepeatedParametersThrows()
765765
"Parameter name: routeTemplate");
766766
}
767767

768-
[Fact]
769-
public void InvalidTemplate_CannotStartWithSlash()
768+
[Theory]
769+
[InlineData("/foo")]
770+
[InlineData("~/foo")]
771+
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
770772
{
771-
ExceptionAssert.Throws<ArgumentException>(
772-
() => TemplateParser.Parse("/foo"),
773-
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
774-
"Parameter name: routeTemplate");
773+
// Arrange & Act
774+
var pattern = TemplateParser.Parse(routeTemplate);
775+
776+
// Assert
777+
Assert.Equal(routeTemplate, pattern.TemplateText);
775778
}
776779

777780
[Fact]
778781
public void InvalidTemplate_CannotStartWithTilde()
779782
{
780783
ExceptionAssert.Throws<ArgumentException>(
781784
() => TemplateParser.Parse("~foo"),
782-
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
785+
"The route template cannot start with a '~' character unless followed by a '/'." + Environment.NewLine +
783786
"Parameter name: routeTemplate");
784787
}
785788

0 commit comments

Comments
 (0)