Skip to content

Fix Prefix Matching Logic in PrefixContainer #58068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 31 additions & 23 deletions src/Mvc/Mvc.Core/src/ModelBinding/PrefixContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,32 +201,40 @@ private int BinarySearch(string prefix)
{
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 pivot;
}

var c = candidate[prefix.Length];
if (c == '.' || c == '[')
// At this point, we have identified a candidate that starts with the given prefix.
// If the candidate's length is greater than that of the prefix, we need to examine
// the character that immediately follows the prefix in the candidate string.
// This step is crucial to determine if the candidate is a valid prefix match.
// A valid prefix match occurs if the next character is either a delimiter ('.' or '['),
// indicating that the candidate is a sub-key of the prefix. If the next character
// is not a delimiter, it means the candidate contains additional characters that
// extend beyond the prefix without forming a valid hierarchical relationship,
// which should not qualify as a prefix match. Therefore, we will continue searching
// for valid matches in this case.

if ((uint)candidate.Length > (uint)prefix.Length)
{
// Match, followed by delimiter
return pivot;
var c = candidate[prefix.Length];
if (c == '.' || c == '[')
{
// Match, followed by delimiter
return pivot;
}

// Okay, so the candidate has some extra text. We need to keep searching.
//
// 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".
//
// Fall back to brute force and cover all the cases.
return LinearSearch(prefix, start, end);
}

// Okay, so the candidate has some extra text. We need to keep searching.
//
// 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".
//
// Fall back to brute force and cover all the cases.
return LinearSearch(prefix, start, end);
return -1;
}

if (compare > 0)
Expand Down
17 changes: 17 additions & 0 deletions src/Mvc/Mvc.Core/test/ModelBinding/PrefixContainerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,23 @@ public void ContainsPrefix_HasEntries_PartialAndPrefixMatch_WithSquareBrace(int
Assert.True(result);
}

[Theory]
[InlineData("parameter")]
[InlineData("foo")]
[InlineData("bar")]
public void ContainsPrefix_ReturnsFalse_WhenPrefixMatchesExactly_WithKeys(string prefix)
{
// Arrange
var keys = new string[] { "parameter", "foo", "bar" };
var container = new PrefixContainer(keys);

// Act
var result = container.ContainsPrefix(prefix);

// Assert
Assert.False(result);
}

[Theory]
[InlineData("")]
[InlineData("foo")]
Expand Down
Loading