Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ namespace System.IO.Enumeration
{
internal static class FileSystemEnumerableFactory
{
// Wildcard characters used in pattern matching
private static ReadOnlySpan<char> SimpleWildcards => "*?";
private static ReadOnlySpan<char> ExtendedWildcards => "\"<>*?";

/// <summary>
/// Validates the directory and expression strings to check that they have no invalid characters, any special DOS wildcard characters in Win32 in the expression get replaced with their proper escaped representation, and if the expression string begins with a directory name, the directory name is moved and appended at the end of the directory string.
/// </summary>
Expand Down Expand Up @@ -98,58 +102,113 @@ internal static bool NormalizeInputs(ref string directory, ref string expression
return isDirectoryModified;
}

private static bool MatchesPattern(string expression, ReadOnlySpan<char> name, EnumerationOptions options)
/// <summary>
/// Returns a delegate that checks whether a file name matches the given expression.
/// The delegate is optimized based on the pattern type (e.g., StartsWith, EndsWith, Contains).
/// </summary>
private static Func<ReadOnlySpan<char>, bool> GetFileNameMatcher(string expression, EnumerationOptions options)
{
bool ignoreCase = (options.MatchCasing == MatchCasing.PlatformDefault && !PathInternal.IsCaseSensitive)
|| options.MatchCasing == MatchCasing.CaseInsensitive;

return options.MatchType switch
StringComparison comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
bool useExtendedWildcards = options.MatchType == MatchType.Win32;

// Check for special patterns that can be optimized
if (expression == "*")
{
MatchType.Simple => FileSystemName.MatchesSimpleExpression(expression.AsSpan(), name, ignoreCase),
MatchType.Win32 => FileSystemName.MatchesWin32Expression(expression.AsSpan(), name, ignoreCase),
_ => throw new ArgumentOutOfRangeException(nameof(options)),
};
// Match all
return _ => true;
}

if (expression.Length > 1)
{
bool startsWithStar = expression[0] == '*';
bool endsWithStar = expression[^1] == '*';

// Determine which wildcards to check for (extended wildcards include DOS special characters)
ReadOnlySpan<char> wildcards = useExtendedWildcards ? ExtendedWildcards : SimpleWildcards;

if (startsWithStar && endsWithStar)
{
// Pattern: *literal* (Contains)
ReadOnlySpan<char> middle = expression.AsSpan(1, expression.Length - 2);
if (!middle.ContainsAny(wildcards))
{
string contains = middle.ToString();
return name => name.Contains(contains, comparison);
}
}
else if (startsWithStar)
{
// Pattern: *literal (EndsWith)
ReadOnlySpan<char> suffix = expression.AsSpan(1);
if (!suffix.ContainsAny(wildcards))
{
string endsWith = suffix.ToString();
return name => name.EndsWith(endsWith, comparison);
}
}
else if (endsWithStar)
{
// Pattern: literal* (StartsWith)
ReadOnlySpan<char> prefix = expression.AsSpan(0, expression.Length - 1);
if (!prefix.ContainsAny(wildcards))
{
string startsWith = prefix.ToString();
return name => name.StartsWith(startsWith, comparison);
}
}
}

// Fall back to the full pattern matching algorithm
return useExtendedWildcards
? name => FileSystemName.MatchesWin32Expression(expression.AsSpan(), name, ignoreCase)
: name => FileSystemName.MatchesSimpleExpression(expression.AsSpan(), name, ignoreCase);
}

internal static IEnumerable<string> UserFiles(string directory,
string expression,
EnumerationOptions options)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<string>(
directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
!entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
!entry.IsDirectory && matcher(entry.FileName)
};
}

internal static IEnumerable<string> UserDirectories(string directory,
string expression,
EnumerationOptions options)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<string>(
directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
entry.IsDirectory && matcher(entry.FileName)
};
}

internal static IEnumerable<string> UserEntries(string directory,
string expression,
EnumerationOptions options)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<string>(
directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
MatchesPattern(expression, entry.FileName, options)
matcher(entry.FileName)
};
}

Expand All @@ -159,14 +218,15 @@ internal static IEnumerable<FileInfo> FileInfos(
EnumerationOptions options,
bool isNormalized)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<FileInfo>(
directory,
(ref FileSystemEntry entry) => (FileInfo)entry.ToFileSystemInfo(),
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
!entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
!entry.IsDirectory && matcher(entry.FileName)
};
}

Expand All @@ -176,14 +236,15 @@ internal static IEnumerable<DirectoryInfo> DirectoryInfos(
EnumerationOptions options,
bool isNormalized)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<DirectoryInfo>(
directory,
(ref FileSystemEntry entry) => (DirectoryInfo)entry.ToFileSystemInfo(),
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
entry.IsDirectory && matcher(entry.FileName)
};
}

Expand All @@ -193,14 +254,15 @@ internal static IEnumerable<FileSystemInfo> FileSystemInfos(
EnumerationOptions options,
bool isNormalized)
{
Func<ReadOnlySpan<char>, bool> matcher = GetFileNameMatcher(expression, options);
return new FileSystemEnumerable<FileSystemInfo>(
directory,
(ref FileSystemEntry entry) => entry.ToFileSystemInfo(),
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
MatchesPattern(expression, entry.FileName, options)
matcher(entry.FileName)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ public static void SimpleMatch(string expression, string name, bool ignoreCase,
{ "*foo", "nofoo", true, true },
{ "*foo", "NoFOO", true, true },
{ "*foo", "noFOO", false, false },
// StartsWith patterns (literal*)
{ "foo*", "foo", false, true },
{ "foo*", "foo", true, true },
{ "foo*", "FOO", false, false },
{ "foo*", "FOO", true, true },
{ "foo*", "foobar", false, true },
{ "foo*", "FooBar", true, true },
{ "foo*", "FOOBAR", false, false },
{ "foo*", "FOOBAR", true, true },
{ "foo*", "barfoo", false, false },
{ "foo*", "barfoo", true, false },
{ "pre*", "prefix", true, true },
{ "pre*", "PRE", true, true },
{ "pre*", "pre", false, true },
{ "pre*", "notpre", true, false },
// Contains patterns (*literal*)
{ "*foo*", "foo", false, true },
{ "*foo*", "foo", true, true },
{ "*foo*", "FOO", false, false },
{ "*foo*", "FOO", true, true },
{ "*foo*", "foobar", false, true },
{ "*foo*", "FooBar", true, true },
{ "*foo*", "barfoo", false, true },
{ "*foo*", "barfoo", true, true },
{ "*foo*", "barfoobar", false, true },
{ "*foo*", "BARFOOBAR", true, true },
{ "*foo*", "BARFOOBAR", false, false },
{ "*foo*", "bar", false, false },
{ "*foo*", "bar", true, false },
{ "*mid*", "beginmiddleend", true, true },
{ "*mid*", "mid", true, true },
{ "*mid*", "midend", true, true },
{ "*mid*", "beginmid", true, true },
{ "*mid*", "nomatch", true, false },
{ @"*", @"foo.txt", true, true },
{ @".", @"foo.txt", true, false },
{ @".", @"footxt", true, false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,60 @@ public void GetFiles_ExtendedDosWildcards_Unix()
results = GetFiles(testDirectory.FullName, "*>*");
Assert.Equal(new string[] { fileThree.FullName }, results);
}

[Fact]
public void GetFiles_StartsWithPattern()
{
DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
FileInfo prefixOne = new FileInfo(Path.Combine(testDirectory.FullName, "prefixOne.txt"));
FileInfo prefixTwo = new FileInfo(Path.Combine(testDirectory.FullName, "prefixTwo.txt"));
FileInfo other = new FileInfo(Path.Combine(testDirectory.FullName, "other.txt"));
prefixOne.Create().Dispose();
prefixTwo.Create().Dispose();
other.Create().Dispose();

string[] results = GetFiles(testDirectory.FullName, "prefix*");
FSAssert.EqualWhenOrdered(new string[] { prefixOne.FullName, prefixTwo.FullName }, results);

results = GetFiles(testDirectory.FullName, "prefix*", new EnumerationOptions { MatchType = MatchType.Simple });
FSAssert.EqualWhenOrdered(new string[] { prefixOne.FullName, prefixTwo.FullName }, results);
}

[Fact]
public void GetFiles_EndsWithPattern()
{
DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
FileInfo txtOne = new FileInfo(Path.Combine(testDirectory.FullName, "one.txt"));
FileInfo txtTwo = new FileInfo(Path.Combine(testDirectory.FullName, "two.txt"));
FileInfo logFile = new FileInfo(Path.Combine(testDirectory.FullName, "app.log"));
txtOne.Create().Dispose();
txtTwo.Create().Dispose();
logFile.Create().Dispose();

string[] results = GetFiles(testDirectory.FullName, "*.txt");
FSAssert.EqualWhenOrdered(new string[] { txtOne.FullName, txtTwo.FullName }, results);

results = GetFiles(testDirectory.FullName, "*.txt", new EnumerationOptions { MatchType = MatchType.Simple });
FSAssert.EqualWhenOrdered(new string[] { txtOne.FullName, txtTwo.FullName }, results);
}

[Fact]
public void GetFiles_ContainsPattern()
{
DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
FileInfo middleOne = new FileInfo(Path.Combine(testDirectory.FullName, "amiddleb.txt"));
FileInfo middleTwo = new FileInfo(Path.Combine(testDirectory.FullName, "xmiddley.log"));
FileInfo noMatch = new FileInfo(Path.Combine(testDirectory.FullName, "other.txt"));
middleOne.Create().Dispose();
middleTwo.Create().Dispose();
noMatch.Create().Dispose();

string[] results = GetFiles(testDirectory.FullName, "*middle*");
FSAssert.EqualWhenOrdered(new string[] { middleOne.FullName, middleTwo.FullName }, results);

results = GetFiles(testDirectory.FullName, "*middle*", new EnumerationOptions { MatchType = MatchType.Simple });
FSAssert.EqualWhenOrdered(new string[] { middleOne.FullName, middleTwo.FullName }, results);
}
}

public class PatternTransformTests_DirectoryInfo : PatternTransformTests_Directory
Expand Down
Loading