Skip to content
Open
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
32 changes: 31 additions & 1 deletion TextFilter.Test/TextFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,31 @@ public void GlobClassWithATrailingDashMatchesTheDashLiterally()
CollectionAssert.AreEqual(items, TextFilter.Filter(items, "-file[0-", TextFilterType.Glob).ToList());
}

[TestMethod]
public void GlobWithAnUnclosedBracketMatchesTheBracketLiterally()
{
// A "[" with no closing "]" is a literal, as in fnmatch and bash. DotNet.Glob read it as a class
// running to the end of the token, so "a[b" matched "ab" but not "a[b", and "-log[1" hid "log1".
List<string> items = ["log[", "log[1", "log1", "logb", "a[b", "ab", "readme.md"];
foreach (TextFilterCaseSensitivity caseSensitivity in new[] { TextFilterCaseSensitivity.CaseSensitive, TextFilterCaseSensitivity.CaseInsensitive })
{
Assert.IsTrue(TextFilter.IsMatch("a[b", "a[b", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity));
Assert.IsFalse(TextFilter.IsMatch("ab", "a[b", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity));
CollectionAssert.AreEqual(
new List<string> { "log[" },
TextFilter.Filter(items, "log[", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity).ToList());
CollectionAssert.AreEqual(
new List<string> { "log[1" },
TextFilter.Filter(items, "log[1", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity).ToList());
CollectionAssert.AreEqual(
new List<string> { "log[", "log1", "logb", "a[b", "ab", "readme.md" },
TextFilter.Filter(items, "-log[1", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity).ToList());
CollectionAssert.AreEqual(
new List<string> { "a[b" },
TextFilter.Filter(items, "a[[]b", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity).ToList());
}
}

[TestMethod]
[DataRow("file[a-]", "file[-a]")]
[DataRow("file[!a-]", "file[!-a]")]
Expand All @@ -1023,13 +1048,18 @@ public void GlobClassWithATrailingDashMatchesTheDashLiterally()
[DataRow("file[0-", "file[0-")]
[DataRow("a]b", "a]b")]
[DataRow("[a-]x[0-", "[-a]x[0-")]
[DataRow("a[b", "a[[]b")]
[DataRow("log[", "log[[]")]
[DataRow("a[b[c", "a[[]b[[]c")]
[DataRow("[ab]x[c", "[ab]x[[]c")]
[DataRow("a[[]b", "a[[]b")]
[DataRow("x[^a]", "x[!a]")]
[DataRow("[^a-]", "[!-a]")]
[DataRow("[^0-9]", "[!0-9]")]
[DataRow("[^0-9a-f]", "[!0123456789abcdef]")]
[DataRow("[^]a]", "[!]a]")]
[DataRow("x[a^]", "x[a^]")]
[DataRow("x[^a", "x[^a")]
[DataRow("x[^a", "x[[]^a")]
public void ExpandGlobClassesRewritesOnlyClassesDotNetGlobMisreads(string token, string expected) =>
Assert.AreEqual(expected, TextFilter.ExpandGlobClasses(token));

Expand Down
23 changes: 19 additions & 4 deletions TextFilter/TextFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ internal static Dictionary<TextFilterTokenType, HashSet<string>> ExtractGlobFilt
/// rather than throwing: as a plain token it is left out of the optional tokens, so it neither
/// matches nor rejects anything; as a required token it matches everything; and as an excluded
/// token it excludes nothing.
/// <para>
/// Any other <c>[</c> with no closing <c>]</c> is a literal bracket, so <c>a[b</c> matches the text
/// <c>a[b</c> and not <c>ab</c>, the same as the escaped form <c>a[[]b</c>.
/// </para>
/// </remarks>
public static bool DoesMatchGlob(string text, string filter, TextFilterMatchOptions textFilterMatchOptions, TextFilterCaseSensitivity caseSensitivity = TextFilterCaseSensitivity.CaseSensitive)
{
Expand Down Expand Up @@ -503,15 +507,19 @@ internal static IReadOnlyList<string> ExpandGlobStarSegments(string maskedToken)
// explicit character list it means, which it does read correctly, negation and case folding
// included: "[0-9a-f]" becomes "[0123456789abcdef]" and "[a-z-]" becomes "[-abc...z]". A lone
// letter or digit range is left for DotNet.Glob to match natively, since it reads that form
// correctly and it may span far more characters than are worth listing. A class with no closing
// "]" is left alone and stays unparseable.
// correctly and it may span far more characters than are worth listing.
// A "[" with no closing "]" is a literal, as fnmatch and bash treat it, so it is written as "[[]".
// DotNet.Glob would otherwise read it as a class running to the end of the token, and "a[b" would
// match "ab" but not "a[b". The exception is a class left open after a dash ("file[0-"), a range
// still being typed: it is left alone and stays unparseable, so it is ignored as documented on
// DoesMatchGlob.
// Returns null for a class that cannot be written as a list: one whose ranges span more than
// MaxGlobClassMembers characters, or that has no members at all because every range is reversed.
private const int MaxGlobClassMembers = 4096;

internal static string? ExpandGlobClasses(string token)
{
if (!token.Contains(']'))
if (!token.Contains('['))
{
return token;
}
Expand All @@ -529,7 +537,14 @@ internal static IReadOnlyList<string> ExpandGlobStarSegments(string maskedToken)
int close = FindGlobClassEnd(token, open, out bool negated, out int contentStart);
if (close < 0)
{
break;
if (token.EndsWith('-'))
{
break;
}

result.Append(token, index, open - index).Append("[[]");
index = open + 1;
continue;
}

string content = token[contentStart..close];
Expand Down
Loading