From fbe02896ef6d9a86d985afb4b22a07f8dc043977 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sat, 10 Oct 2026 23:30:39 +0000 Subject: [PATCH] fix(items): run the enumeration of Get-ChildItem2 with the invariant culture The change of the previous commit was necessary but not enough. AlphaFS 2.2 also matches the filter: it builds a case-insensitive regular expression from it that takes the culture of the thread when it is created. With tr-TR the enumeration itself returned nothing for index* in a folder with Index.txt, in both editions (Directory.EnumerateFileSystemEntries of AlphaFS returned an empty list, .NET returned Index.txt), so the new test still failed on a build that had the previous change alone. The enumeration, that is the creation of the enumerator and every MoveNext, now runs with the invariant culture, which is restored before an item reaches the pipeline, so that no later command sees it. No other cmdlet passes a pattern of the user to AlphaFS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 60 +++++++++++++++++++---- Tests/ItemCmdlets.Tests.ps1 | 8 +-- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 8a31073..82dcdf1 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -150,8 +150,8 @@ namespace NTFSSecurity // for itself. Before 5.0.0, [1] was read as a character class, so a file with brackets in its name was not // returned for its name. The enumeration returns every item for *.* as Windows does, so the comparison does // too; with the dot as an ordinary character, it would drop the items without a dot, most folders. - // CultureInvariant: the enumeration ignores case without the culture of the thread, and with tr-TR the - // comparison would otherwise treat I and i as different letters and drop items that the enumeration matched. + // CultureInvariant: like the enumeration (see MoveNextInvariant), the comparison would otherwise treat I and i as + // different letters with tr-TR and drop items that the enumeration returned. var pattern = filter == "*.*" ? "*" : filter; wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); @@ -208,18 +208,33 @@ namespace NTFSSecurity { if (directory) { - var files = di.EnumerateDirectories(filter, global::System.IO.SearchOption.TopDirectoryOnly); - WriteFileSystemInfoCollection(files.GetEnumerator()); + IEnumerator files; + using (new InvariantCultureScope()) + { + files = di.EnumerateDirectories(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + } + + WriteFileSystemInfoCollection(files); } else if (file) { - var files = di.EnumerateFiles(filter, global::System.IO.SearchOption.TopDirectoryOnly); - WriteFileSystemInfoCollection(files.GetEnumerator()); + IEnumerator files; + using (new InvariantCultureScope()) + { + files = di.EnumerateFiles(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + } + + WriteFileSystemInfoCollection(files); } else { - var files = di.EnumerateFileSystemInfos(filter, global::System.IO.SearchOption.TopDirectoryOnly); - WriteFileSystemInfoCollection(files.GetEnumerator()); + IEnumerator files; + using (new InvariantCultureScope()) + { + files = di.EnumerateFileSystemInfos(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + } + + WriteFileSystemInfoCollection(files); } } @@ -286,7 +301,7 @@ namespace NTFSSecurity protected void WriteFileSystemInfoCollection(IEnumerator fileSystemInfos) { - while (fileSystemInfos.MoveNext()) + while (MoveNextInvariant(fileSystemInfos)) { FileSystemInfo current = (FileSystemInfo)fileSystemInfos.Current; if (!wildcard.IsMatch(current.Name)) @@ -359,5 +374,32 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // AlphaFS 2.2 matches the filter with a regular expression that takes the culture of the thread, and with tr-TR the + // letters I and i differ: index* did not match Index.txt. The enumeration therefore runs with the invariant culture, + // which is back before an item reaches the pipeline, so that no later command sees it. + private static bool MoveNextInvariant(IEnumerator enumerator) + { + using (new InvariantCultureScope()) + { + return enumerator.MoveNext(); + } + } + + private sealed class InvariantCultureScope : IDisposable + { + private readonly System.Globalization.CultureInfo previous; + + public InvariantCultureScope() + { + previous = System.Threading.Thread.CurrentThread.CurrentCulture; + System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; + } + + public void Dispose() + { + System.Threading.Thread.CurrentThread.CurrentCulture = previous; + } + } } } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 0d907db..9adbfe5 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -159,10 +159,10 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',') } - # With the culture of the thread tr-TR, a case-insensitive comparison that is not culture invariant treats I and i as - # different letters: the cmdlet dropped the items whose names differ in the case of an I from the pattern, which - # the enumeration had matched without regard to the culture. INDEX* is the control: both sides turn I into the - # dotless letter. + # With the culture of the thread tr-TR, a case-insensitive regular expression that is not culture invariant treats I and i + # as different letters. AlphaFS matches the filter that way, and so did the comparison of the cmdlet, so the cmdlet + # dropped the items whose names differ from the filter in the case of an I. INDEX* is the control: both sides turn I + # into the dotless letter. It 'Should match -Filter ignoring case in Turkish, where I and i are different letters' -ForEach @( @{ Filter = 'index*'; Expected = 'Index.txt' } @{ Filter = 'INDEX*'; Expected = 'Index.txt' }