From 0028ccd23278df919b5c154bb29f9e8a15019633 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sat, 10 Oct 2026 23:19:09 +0000 Subject: [PATCH] fix(items): match the filter of Get-ChildItem2 without the culture of the thread With the culture of the thread tr-TR, the case-insensitive WildcardPattern that compares each name with the filter treats I and i as different letters. The enumeration had already matched the name without regard to the culture, so the cmdlet dropped the items whose names differ from the filter in the case of an I: -Filter 'index*' returned nothing for Index.txt, and -Filter '*.ini' did not return SETTINGS.INI. Both editions. The independent review of the coverage report found it; the scan of culture-sensitive calls had not listed WildcardPattern. The test sets the culture of the thread to tr-TR. On the previous build it fails in the four configurations (3 of 134 cases of ItemCmdlets.Tests.ps1: index*, *.ini, and *.INI; INDEX* is the control). The pattern is now WildcardOptions.CultureInvariant. The changelog entry of the identity comparison named the operator -eq, which tr-TR never affected (probes on the previous build: -eq, -contains, and -in return True for every case); it now names the method Equals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 11 ++++++--- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 4 ++- Tests/ItemCmdlets.Tests.ps1 | 30 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b28ba..4e79419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -439,11 +439,16 @@ The format is based on - Fix [Get-ChildItem2](Docs/Cmdlets/Get-ChildItem2.md) with `-Hidden`, which omitted the first hidden item unless `-Force` was also supplied -- Fix the comparison of an identity with a name, such as - `$entry.Account -eq 'nt authority\system'`, in the Turkish culture +- Fix the comparison of an identity with a name by its `Equals` method, such + as `$entry.Account.Equals('nt authority\system')`, in the Turkish culture (`tr-TR`): it changed the case of both names with the culture of the thread, which turns `I` into a dotless `ı`, so an account whose name holds an uppercase `I`, such as `NT AUTHORITY\SYSTEM` or `BUILTIN\Administrators`, - did not match the same name in another case + did not match the same name in another case. The operators `-eq`, + `-contains`, and `-in` were not affected +- Fix [Get-ChildItem2](Docs/Cmdlets/Get-ChildItem2.md) with `-Filter` in the + Turkish culture (`tr-TR`), which dropped the items whose names differ from + the filter in the case of an `I`, such as `Index.txt` for `-Filter 'index*'` + or `SETTINGS.INI` for `-Filter '*.ini'` [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 7e8b05c..8a31073 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -150,8 +150,10 @@ 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. var pattern = filter == "*.*" ? "*" : filter; - wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase); + wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode"); diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index ea1c99f..0d907db 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -159,6 +159,36 @@ 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. + 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' } + @{ Filter = '*.ini'; Expected = 'desktop.ini,SETTINGS.INI' } + @{ Filter = '*.INI'; Expected = 'desktop.ini,SETTINGS.INI' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterTurkish' -Directory + foreach ($name in 'Index.txt', 'desktop.ini', 'SETTINGS.INI', 'Other.txt') { + $file = Join-Path -Path $folder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value $name + } + $original = [System.Threading.Thread]::CurrentThread.CurrentCulture + try { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR') + 'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything' + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -ErrorAction Stop) + } + finally { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $original + } + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected + } + # Only * and ? are wildcards in -Filter. A bracket stands for itself, so a file with brackets in its name is found # by its name, as Get-ChildItem finds it, and the file that the brackets would select as a character class is not. # Before 5.0.0, the cmdlet read [1] as a character class and returned nothing.