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
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;

namespace System.IO.Enumeration
{
internal static class FileSystemEnumerableFactory
{
// Filter modes for file system enumeration
private enum FileSystemEntryType
{
All,
Files,
Directories
}

/// <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,16 +105,147 @@ 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 predicate that checks whether a file entry matches the given expression and entry type.
/// The predicate is optimized based on the pattern type (e.g., StartsWith, EndsWith, Contains).
/// </summary>
private static FileSystemEnumerable<T>.FindPredicate GetPredicate<T>(string expression, EnumerationOptions options, FileSystemEntryType entryType)
{
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;

// Determine which wildcards to check for (extended wildcards include DOS special characters)
SearchValues<char> wildcards = useExtendedWildcards ? FileSystemName.s_extendedWildcards : FileSystemName.s_simpleWildcards;

// Check for special patterns that can be optimized
if (expression == "*")
{
// Match all - only need to check entry type
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) => !entry.IsDirectory,
FileSystemEntryType.Directories => (ref FileSystemEntry entry) => entry.IsDirectory,
_ => (ref FileSystemEntry entry) => true
};
}

// Check for literal pattern (no wildcards and no escape characters) - use simple Equals
// The backslash is an escape character that needs to be processed by the full matcher
if (!expression.AsSpan().ContainsAny(wildcards) && !expression.Contains('\\'))
{
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) => !entry.IsDirectory && entry.FileName.Equals(expression, comparison),
FileSystemEntryType.Directories => (ref FileSystemEntry entry) => entry.IsDirectory && entry.FileName.Equals(expression, comparison),
_ => (ref FileSystemEntry entry) => entry.FileName.Equals(expression, comparison)
};
}

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

switch ((startsWithStar, endsWithStar))
{
case (true, true):
{
// Pattern: *literal* (Contains)
ReadOnlySpan<char> middle = expression.AsSpan(1, expression.Length - 2);
if (!middle.ContainsAny(wildcards))
{
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) => !entry.IsDirectory && entry.FileName.Contains(expression.AsSpan(1, expression.Length - 2), comparison),
FileSystemEntryType.Directories => (ref FileSystemEntry entry) => entry.IsDirectory && entry.FileName.Contains(expression.AsSpan(1, expression.Length - 2), comparison),
_ => (ref FileSystemEntry entry) => entry.FileName.Contains(expression.AsSpan(1, expression.Length - 2), comparison)
};
}
break;
}

case (true, false):
{
// Pattern: *literal (EndsWith)
ReadOnlySpan<char> suffix = expression.AsSpan(1);
if (!suffix.ContainsAny(wildcards))
{
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) => !entry.IsDirectory && entry.FileName.EndsWith(expression.AsSpan(1), comparison),
FileSystemEntryType.Directories => (ref FileSystemEntry entry) => entry.IsDirectory && entry.FileName.EndsWith(expression.AsSpan(1), comparison),
_ => (ref FileSystemEntry entry) => entry.FileName.EndsWith(expression.AsSpan(1), comparison)
};
}
break;
}

case (false, true):
{
// Pattern: literal* (StartsWith)
ReadOnlySpan<char> prefix = expression.AsSpan(0, expression.Length - 1);
if (!prefix.ContainsAny(wildcards))
{
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) => !entry.IsDirectory && entry.FileName.StartsWith(expression.AsSpan(0, expression.Length - 1), comparison),
FileSystemEntryType.Directories => (ref FileSystemEntry entry) => entry.IsDirectory && entry.FileName.StartsWith(expression.AsSpan(0, expression.Length - 1), comparison),
_ => (ref FileSystemEntry entry) => entry.FileName.StartsWith(expression.AsSpan(0, expression.Length - 1), comparison)
};
}
break;
}

case (false, false):
{
// Check for prefix*suffix pattern
int starIndex = expression.IndexOf('*');
if (starIndex > 0)
{
// Pattern: prefix*suffix (StartsWith + EndsWith)
ReadOnlySpan<char> prefix = expression.AsSpan(0, starIndex);
ReadOnlySpan<char> suffix = expression.AsSpan(starIndex + 1);
if (!prefix.ContainsAny(wildcards) && !suffix.ContainsAny(wildcards))
{
int prefixLength = starIndex;
int suffixLength = expression.Length - starIndex - 1;
int minLength = prefixLength + suffixLength;
return entryType switch
{
FileSystemEntryType.Files => (ref FileSystemEntry entry) =>
!entry.IsDirectory &&
entry.FileName.Length >= minLength &&
entry.FileName.StartsWith(expression.AsSpan(0, prefixLength), comparison) &&
entry.FileName.EndsWith(expression.AsSpan(prefixLength + 1), comparison),
FileSystemEntryType.Directories => (ref FileSystemEntry entry) =>
entry.IsDirectory &&
entry.FileName.Length >= minLength &&
entry.FileName.StartsWith(expression.AsSpan(0, prefixLength), comparison) &&
entry.FileName.EndsWith(expression.AsSpan(prefixLength + 1), comparison),
_ => (ref FileSystemEntry entry) =>
entry.FileName.Length >= minLength &&
entry.FileName.StartsWith(expression.AsSpan(0, prefixLength), comparison) &&
entry.FileName.EndsWith(expression.AsSpan(prefixLength + 1), comparison)
};
}
}
break;
}
}
}

// Fall back to the full pattern matching algorithm
return (useExtendedWildcards, entryType) switch
{
MatchType.Simple => FileSystemName.MatchesSimpleExpression(expression.AsSpan(), name, ignoreCase),
MatchType.Win32 => FileSystemName.MatchesWin32Expression(expression.AsSpan(), name, ignoreCase),
_ => throw new ArgumentOutOfRangeException(nameof(options)),
(true, FileSystemEntryType.Files) => (ref FileSystemEntry entry) => !entry.IsDirectory && FileSystemName.MatchesWin32Expression(expression, entry.FileName, ignoreCase),
(true, FileSystemEntryType.Directories) => (ref FileSystemEntry entry) => entry.IsDirectory && FileSystemName.MatchesWin32Expression(expression, entry.FileName, ignoreCase),
(true, _) => (ref FileSystemEntry entry) => FileSystemName.MatchesWin32Expression(expression, entry.FileName, ignoreCase),
(false, FileSystemEntryType.Files) => (ref FileSystemEntry entry) => !entry.IsDirectory && FileSystemName.MatchesSimpleExpression(expression, entry.FileName, ignoreCase),
(false, FileSystemEntryType.Directories) => (ref FileSystemEntry entry) => entry.IsDirectory && FileSystemName.MatchesSimpleExpression(expression, entry.FileName, ignoreCase),
(false, _) => (ref FileSystemEntry entry) => FileSystemName.MatchesSimpleExpression(expression, entry.FileName, ignoreCase)
};
}

Expand All @@ -120,8 +258,7 @@ internal static IEnumerable<string> UserFiles(string directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
!entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<string>(expression, options, FileSystemEntryType.Files)
};
}

Expand All @@ -134,8 +271,7 @@ internal static IEnumerable<string> UserDirectories(string directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<string>(expression, options, FileSystemEntryType.Directories)
};
}

Expand All @@ -148,8 +284,7 @@ internal static IEnumerable<string> UserEntries(string directory,
(ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(),
options)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<string>(expression, options, FileSystemEntryType.All)
};
}

Expand All @@ -165,8 +300,7 @@ internal static IEnumerable<FileInfo> FileInfos(
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
!entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<FileInfo>(expression, options, FileSystemEntryType.Files)
};
}

Expand All @@ -182,8 +316,7 @@ internal static IEnumerable<DirectoryInfo> DirectoryInfos(
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
entry.IsDirectory && MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<DirectoryInfo>(expression, options, FileSystemEntryType.Directories)
};
}

Expand All @@ -199,8 +332,7 @@ internal static IEnumerable<FileSystemInfo> FileSystemInfos(
options,
isNormalized)
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
MatchesPattern(expression, entry.FileName, options)
ShouldIncludePredicate = GetPredicate<FileSystemInfo>(expression, options, FileSystemEntryType.All)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Text;

namespace System.IO.Enumeration
{
/// <summary>Provides methods for matching file system names.</summary>
public static class FileSystemName
{
// Wildcard characters used in pattern matching
internal static readonly SearchValues<char> s_simpleWildcards = SearchValues.Create("*?");
internal static readonly SearchValues<char> s_extendedWildcards = SearchValues.Create("\"<>*?");

/// <summary>Translates the given Win32 expression. Change '*' and '?' to '&lt;', '&gt;' and '"' to match Win32 behavior.</summary>
/// <param name="expression">The expression to translate.</param>
/// <returns>A string with the translated Win32 expression.</returns>
Expand Down Expand Up @@ -158,9 +162,7 @@ private static bool MatchPattern(ReadOnlySpan<char> expression, ReadOnlySpan<cha

// [MS - FSA] 2.1.4.4 Algorithm for Determining if a FileName Is in an Expression
// https://msdn.microsoft.com/en-us/library/ff469270.aspx
bool hasWildcards = useExtendedWildcards ?
expressionEnd.ContainsAny("\"<>*?") :
expressionEnd.ContainsAny('*', '?');
bool hasWildcards = expressionEnd.ContainsAny(useExtendedWildcards ? s_extendedWildcards : s_simpleWildcards);
if (!hasWildcards)
{
// Handle the special case of a single starting *, which essentially means "ends with"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,77 @@ public static void SimpleMatch(string expression, string name, bool ignoreCase,
{ "*", "", true, false },
{ "*", "ab", false, true },
{ "*", "AB", true, true },
// Literal patterns (no wildcards) - use Equals
{ "log.txt", "log.txt", false, true },
{ "log.txt", "log.txt", true, true },
{ "log.txt", "LOG.TXT", false, false },
{ "log.txt", "LOG.TXT", true, true },
{ "log.txt", "log.txt.bak", false, false },
{ "log.txt", "log.txt.bak", true, false },
{ "log.txt", "mylog.txt", false, false },
{ "log.txt", "mylog.txt", true, false },
{ "exact", "exact", false, true },
{ "exact", "EXACT", true, true },
{ "exact", "exactmatch", false, false },
{ "*foo", "foo", false, true },
{ "*foo", "foo", true, true },
{ "*foo", "FOO", false, false },
{ "*foo", "FOO", true, true },
{ "*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 },
// prefix*suffix patterns (StartsWith + EndsWith)
{ "pre*fix", "prefix", false, true },
{ "pre*fix", "prefix", true, true },
{ "pre*fix", "PREFIX", false, false },
{ "pre*fix", "PREFIX", true, true },
{ "pre*fix", "preFIX", false, false },
{ "pre*fix", "preFIX", true, true },
{ "pre*fix", "pre_extra_fix", false, true },
{ "pre*fix", "pre_extra_fix", true, true },
{ "pre*fix", "prefi", false, false },
{ "pre*fix", "prefi", true, false },
{ "pre*fix", "refix", false, false },
{ "pre*fix", "refix", true, false },
{ "file*txt", "file.txt", false, true },
{ "file*txt", "file123txt", false, true },
{ "file*txt", "filetxt", false, true },
{ "file*txt", "file", false, false },
{ "file*txt", "txt", false, false },
{ @"*", @"foo.txt", true, true },
{ @".", @"foo.txt", true, false },
{ @".", @"footxt", true, false },
Expand Down
Loading
Loading