From f76a390a4eb958785eabb8eb49866a1b4f3d04da Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Thu, 29 Jun 2017 22:14:50 -0700 Subject: [PATCH] Fall back to linear search for prefix matches - #6469 --- .../Internal/PrefixContainer.cs | 58 +++++++++++++++++-- .../Internal/PrefixContainerTest.cs | 48 +++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs index 36b8e65493..ade8cc8102 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs @@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { Debug.Assert(candidate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); - // Ok, so now we have a candiate that starts with the prefix. If the candidate is longer than + // Okay, now we have a candidate that starts with the prefix. If the candidate is longer than // the prefix, we need to look at the next character and see if it's a delimiter. if (candidate.Length == prefix.Length) { @@ -208,15 +208,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal return pivot; } - // Ok, so the candidate has some extra text. We need to keep searching, but we know - // the candidate string is considered "greater" than the prefix, so treat it as-if - // the comparer returned a negative number. + // Okay, so the candidate has some extra text. We need to keep searching. // - // Ex: + // Can often assume the candidate string is greater than the prefix e.g. that works for // prefix: product // candidate: productId + // most of the time because "product", "product.id", etc. will sort earlier than "productId". But, + // the assumption isn't correct if "product[0]" is also in _sortedValues because that value will + // sort later than "productId". // - compare = -1; + // Fall back to brute force and cover all the cases. + return LinearSearch(prefix, start, end); } if (compare > 0) @@ -231,5 +233,49 @@ namespace Microsoft.AspNetCore.Mvc.Internal return ~start; } + + private int LinearSearch(string prefix, int start, int end) + { + for (; start <= end; start++) + { + var candidate = _sortedValues[start]; + var compare = string.Compare( + prefix, + 0, + candidate, + 0, + prefix.Length, + StringComparison.OrdinalIgnoreCase); + if (compare == 0) + { + Debug.Assert(candidate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + + // Okay, now we have a candidate that starts with the prefix. If the candidate is longer than + // the prefix, we need to look at the next character and see if it's a delimiter. + if (candidate.Length == prefix.Length) + { + // Exact match + return start; + } + + var c = candidate[prefix.Length]; + if (c == '.' || c == '[') + { + // Match, followed by delimiter + return start; + } + + // Keep checking until we've passed all StartsWith() matches. + } + + if (compare < 0) + { + // Prefix is less than the candidate. No potential matches left. + break; + } + } + + return ~start; + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/PrefixContainerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/PrefixContainerTest.cs index 39fef73219..91d925bdab 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/PrefixContainerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/PrefixContainerTest.cs @@ -137,6 +137,54 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.True(result); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void ContainsPrefix_HasEntries_PartialAndPrefixMatch_WithDot(int partialMatches) + { + // Arrange + var keys = new string[partialMatches + 1]; + for (var i = 0; i < partialMatches; i++) + { + keys[i] = $"aa[{i}]"; + } + keys[partialMatches] = "a.b"; // Sorted before all "aa" keys. + var container = new PrefixContainer(keys); + + // Act + var result = container.ContainsPrefix("a"); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void ContainsPrefix_HasEntries_PartialAndPrefixMatch_WithSquareBrace(int partialMatches) + { + // Arrange + var keys = new string[partialMatches + 1]; + for (var i = 0; i < partialMatches; i++) + { + keys[i] = $"aa[{i}]"; + } + keys[partialMatches] = "a[0]"; // Sorted after all "aa" keys. + var container = new PrefixContainer(keys); + + // Act + var result = container.ContainsPrefix("a"); + + // Assert + Assert.True(result); + } + [Theory] [InlineData("")] [InlineData("foo")]