diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 313d6d9..39d8b21 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -401,13 +401,14 @@ namespace NTFSSecurity // Windows PowerShell 5.1 applies to Get-ChildItem -Filter, in both editions, so that the two editions and // the short-name based matching that a past fix removed do not disagree. The rules come from // FsRtlIsNameInExpression with the Win32 translation of a DOS pattern (DOS_STAR, DOS_QM, DOS_DOT): an - // asterisk matches a run of characters (but not past a dot that a following dot token still has to - // account for); a question mark matches one character that is not a dot, or nothing where the name has - // no more characters left or the next one is a dot; and a dot matches a literal dot in the name, or - // nothing where the name has no more characters left, which is why "Report.*" also returns "Report" and - // "Report." returns only the names without a dot. Comparisons are case-insensitive with the invariant - // culture, never the thread culture, matching the enumeration (see InvariantCultureScope). Every - // character other than an asterisk or a question mark is ordinary, including a bracket and a backtick. + // asterisk matches a run of characters (but not past the LAST dot in the name, which a following dot + // token still has to account for); a question mark matches one character that is not a dot, or nothing + // where the name has no more characters left or the next one is a dot; and a dot matches a literal dot in + // the name, or nothing where the name has no more characters left, which is why "Report.*" also returns + // "Report" and "Report." returns only the names without a dot. Comparisons are case-insensitive with the + // invariant culture, never the thread culture, matching the enumeration (see InvariantCultureScope). + // Every character other than an asterisk or a question mark is ordinary, including a bracket and a + // backtick. private static class DosWildcard { private enum TokenKind @@ -521,17 +522,19 @@ namespace NTFSSecurity if (tok.Kind == TokenKind.Star) { // A star that is directly followed by a dot (a DOS_DOT or an ordinary dot) must leave that dot - // for the dot token to account for: it cannot run past the next dot in the name, which is why - // "Rep*." does not return "Report.txt" although a plain "*" could reach the end of the name by - // crossing the dot. A star with anything else after it, including another star, is free to - // cross dots, which is why "**." returns every item and "R*t" returns "Report.txt". + // for the dot token to account for: it may cross every dot in the name but the LAST one (Win32 + // DOS_STAR), which is why "Rep*." does not return "Report.txt" although a plain "*" could reach + // the end of the name by crossing the dot, and why "*.?" reaches the final segment of a name + // with two or more dots, such as "archive.tar.gz". A star with anything else after it, + // including another star, is free to cross every dot, which is why "**." returns every item + // and "R*t" returns "Report.txt". var max = name.Length; if (ti + 1 < tokens.Count && tokens[ti + 1].Kind == TokenKind.Dot) { - var nextDot = name.IndexOf('.', ni); - if (nextDot >= 0) + var lastDot = name.LastIndexOf('.'); + if (lastDot >= ni) { - max = nextDot; + max = lastDot; } } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 8d8cb73..9a15360 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -166,9 +166,10 @@ Describe 'Get-ChildItem2' { @{ Filter = 'Page.ht?'; Expected = @('Page.htm') } @{ Filter = 'Page2.h*'; Expected = @('Page2.html') } @{ Filter = 'Page2.'; Expected = @() } + @{ Filter = 'LONGFI~1*'; Expected = @() } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory - foreach ($name in 'Page.htm', 'Page2.html') { + foreach ($name in 'Page.htm', 'Page2.html', 'LongFileName') { $file = Join-Path -Path $folder -ChildPath $name Assert-TestSandboxPath -Sandbox $sandbox -Path $file Set-Content -LiteralPath $file -Value $name @@ -344,6 +345,37 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') } + # A star in front of a dot token may cross every dot in the name but the LAST one (Win32 DOS_STAR), not stop + # at the first one: with names that have two or more dots, a pattern such as "*.?" still has to reach the + # last dot before the dot token and the question mark account for the final segment. The 8.3-compliant + # names of the truth table above have at most one dot, where the first dot and the last dot are the same + # position, so they cannot tell the two rules apart; this fixture uses names with two or three dots + # instead, including the realistic ones from the WI. Expected values are pinned with the real name of each + # item, confirmed with `fsutil file setshortname` so that a short name the volume might otherwise assign + # cannot also produce a match. + It 'Should match -Filter "" on a name with two or more dots the way Windows PowerShell 5.1 does' -ForEach @( + @{ Filter = '*.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.??'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.???'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.?.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.*.*'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'one.two.three', 'v1.2.3', 'x.y.z.w') } + @{ Filter = 'a*.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.??'; Expected = @('a.b.c', 'a.b.cd') } + @{ Filter = 'x.*.?'; Expected = @('x.y.z.w') } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMultiDot' -Directory + foreach ($name in 'a.b.c', 'a.b.cd', 'x.y.z.w', 'my.file.tx', 'one.two.three', 'archive.tar.gz', 'jquery.min.js', 'my.file.name.txt', 'v1.2.3', 'index.d.ts') { + $file = Join-Path -Path $folder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value $name + } + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') + } + It 'Should apply a -Filter at every level of a recursive listing, directories included' { $recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory $paths = @(