From eebc7db2caced69de290f3f4325f5c77dd24dc0a Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 25 Oct 2017 14:16:04 -0700 Subject: [PATCH] Use RoutePatternMatcher logic in TemplateMatcher (#484) --- .../Patterns/RoutePatternMatcher.cs | 61 ++- .../Template/TemplateMatcher.cs | 390 +----------------- .../RoutePatternMatcherTests.cs | 2 - .../Template/RouteTemplateTest.cs | 1 - 4 files changed, 56 insertions(+), 398 deletions(-) diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs index 2a5c431e57..0340ead1b1 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs @@ -209,10 +209,23 @@ namespace Microsoft.AspNetCore.Dispatcher if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter) { // This is a literal segment, so we need to match the text, or the route isn't a match. - var part = pathSegment.Parts[0]; - if (!stringSegment.Equals(part.RawText, StringComparison.OrdinalIgnoreCase)) + if (pathSegment.Parts[0].IsLiteral) { - return false; + var part = (RoutePatternLiteral)pathSegment.Parts[0]; + + if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + var part = (RoutePatternSeparator)pathSegment.Parts[0]; + + if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) + { + return false; + } } } else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) @@ -301,12 +314,11 @@ namespace Microsoft.AspNetCore.Dispatcher } else { + var separator = (RoutePatternSeparator)routeSegment.Parts[indexOfLastSegment - 1]; if (requestSegment.EndsWith( - routeSegment.Parts[indexOfLastSegment - 1].RawText, - StringComparison.OrdinalIgnoreCase)) - { + separator.Content, + StringComparison.OrdinalIgnoreCase)) return false; - } return MatchComplexSegmentCore( routeSegment, @@ -367,10 +379,24 @@ namespace Microsoft.AspNetCore.Dispatcher return false; } - var indexOfLiteral = requestSegment.LastIndexOf( - part.RawText, + int indexOfLiteral; + if (part.IsLiteral) + { + var literal = (RoutePatternLiteral)part; + indexOfLiteral = requestSegment.LastIndexOf( + literal.Content, startIndex, StringComparison.OrdinalIgnoreCase); + } + else + { + var literal = (RoutePatternSeparator)part; + indexOfLiteral = requestSegment.LastIndexOf( + literal.Content, + startIndex, + StringComparison.OrdinalIgnoreCase); + } + if (indexOfLiteral == -1) { // If we couldn't find this literal index, this segment cannot match @@ -382,7 +408,11 @@ namespace Microsoft.AspNetCore.Dispatcher // This check is related to the check we do at the very end of this function. if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) { - if ((indexOfLiteral + part.RawText.Length) != requestSegment.Length) + if (part is RoutePatternLiteral literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length)) + { + return false; + } + else if (part is RoutePatternSeparator separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length)) { return false; } @@ -422,7 +452,16 @@ namespace Microsoft.AspNetCore.Dispatcher } else { - parameterStartIndex = newLastIndex + lastLiteral.RawText.Length; + if (lastLiteral.IsLiteral) + { + var literal = (RoutePatternLiteral)lastLiteral; + parameterStartIndex = newLastIndex + literal.Content.Length; + } + else + { + var separator = (RoutePatternSeparator)lastLiteral; + parameterStartIndex = newLastIndex + separator.Content.Length; + } parameterTextLength = lastIndex - parameterStartIndex; } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs index a63eedd412..bfc51eab4a 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs @@ -2,9 +2,7 @@ // 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.Diagnostics; -using Microsoft.AspNetCore.Dispatcher.Internal; +using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Template @@ -19,6 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template private readonly object[] _defaultValues; private static readonly char[] Delimiters = new char[] { SeparatorChar }; + private RoutePatternMatcher _routePatternMatcher; public TemplateMatcher( RouteTemplate template, @@ -57,6 +56,9 @@ namespace Microsoft.AspNetCore.Routing.Template _defaultValues[i] = value; } } + + var routePattern = Template.ToRoutePattern(); + _routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults); } public RouteValueDictionary Defaults { get; } @@ -70,387 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Template throw new ArgumentNullException(nameof(values)); } - var i = 0; - var pathTokenizer = new PathTokenizer(path); - - // Perf: We do a traversal of the request-segments + route-segments twice. - // - // For most segment-types, we only really need to any work on one of the two passes. - // - // On the first pass, we're just looking to see if there's anything that would disqualify us from matching. - // The most common case would be a literal segment that doesn't match. - // - // On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values' - // and start capturing strings. - foreach (var pathSegment in pathTokenizer) - { - if (pathSegment.Length == 0) - { - return false; - } - - var routeSegment = Template.GetSegment(i++); - if (routeSegment == null && pathSegment.Length > 0) - { - // If routeSegment is null, then we're out of route segments. All we can match is the empty - // string. - return false; - } - else if (routeSegment.IsSimple && routeSegment.Parts[0].IsLiteral) - { - // This is a literal segment, so we need to match the text, or the route isn't a match. - var part = routeSegment.Parts[0]; - if (!pathSegment.Equals(part.Text, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - else if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll) - { - // Nothing to validate for a catch-all - it can match any string, including the empty string. - // - // Also, a catch-all has to be the last part, so we're done. - break; - } - else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter) - { - // For a parameter, validate that it's a has some length, or we have a default, or it's optional. - var part = routeSegment.Parts[0]; - if (pathSegment.Length == 0 && - !_hasDefaultValue[i] && - !part.IsOptional) - { - // There's no value for this parameter, the route can't match. - return false; - } - } - else - { - Debug.Assert(!routeSegment.IsSimple); - - // Don't attempt to validate a complex segment at this point other than being non-emtpy, - // do it in the second pass. - } - } - - for (; i < Template.Segments.Count; i++) - { - // We've matched the request path so far, but still have remaining route segments. These need - // to be all single-part parameter segments with default values or else they won't match. - var routeSegment = Template.GetSegment(i); - Debug.Assert(routeSegment != null); - - if (!routeSegment.IsSimple) - { - // If the segment is a complex segment, it MUST contain literals, and we've parsed the full - // path so far, so it can't match. - return false; - } - - var part = routeSegment.Parts[0]; - if (part.IsLiteral) - { - // If the segment is a simple literal - which need the URL to provide a value, so we don't match. - return false; - } - - if (part.IsCatchAll) - { - // Nothing to validate for a catch-all - it can match any string, including the empty string. - // - // Also, a catch-all has to be the last part, so we're done. - break; - } - - // If we get here, this is a simple segment with a parameter. We need it to be optional, or for the - // defaults to have a value. - Debug.Assert(routeSegment.IsSimple && part.IsParameter); - if (!_hasDefaultValue[i] && !part.IsOptional) - { - // There's no default for this (non-optional) parameter so it can't match. - return false; - } - } - - // At this point we've very likely got a match, so start capturing values for real. - - i = 0; - foreach (var requestSegment in pathTokenizer) - { - var routeSegment = Template.GetSegment(i++); - - if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll) - { - // A catch-all captures til the end of the string. - var part = routeSegment.Parts[0]; - var captured = requestSegment.Buffer.Substring(requestSegment.Offset); - if (captured.Length > 0) - { - values[part.Name] = captured; - } - else - { - // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue. - values[part.Name] = _defaultValues[i]; - } - - // A catch-all has to be the last part, so we're done. - break; - } - else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter) - { - // A simple parameter captures the whole segment, or a default value if nothing was - // provided. - var part = routeSegment.Parts[0]; - if (requestSegment.Length > 0) - { - values[part.Name] = requestSegment.ToString(); - } - else - { - if (_hasDefaultValue[i]) - { - values[part.Name] = _defaultValues[i]; - } - } - } - else if (!routeSegment.IsSimple) - { - if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values)) - { - return false; - } - } - } - - for (; i < Template.Segments.Count; i++) - { - // We've matched the request path so far, but still have remaining route segments. We already know these - // are simple parameters that either have a default, or don't need to produce a value. - var routeSegment = Template.GetSegment(i); - Debug.Assert(routeSegment != null); - Debug.Assert(routeSegment.IsSimple); - - var part = routeSegment.Parts[0]; - Debug.Assert(part.IsParameter); - - // It's ok for a catch-all to produce a null value - if (_hasDefaultValue[i] || part.IsCatchAll) - { - // Don't replace an existing value with a null. - var defaultValue = _defaultValues[i]; - if (defaultValue != null || !values.ContainsKey(part.Name)) - { - values[part.Name] = defaultValue; - } - } - } - - // Copy all remaining default values to the route data - foreach (var kvp in Defaults) - { - if (!values.ContainsKey(kvp.Key)) - { - values.Add(kvp.Key, kvp.Value); - } - } - - return true; - } - - private bool MatchComplexSegment( - TemplateSegment routeSegment, - string requestSegment, - IReadOnlyDictionary defaults, - RouteValueDictionary values) - { - var indexOfLastSegment = routeSegment.Parts.Count - 1; - - // We match the request to the template starting at the rightmost parameter - // If the last segment of template is optional, then request can match the - // template with or without the last parameter. So we start with regular matching, - // but if it doesn't match, we start with next to last parameter. Example: - // Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away - // giving p3 value of three. But if the request is one/two, we start matching from the - // rightmost giving p3 the value of two, then we end up not matching the segment. - // In this case we start again from p2 to match the request and we succeed giving - // the value two to p2 - if (routeSegment.Parts[indexOfLastSegment].IsOptional && - routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator) - { - if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment)) - { - return true; - } - else - { - if (requestSegment.EndsWith( - routeSegment.Parts[indexOfLastSegment - 1].Text, - StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return MatchComplexSegmentCore( - routeSegment, - requestSegment, - Defaults, - values, - indexOfLastSegment - 2); - } - } - else - { - return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment); - } - } - - private bool MatchComplexSegmentCore( - TemplateSegment routeSegment, - string requestSegment, - IReadOnlyDictionary defaults, - RouteValueDictionary values, - int indexOfLastSegmentUsed) - { - Debug.Assert(routeSegment != null); - Debug.Assert(routeSegment.Parts.Count > 1); - - // Find last literal segment and get its last index in the string - var lastIndex = requestSegment.Length; - - TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered - - var outValues = new RouteValueDictionary(); - - while (indexOfLastSegmentUsed >= 0) - { - var newLastIndex = lastIndex; - - var part = routeSegment.Parts[indexOfLastSegmentUsed]; - if (part.IsParameter) - { - // Hold on to the parameter so that we can fill it in when we locate the next literal - parameterNeedsValue = part; - } - else - { - Debug.Assert(part.IsLiteral || part.IsOptionalSeperator); - lastLiteral = part; - - var startIndex = lastIndex - 1; - // If we have a pending parameter subsegment, we must leave at least one character for that - if (parameterNeedsValue != null) - { - startIndex--; - } - - if (startIndex < 0) - { - return false; - } - - var indexOfLiteral = requestSegment.LastIndexOf( - part.Text, - startIndex, - StringComparison.OrdinalIgnoreCase); - if (indexOfLiteral == -1) - { - // If we couldn't find this literal index, this segment cannot match - return false; - } - - // If the first subsegment is a literal, it must match at the right-most extent of the request URI. - // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". - // This check is related to the check we do at the very end of this function. - if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) - { - if ((indexOfLiteral + part.Text.Length) != requestSegment.Length) - { - return false; - } - } - - newLastIndex = indexOfLiteral; - } - - if ((parameterNeedsValue != null) && - (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0))) - { - // If we have a pending parameter that needs a value, grab that value - - int parameterStartIndex; - int parameterTextLength; - - if (lastLiteral == null) - { - if (indexOfLastSegmentUsed == 0) - { - parameterStartIndex = 0; - } - else - { - parameterStartIndex = newLastIndex; - Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); - } - parameterTextLength = lastIndex; - } - else - { - // If we're getting a value for a parameter that is somewhere in the middle of the segment - if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) - { - parameterStartIndex = 0; - parameterTextLength = lastIndex; - } - else - { - parameterStartIndex = newLastIndex + lastLiteral.Text.Length; - parameterTextLength = lastIndex - parameterStartIndex; - } - } - - var parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength); - - if (string.IsNullOrEmpty(parameterValueString)) - { - // If we're here that means we have a segment that contains multiple sub-segments. - // For these segments all parameters must have non-empty values. If the parameter - // has an empty value it's not a match. - return false; - - } - else - { - // If there's a value in the segment for this parameter, use the subsegment value - outValues.Add(parameterNeedsValue.Name, parameterValueString); - } - - parameterNeedsValue = null; - lastLiteral = null; - } - - lastIndex = newLastIndex; - indexOfLastSegmentUsed--; - } - - // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of - // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment - // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching - // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* - // request URI in order for it to be a match. - // This check is related to the check we do earlier in this function for LiteralSubsegments. - if (lastIndex == 0 || routeSegment.Parts[0].IsParameter) - { - foreach (var item in outValues) - { - values.Add(item.Key, item.Value); - } - - return true; - } - - return false; + return _routePatternMatcher.TryMatch(path, values); } } } diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTests.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTests.cs index 1332fd90e9..cf14769145 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTests.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTests.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Dispatcher.Patterns; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Dispatcher diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs index d78a0bc419..b959bcf90c 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs @@ -1,7 +1,6 @@ // 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. -using System.Collections.Generic; using Microsoft.AspNetCore.Dispatcher.Patterns; using Xunit;