Skip to content
Merged
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
21 changes: 21 additions & 0 deletions TextFilter.Test/TextFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -771,4 +771,25 @@
"A pattern evicted by the bound should simply be recompiled, not answer differently.");
Assert.IsFalse(TextFilter.IsMatch("hello world", "^goodbye", TextFilterType.Regex, TextFilterMatchOptions.ByWholeString));
}

[TestMethod]
[DataRow("file[0-")]
[DataRow("[a-")]
[DataRow("a[b-")]
[DataRow("[!a-")]
[DataRow("*[0-")]
[DataRow("[a-]")]
public void GlobWithAHalfTypedRangeDoesNotThrow(string pattern)
{
// DotNet.Glob's tokeniser throws IndexOutOfRangeException on a range left open after the dash,
// which every type-ahead filter passes through while the user types "file[0-9].txt". An
// unparseable token degrades to match-anything, as an invalid regex already does.
foreach (TextFilterCaseSensitivity caseSensitivity in new[] { TextFilterCaseSensitivity.CaseSensitive, TextFilterCaseSensitivity.CaseInsensitive })
{
Assert.IsTrue(TextFilter.IsMatch("file1.txt", pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity));
CollectionAssert.AreEqual(
new List<string> { "file1.txt" },
TextFilter.Filter(["file1.txt"], pattern, TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, caseSensitivity).ToList());

Check warning on line 792 in TextFilter.Test/TextFilterTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TextFilter&issues=AaDbxUanXIrpbw8WV3D3&open=AaDbxUanXIrpbw8WV3D3&pullRequest=108
}
}
}
24 changes: 21 additions & 3 deletions TextFilter/TextFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(1);

[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", Justification = "Not available in older frameworks")]
private static Regex RegexMatchAnything() => new(".*", RegexOptions.Compiled);

Check warning on line 126 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 126 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 126 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 126 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

private static Glob GlobMatchAnything() => Glob.Parse("*");

/// <summary>
/// Gets a hint for the specified filter type.
/// </summary>
Expand Down Expand Up @@ -275,7 +277,7 @@

return ExcludedTokenPrefixes.Contains(prefix)
? TextFilterTokenType.Excluded
: RequiredTokenPrefixes.Contains(prefix)

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 280 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.
? TextFilterTokenType.Required
: TextFilterTokenType.Optional;
})
Expand All @@ -300,6 +302,10 @@
/// <param name="textFilterMatchOptions">The options for matching text filters.</param>
/// <param name="caseSensitivity">Whether the match distinguishes uppercase from lowercase.</param>
/// <returns><c>true</c> if the text matches the glob filter pattern; otherwise, <c>false</c>.</returns>
/// <remarks>
/// A token that cannot be parsed as a glob, such as the half-typed range <c>file[0-</c>, matches
/// everything rather than throwing, the same way an invalid regex pattern does.
/// </remarks>
public static bool DoesMatchGlob(string text, string filter, TextFilterMatchOptions textFilterMatchOptions, TextFilterCaseSensitivity caseSensitivity = TextFilterCaseSensitivity.CaseSensitive)
{
Ensure.NotNull(text);
Expand Down Expand Up @@ -400,9 +406,21 @@

if (!GlobCache.TryGetValue(cacheKey, out Glob? glob))
{
glob = caseSensitivity is TextFilterCaseSensitivity.CaseInsensitive
? Glob.Parse(filterToken, CaseInsensitiveGlobOptions)
: Glob.Parse(filterToken);
try
{
glob = caseSensitivity is TextFilterCaseSensitivity.CaseInsensitive
? Glob.Parse(filterToken, CaseInsensitiveGlobOptions)
: Glob.Parse(filterToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
// DotNet.Glob's tokeniser throws IndexOutOfRangeException on a range left open after the
// dash ("file[0-"), which is ordinary intermediate input while someone types a range into
// a filter box. Degrade the way an invalid regex does: match anything, and cache that so
// the exception is not raised again on every keystroke. Caught broadly so the next
// tokeniser bug is contained too.
glob = GlobMatchAnything();
}

AddBounded(GlobCache, cacheKey, glob);
}
Expand Down
Loading