You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1329 lines
75 KiB

<#
Tests the long-path item cmdlets of the module built in NTFSSecurity\bin\Release on files in a sandbox folder.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
)]
param ()
BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
# A path on the administrative share of the drive of the sandboxes is another volume for Windows, like a share of a
# file server.
$canUseAdminShare = Test-AdminShareAvailable
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
}
BeforeAll {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
Import-Module -Name $modulePath -Force -ErrorAction Stop
$sandbox = New-TestSandbox -Name 'ItemCmdlets'
Push-Location -LiteralPath $sandbox
}
AfterAll {
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'Get-ChildItem2' {
Context 'When -Path is a file' {
BeforeAll {
$folder = Join-Path -Path $sandbox -ChildPath 'ChildItem'
$file = Join-Path -Path $folder -ChildPath 'File.txt'
$other = Join-Path -Path $folder -ChildPath 'Folder\Other.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder, $file, $other
New-Item -ItemType Directory -Path (Split-Path -Path $other -Parent) -Force | Out-Null
Set-Content -LiteralPath $file -Value 'File'
Set-Content -LiteralPath $other -Value 'Other'
}
It 'Should return the file itself, like Get-ChildItem' {
$items = @(Get-ChildItem2 -Path $file -ErrorVariable childItemErrors -ErrorAction SilentlyContinue)
$childItemErrors | Should -BeNullOrEmpty
$items | Should -HaveCount 1
$items[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$items[0].Name | Should -BeExactly 'File.txt'
}
It 'Should continue with the next path after a file' {
$items = @(Get-ChildItem2 -Path $file, (Join-Path -Path $folder -ChildPath 'Folder') -ErrorAction SilentlyContinue)
$items.Name | Should -Be @('File.txt', 'Other.txt')
}
It 'Should return nothing for a file with -Directory' {
$items = @(Get-ChildItem2 -Path $file -Directory -ErrorVariable childItemErrors -ErrorAction SilentlyContinue)
$childItemErrors | Should -BeNullOrEmpty
$items | Should -BeNullOrEmpty
}
}
Context 'Attribute switches' {
BeforeAll {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Switches' -Directory
$attributeCases = @{
'Plain.txt' = 'Normal'
'Hidden.txt' = 'Hidden'
'System.txt' = 'System'
'ReadOnly.txt' = 'ReadOnly'
'HiddenSystem.txt' = 'Hidden, System'
'HiddenReadOnly.txt' = 'Hidden, ReadOnly'
'All.txt' = 'Hidden, ReadOnly, System'
}
foreach ($name in $attributeCases.Keys) {
$path = Join-Path -Path $folder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
Set-Content -LiteralPath $path -Value $name
[IO.File]::SetAttributes($path, [IO.FileAttributes] $attributeCases[$name])
}
}
It 'Should apply <Case> without broadening the other attribute filters' -ForEach @(
@{ Case = 'default'; Parameters = @{}; Expected = @('Plain.txt', 'System.txt', 'ReadOnly.txt') }
@{ Case = 'Force'; Parameters = @{ Force = $true }; Expected = @('Plain.txt', 'Hidden.txt', 'System.txt', 'ReadOnly.txt', 'HiddenSystem.txt', 'HiddenReadOnly.txt', 'All.txt') }
@{ Case = 'Hidden'; Parameters = @{ Hidden = $true }; Expected = @('Hidden.txt', 'HiddenSystem.txt', 'HiddenReadOnly.txt', 'All.txt') }
@{ Case = 'System'; Parameters = @{ System = $true }; Expected = @('System.txt') }
@{ Case = 'System with Force'; Parameters = @{ System = $true; Force = $true }; Expected = @('System.txt', 'HiddenSystem.txt', 'All.txt') }
@{ Case = 'ReadOnly'; Parameters = @{ ReadOnly = $true }; Expected = @('ReadOnly.txt') }
@{ Case = 'ReadOnly with Force'; Parameters = @{ ReadOnly = $true; Force = $true }; Expected = @('ReadOnly.txt', 'HiddenReadOnly.txt', 'All.txt') }
@{ Case = 'Hidden and System'; Parameters = @{ Hidden = $true; System = $true }; Expected = @('HiddenSystem.txt', 'All.txt') }
@{ Case = 'Hidden and ReadOnly'; Parameters = @{ Hidden = $true; ReadOnly = $true }; Expected = @('HiddenReadOnly.txt', 'All.txt') }
@{ Case = 'System and ReadOnly with Force'; Parameters = @{ System = $true; ReadOnly = $true; Force = $true }; Expected = @('All.txt') }
) {
$result = @(Get-ChildItem2 -Path $folder @Parameters -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',')
}
}
It 'Should include the first hidden item without requiring explicit -Force' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FirstHidden' -Directory
$file = Join-Path -Path $folder -ChildPath 'Only.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Hidden'
[IO.File]::SetAttributes($file, [IO.FileAttributes]::Hidden)
$result = @(Get-ChildItem2 -Path $folder -Hidden -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $file
}
Context 'Recursion, type filters, and depth' {
BeforeAll {
$tree = New-TestSandboxItem -Sandbox $sandbox -Name 'EnumerationTree' -Directory
$grandchild = Join-Path -Path $tree -ChildPath 'Child\Grandchild'
$paths = @('Root.txt', 'Child\Child.log', 'Child\Grandchild\Grand.TXT') | ForEach-Object { Join-Path -Path $tree -ChildPath $_ }
Assert-TestSandboxPath -Sandbox $sandbox -Path (@($grandchild) + @($paths))
New-Item -ItemType Directory -Path $grandchild -Force | Out-Null
foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Tree' }
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# The fixture for the -Filter truth table: 18 files and 4 folders, all 8.3-compliant (at most one dot,
# up to 8 characters of base name and 3 of extension, no spaces), so each real name is also its own
# short name and AlphaFS enumerates the same identity that the matcher compares.
$filterTruthTableFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterTruthTable' -Directory
$truthTableFiles = 'a', 'ab', 'abc', 'a.b', 'ab.c', 'abc.de', 'a.bcd', 'abcdefgh', 'abcdefgh.ijk', 'x.y', 'a1.b2', 'bca', 'cab.cd', 'ccc', 'c.c', 'bcabcabc', 'a2.b1', '1cb'
$truthTableDirs = 'bdir', 'bdir.cd', 'fold', 'fo.ld'
foreach ($name in $truthTableFiles) {
$path = Join-Path -Path $filterTruthTableFolder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
Set-Content -LiteralPath $path -Value $name
}
foreach ($name in $truthTableDirs) {
$path = Join-Path -Path $filterTruthTableFolder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
New-Item -ItemType Directory -Path $path | Out-Null
}
}
It 'Should return the exact tree for <Case>' -ForEach @(
@{ Case = 'immediate children'; Parameters = @{}; Expected = @('Child', 'Root.txt') }
@{ Case = 'all descendants'; Parameters = @{ Recurse = $true }; Expected = @('Child', 'Root.txt', 'Child\Grandchild', 'Child\Child.log', 'Child\Grandchild\Grand.TXT') }
@{ Case = 'depth zero'; Parameters = @{ Recurse = $true; Depth = 0 }; Expected = @('Child', 'Root.txt') }
@{ Case = 'depth one'; Parameters = @{ Recurse = $true; Depth = 1 }; Expected = @('Child', 'Root.txt', 'Child\Grandchild', 'Child\Child.log') }
@{ Case = 'depth two'; Parameters = @{ Recurse = $true; Depth = 2 }; Expected = @('Child', 'Root.txt', 'Child\Grandchild', 'Child\Child.log', 'Child\Grandchild\Grand.TXT') }
@{ Case = 'directories'; Parameters = @{ Recurse = $true; Directory = $true }; Expected = @('Child', 'Child\Grandchild') }
@{ Case = 'files'; Parameters = @{ Recurse = $true; File = $true }; Expected = @('Root.txt', 'Child\Child.log', 'Child\Grandchild\Grand.TXT') }
@{ Case = 'case-insensitive file filter'; Parameters = @{ Recurse = $true; File = $true; Filter = '*.txt' }; Expected = @('Root.txt', 'Child\Grandchild\Grand.TXT') }
) {
$result = @(Get-ChildItem2 -Path $tree @Parameters -ErrorAction Stop)
$relative = @($result | ForEach-Object { $_.FullName.Substring($tree.Length + 1) })
($relative | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',')
}
# The pattern must match the name of the item, not its short name (8.3), which Get-ChildItem in Windows PowerShell
# also compares: there, *.htm returns Page2.html on a volume that creates short names.
It 'Should return only the items whose name matches -Filter <Filter>' -ForEach @(
@{ Filter = '*.htm'; Expected = @('Page.htm') }
@{ Filter = 'Page?.html'; Expected = @('Page2.html') }
@{ Filter = 'PAGE*'; Expected = @('Page.htm', 'Page2.html') }
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
@{ Filter = 'Page.ht?'; Expected = @('Page.htm') }
@{ Filter = 'Page2.h*'; Expected = @('Page2.html') }
@{ Filter = 'Page2.'; Expected = @() }
fix(items): let an asterisk before a dot in -Filter cross every dot but the last An independent review found that the matcher of Get-ChildItem2 -Filter (commit 69cd8ed) bounded a star in front of a dot token at the FIRST dot of the name instead of the LAST one (Win32 DOS_STAR). The differential oracle of 69cd8ed used only 8.3-compliant names, which have at most one dot, so the first dot and the last dot are always the same position there and the bug could not show up. With a name that has two or more dots, such as &#34;archive.tar.gz&#34; or &#34;jquery.min.js&#34;, the star stopped too early and the rest of the pattern could not reach the final segment of the name, so a pattern such as &#34;*.?&#34;, &#34;*.??&#34;, or &#34;*.?.?&#34; wrongly returned nothing for these names, although Windows PowerShell 5.1 returns them. What a user sees changing: -Filter patterns of the shape &#34;a star followed by a dot&#34; (including &#34;*.?&#34;, &#34;*.??&#34;, &#34;*.???&#34;, a star in a multi-part pattern such as &#34;a*.?&#34; or &#34;x.*.?&#34;) now also match names with two or more dots, the same as Windows PowerShell 5.1, in both editions. A name with at most one dot is unaffected (that case was already correct). Fixed by bounding the star at `name.LastIndexOf(&#39;.&#39;)` instead of the first dot at or after the current position, and corrected the two comments that described the old, narrower rule. Also added a discriminating row to the existing short-name test (`-Filter &#39;LONGFI~1*&#39;` against a plain file named `LongFileName`, expected to return nothing): the three rows already in that test happen to give the same result whether the matcher reads the real name or the short name, so they could not have caught a regression to short-name matching; this row can. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): added a 9-row test with 10 names that have two or three dots (including the realistic ones from the review: archive.tar.gz, jquery.min.js, my.file.name.txt, v1.2.3, index.d.ts), expected values pinned from Windows PowerShell 5.1 with `fsutil file setshortname` (and, for one row, `fsutil 8dot3name strip`) so that a short name the volume assigns cannot also produce a match; and the LONGFI~1* row above. Both fail on the build of 69cd8ed and pass after this fix. The fix was checked with the reviewer&#39;s differential harness (C:\Temp\ntfs-rc9\review-probes-r3\diff5.ps1) against this build: 0 mismatches over two independent fixtures (732,630 real-name/pattern pairs with names of two or more dots, short names overridden with `fsutil file setshortname` so that a hit can only come from the real name), in addition to the 109,308 pairs of 8.3-compliant names checked for 69cd8ed. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
@{ Filter = 'LONGFI~1*'; Expected = @() }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory
fix(items): let an asterisk before a dot in -Filter cross every dot but the last An independent review found that the matcher of Get-ChildItem2 -Filter (commit 69cd8ed) bounded a star in front of a dot token at the FIRST dot of the name instead of the LAST one (Win32 DOS_STAR). The differential oracle of 69cd8ed used only 8.3-compliant names, which have at most one dot, so the first dot and the last dot are always the same position there and the bug could not show up. With a name that has two or more dots, such as &#34;archive.tar.gz&#34; or &#34;jquery.min.js&#34;, the star stopped too early and the rest of the pattern could not reach the final segment of the name, so a pattern such as &#34;*.?&#34;, &#34;*.??&#34;, or &#34;*.?.?&#34; wrongly returned nothing for these names, although Windows PowerShell 5.1 returns them. What a user sees changing: -Filter patterns of the shape &#34;a star followed by a dot&#34; (including &#34;*.?&#34;, &#34;*.??&#34;, &#34;*.???&#34;, a star in a multi-part pattern such as &#34;a*.?&#34; or &#34;x.*.?&#34;) now also match names with two or more dots, the same as Windows PowerShell 5.1, in both editions. A name with at most one dot is unaffected (that case was already correct). Fixed by bounding the star at `name.LastIndexOf(&#39;.&#39;)` instead of the first dot at or after the current position, and corrected the two comments that described the old, narrower rule. Also added a discriminating row to the existing short-name test (`-Filter &#39;LONGFI~1*&#39;` against a plain file named `LongFileName`, expected to return nothing): the three rows already in that test happen to give the same result whether the matcher reads the real name or the short name, so they could not have caught a regression to short-name matching; this row can. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): added a 9-row test with 10 names that have two or three dots (including the realistic ones from the review: archive.tar.gz, jquery.min.js, my.file.name.txt, v1.2.3, index.d.ts), expected values pinned from Windows PowerShell 5.1 with `fsutil file setshortname` (and, for one row, `fsutil 8dot3name strip`) so that a short name the volume assigns cannot also produce a match; and the LONGFI~1* row above. Both fail on the build of 69cd8ed and pass after this fix. The fix was checked with the reviewer&#39;s differential harness (C:\Temp\ntfs-rc9\review-probes-r3\diff5.ps1) against this build: 0 mismatches over two independent fixtures (732,630 real-name/pattern pairs with names of two or more dots, short names overridden with `fsutil file setshortname` so that a hit can only come from the real name), in addition to the 109,308 pairs of 8.3-compliant names checked for 69cd8ed. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
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
}
$result = @(Get-ChildItem2 -Path $folder -Filter $Filter -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',')
}
# 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 <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'
# The next stage of the pipeline reads the culture of the thread for each item, and the test reads it once more
# after the cmdlet has returned: Turkish both times shows that the cmdlet restores the culture and writes no
# item inside the scope of the invariant culture.
$result = @(Get-ChildItem2 -Path $folder -Filter $Filter -ErrorAction Stop |
ForEach-Object -Process { [pscustomobject]@{ Name = $_.Name; Culture = [System.Threading.Thread]::CurrentThread.CurrentCulture.Name } })
$cultureAfter = [System.Threading.Thread]::CurrentThread.CurrentCulture.Name
}
finally {
[System.Threading.Thread]::CurrentThread.CurrentCulture = $original
}
($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected
(@($result.Culture) | Select-Object -Unique) -join ',' | Should -BeExactly 'tr-TR' -Because 'the cmdlet writes each item outside the scope of the invariant culture'
$cultureAfter | Should -BeExactly 'tr-TR' -Because 'the cmdlet restores the culture of the caller'
}
# 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.
It 'Should find a file whose name contains brackets by that name with -Filter' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterBrackets' -Directory
foreach ($name in 'Report[1].txt', 'Report1.txt') {
$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 'Report[1].txt' -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].Name | Should -BeExactly 'Report[1].txt'
}
fix: pass on what a later command raises, and return every item for -Filter *.* A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message. Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns. The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*. Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
3 weeks ago
# The dot is an ordinary character of the pattern, but not in *.*, which Windows, Get-ChildItem, and .NET read as
# every item. Before 5.0.0, the cmdlet compared each name with the pattern again and dropped the items without a
# dot, files and folders alike, so that a listing of a tree with this filter missed most of its folders.
It 'Should return every item for -Filter *.*, also the ones without a dot in their names' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDot' -Directory
$paths = @('Page.htm', 'NoExtension', 'NoExtensionFolder') | ForEach-Object -Process { Join-Path -Path $folder -ChildPath $_ }
Assert-TestSandboxPath -Sandbox $sandbox -Path $paths
Set-Content -LiteralPath $paths[0] -Value 'Page'
Set-Content -LiteralPath $paths[1] -Value 'NoExtension'
New-Item -ItemType Directory -Path $paths[2] | Out-Null
$result = @(Get-ChildItem2 -Path $folder -Filter '*.*' -ErrorAction Stop)
fix: pass on what a later command raises, and return every item for -Filter *.* A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message. Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns. The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*. Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
3 weeks ago
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm'
}
docs(items): split -Filter paragraphs for platyPS, sharpen the PowerShell 7 claim, fix -Path wording An independent review found that platyPS 0.14.2, which generates the shipped help file from these pages, pairs up every two literal asterisks within one paragraph as markdown emphasis and drops them, even inside backticks: the -Filter parameter description and the matching paragraph of NOTES each had three or four literal asterisks in one paragraph, so `Get-Help Get-ChildItem2 -Parameter Filter` and the NOTES section showed `Report. ` instead of `` Report.* `` and ` . ` instead of a plain asterisk pattern, silently losing the character that the sentence was about. Both paragraphs are now several short paragraphs, each with at most one literal asterisk; a pattern that itself needs more than one asterisk, such as a plain asterisk, a dot, and an asterisk, is spelled out in words instead, the way the page already did before this feature. Verified by running `New-ExternalHelp` from platyPS 0.14.2 into a temporary folder (not the repository&#39;s help file) and confirming every pattern renders intact in the generated XML, then removing the folder. The same two paragraphs also said PowerShell 7&#39;s own `Get-ChildItem -Filter` returns nothing for any pattern that ends in a dot and returns every item only for a pattern of an asterisk, a dot, and one question mark. Measured directly on PowerShell 7.6.7, both overstated the difference: a trailing dot returns nothing there only when the character right before the dot is not an asterisk (a star right before the dot behaves the same as Windows PowerShell 5.1 and as Get-ChildItem2 there too), and a pattern of an asterisk, a dot, and two question marks also returns every item, not only one question mark. Both paragraphs now say exactly that. Also removed &#34;(Decision 27, item 4)&#34; from both paragraphs and from two test comments (the maintainer&#39;s agent already removed it from the integration branch; this avoids a conflicting edit), and changed one sentence of the DESCRIPTION section that said every -Path value must name a folder: a value that names a file returns that file, as the PARAMETERS section and NOTES already said and the cmdlet already did. No test changes: these are documentation wording changes only, and the page keeps its existing structure (headings, parameter block, NOTES list). Re-ran Tests\ItemCmdlets.Tests.ps1 (no code changed in this commit) to confirm the suite is still green: elevated-Desktop 193/0/0, elevated-Core 192/0/1, basic-Desktop 187/0/6, basic-Core 186/0/7 (passed/failed/skipped), three times, identical. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# The cmdlet now has one matcher, not two: every dot in -Filter whose single next
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# character is an asterisk or a question mark, or that has nothing after it, is a DOS_DOT, and *.*.* has
# two such dots, so it returns every item, the same as *.* and a plain *. Before this change, the cmdlet
# compared the name with the pattern a second time with the dots read as ordinary characters, so only a
# name with two literal dots matched.
It 'Should return every item for -Filter *.*.*, also the ones with one dot or none' {
fix: pass on what a later command raises, and return every item for -Filter *.* A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message. Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns. The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*. Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
3 weeks ago
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDots' -Directory
foreach ($name in 'Page.htm', 'NoExtension', 'Two.dots.txt') {
$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 '*.*.*' -ErrorAction Stop)
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,Page.htm,Two.dots.txt'
}
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1
docs(items): split -Filter paragraphs for platyPS, sharpen the PowerShell 7 claim, fix -Path wording An independent review found that platyPS 0.14.2, which generates the shipped help file from these pages, pairs up every two literal asterisks within one paragraph as markdown emphasis and drops them, even inside backticks: the -Filter parameter description and the matching paragraph of NOTES each had three or four literal asterisks in one paragraph, so `Get-Help Get-ChildItem2 -Parameter Filter` and the NOTES section showed `Report. ` instead of `` Report.* `` and ` . ` instead of a plain asterisk pattern, silently losing the character that the sentence was about. Both paragraphs are now several short paragraphs, each with at most one literal asterisk; a pattern that itself needs more than one asterisk, such as a plain asterisk, a dot, and an asterisk, is spelled out in words instead, the way the page already did before this feature. Verified by running `New-ExternalHelp` from platyPS 0.14.2 into a temporary folder (not the repository&#39;s help file) and confirming every pattern renders intact in the generated XML, then removing the folder. The same two paragraphs also said PowerShell 7&#39;s own `Get-ChildItem -Filter` returns nothing for any pattern that ends in a dot and returns every item only for a pattern of an asterisk, a dot, and one question mark. Measured directly on PowerShell 7.6.7, both overstated the difference: a trailing dot returns nothing there only when the character right before the dot is not an asterisk (a star right before the dot behaves the same as Windows PowerShell 5.1 and as Get-ChildItem2 there too), and a pattern of an asterisk, a dot, and two question marks also returns every item, not only one question mark. Both paragraphs now say exactly that. Also removed &#34;(Decision 27, item 4)&#34; from both paragraphs and from two test comments (the maintainer&#39;s agent already removed it from the integration branch; this avoids a conflicting edit), and changed one sentence of the DESCRIPTION section that said every -Path value must name a folder: a value that names a file returns that file, as the PARAMETERS section and NOTES already said and the cmdlet already did. No test changes: these are documentation wording changes only, and the page keeps its existing structure (headings, parameter block, NOTES list). Re-ran Tests\ItemCmdlets.Tests.ps1 (no code changed in this commit) to confirm the suite is still green: elevated-Desktop 193/0/0, elevated-Core 192/0/1, basic-Desktop 187/0/6, basic-Core 186/0/7 (passed/failed/skipped), three times, identical. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# applies to Get-ChildItem -Filter, in both editions: Report.* also returns Report,
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# which has no dot, and a trailing dot asks for the names without a dot. Report* is the control: without a
# dot in the pattern, the names with and without a dot are both returned, as before.
It 'Should return <Outcome> for -Filter "<Filter>"' -ForEach @(
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
@{ Filter = 'Report.*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' }
@{ Filter = 'Report*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' }
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
@{ Filter = 'Report.'; Expected = 'Report'; Outcome = 'only the name without a dot' }
@{ Filter = 'Rep*.'; Expected = 'Report'; Outcome = 'only the name without a dot' }
@{ Filter = '*.'; Expected = 'Other,Report'; Outcome = 'the names without a dot' }
@{ Filter = ''; Expected = 'Other,Report,Report.txt'; Outcome = 'every item, like no filter at all' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDotRules' -Directory
foreach ($name in 'Report', 'Report.txt', 'Other') {
$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 -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected
}
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# A fixed truth table, pinned with the literal result of Windows PowerShell 5.1's Get-ChildItem on 8.3-compliant
# names (at most one dot, up to 8 characters of base name and 3 of extension, no spaces), whose short name is
# its own real name, so AlphaFS and the cmdlet agree on which name they compare. The expected values are data,
# not the output of a run of Get-ChildItem: this test is its own oracle.
It 'Should match -Filter "<Filter>" the way Windows PowerShell 5.1 does' -ForEach @(
@{ Filter = '*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '*.'; Expected = @('1cb', 'a', 'ab', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'ccc', 'fold') }
@{ Filter = 'a.*'; Expected = @('a', 'a.b', 'a.bcd') }
@{ Filter = 'ab.*'; Expected = @('ab', 'ab.c') }
@{ Filter = 'a.'; Expected = @('a') }
@{ Filter = 'ab.'; Expected = @('ab') }
@{ Filter = 'a*.'; Expected = @('a', 'ab', 'abc', 'abcdefgh') }
@{ Filter = 'ab*.'; Expected = @('ab', 'abc', 'abcdefgh') }
@{ Filter = 'c*.'; Expected = @('ccc') }
@{ Filter = '*.b'; Expected = @('a.b') }
@{ Filter = '*.c?'; Expected = @('ab.c', 'bdir.cd', 'c.c', 'cab.cd') }
@{ Filter = '*.b?'; Expected = @('a.b', 'a1.b2', 'a2.b1') }
@{ Filter = '*.?'; Expected = @('1cb', 'a', 'a.b', 'ab', 'ab.c', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'c.c', 'ccc', 'fold', 'x.y') }
@{ Filter = '*.??'; Expected = @('1cb', 'a', 'a.b', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '?'; Expected = @('a') }
@{ Filter = '??'; Expected = @('a', 'ab') }
@{ Filter = 'a?'; Expected = @('a', 'ab') }
@{ Filter = 'b?'; Expected = @() }
@{ Filter = 'ab?'; Expected = @('ab', 'abc') }
@{ Filter = 'a?.'; Expected = @('a', 'ab') }
@{ Filter = 'bc?'; Expected = @('bca') }
@{ Filter = '?.?'; Expected = @('a', 'a.b', 'c.c', 'x.y') }
@{ Filter = 'c.?'; Expected = @('c.c') }
@{ Filter = '*a*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'cab.cd') }
@{ Filter = '*c*'; Expected = @('1cb', 'a.bcd', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir.cd', 'c.c', 'cab.cd', 'ccc') }
@{ Filter = 'a*.*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk') }
@{ Filter = 'bca*.'; Expected = @('bca', 'bcabcabc') }
@{ Filter = '*.*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '.*'; Expected = @() }
@{ Filter = '*..*'; Expected = @() }
@{ Filter = 'abc'; Expected = @('abc') }
@{ Filter = 'cba'; Expected = @() }
@{ Filter = 'a.b.'; Expected = @('a.b') }
@{ Filter = 'ab.c.'; Expected = @('ab.c') }
@{ Filter = '*b.'; Expected = @('1cb', 'a.b', 'ab') }
@{ Filter = 'cab*'; Expected = @('cab.cd') }
@{ Filter = 'a??'; Expected = @('a', 'ab', 'abc') }
@{ Filter = 'c.c'; Expected = @('c.c') }
@{ Filter = 'ca?'; Expected = @() }
) {
$folder = $filterTruthTableFolder
$result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',')
}
fix(items): let an asterisk before a dot in -Filter cross every dot but the last An independent review found that the matcher of Get-ChildItem2 -Filter (commit 69cd8ed) bounded a star in front of a dot token at the FIRST dot of the name instead of the LAST one (Win32 DOS_STAR). The differential oracle of 69cd8ed used only 8.3-compliant names, which have at most one dot, so the first dot and the last dot are always the same position there and the bug could not show up. With a name that has two or more dots, such as &#34;archive.tar.gz&#34; or &#34;jquery.min.js&#34;, the star stopped too early and the rest of the pattern could not reach the final segment of the name, so a pattern such as &#34;*.?&#34;, &#34;*.??&#34;, or &#34;*.?.?&#34; wrongly returned nothing for these names, although Windows PowerShell 5.1 returns them. What a user sees changing: -Filter patterns of the shape &#34;a star followed by a dot&#34; (including &#34;*.?&#34;, &#34;*.??&#34;, &#34;*.???&#34;, a star in a multi-part pattern such as &#34;a*.?&#34; or &#34;x.*.?&#34;) now also match names with two or more dots, the same as Windows PowerShell 5.1, in both editions. A name with at most one dot is unaffected (that case was already correct). Fixed by bounding the star at `name.LastIndexOf(&#39;.&#39;)` instead of the first dot at or after the current position, and corrected the two comments that described the old, narrower rule. Also added a discriminating row to the existing short-name test (`-Filter &#39;LONGFI~1*&#39;` against a plain file named `LongFileName`, expected to return nothing): the three rows already in that test happen to give the same result whether the matcher reads the real name or the short name, so they could not have caught a regression to short-name matching; this row can. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): added a 9-row test with 10 names that have two or three dots (including the realistic ones from the review: archive.tar.gz, jquery.min.js, my.file.name.txt, v1.2.3, index.d.ts), expected values pinned from Windows PowerShell 5.1 with `fsutil file setshortname` (and, for one row, `fsutil 8dot3name strip`) so that a short name the volume assigns cannot also produce a match; and the LONGFI~1* row above. Both fail on the build of 69cd8ed and pass after this fix. The fix was checked with the reviewer&#39;s differential harness (C:\Temp\ntfs-rc9\review-probes-r3\diff5.ps1) against this build: 0 mismatches over two independent fixtures (732,630 real-name/pattern pairs with names of two or more dots, short names overridden with `fsutil file setshortname` so that a hit can only come from the real name), in addition to the 109,308 pairs of 8.3-compliant names checked for 69cd8ed. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
# 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 realistic ones such as archive.tar.gz and jquery.min.js. 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.
fix(items): let an asterisk before a dot in -Filter cross every dot but the last An independent review found that the matcher of Get-ChildItem2 -Filter (commit 69cd8ed) bounded a star in front of a dot token at the FIRST dot of the name instead of the LAST one (Win32 DOS_STAR). The differential oracle of 69cd8ed used only 8.3-compliant names, which have at most one dot, so the first dot and the last dot are always the same position there and the bug could not show up. With a name that has two or more dots, such as &#34;archive.tar.gz&#34; or &#34;jquery.min.js&#34;, the star stopped too early and the rest of the pattern could not reach the final segment of the name, so a pattern such as &#34;*.?&#34;, &#34;*.??&#34;, or &#34;*.?.?&#34; wrongly returned nothing for these names, although Windows PowerShell 5.1 returns them. What a user sees changing: -Filter patterns of the shape &#34;a star followed by a dot&#34; (including &#34;*.?&#34;, &#34;*.??&#34;, &#34;*.???&#34;, a star in a multi-part pattern such as &#34;a*.?&#34; or &#34;x.*.?&#34;) now also match names with two or more dots, the same as Windows PowerShell 5.1, in both editions. A name with at most one dot is unaffected (that case was already correct). Fixed by bounding the star at `name.LastIndexOf(&#39;.&#39;)` instead of the first dot at or after the current position, and corrected the two comments that described the old, narrower rule. Also added a discriminating row to the existing short-name test (`-Filter &#39;LONGFI~1*&#39;` against a plain file named `LongFileName`, expected to return nothing): the three rows already in that test happen to give the same result whether the matcher reads the real name or the short name, so they could not have caught a regression to short-name matching; this row can. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): added a 9-row test with 10 names that have two or three dots (including the realistic ones from the review: archive.tar.gz, jquery.min.js, my.file.name.txt, v1.2.3, index.d.ts), expected values pinned from Windows PowerShell 5.1 with `fsutil file setshortname` (and, for one row, `fsutil 8dot3name strip`) so that a short name the volume assigns cannot also produce a match; and the LONGFI~1* row above. Both fail on the build of 69cd8ed and pass after this fix. The fix was checked with the reviewer&#39;s differential harness (C:\Temp\ntfs-rc9\review-probes-r3\diff5.ps1) against this build: 0 mismatches over two independent fixtures (732,630 real-name/pattern pairs with names of two or more dots, short names overridden with `fsutil file setshortname` so that a hit can only come from the real name), in addition to the 109,308 pairs of 8.3-compliant names checked for 69cd8ed. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
It 'Should match -Filter "<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 ',')
}
fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 &#34;DOS&#34; wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with &#34;*&#34;, and the new matcher decides alone. What a user sees changing: - &#34;Report.*&#34; also returns &#34;Report&#34;, which has no dot (previously only names with a dot matched a pattern ending in &#34;.*&#34;). - A pattern that ends in a dot, such as &#34;Report.&#34; or &#34;Rep*.&#34;, now returns the names without a dot, instead of nothing. - &#34;*.*.*&#34; now returns every item, the same as &#34;*.*&#34; and a plain &#34;*&#34;, instead of only names with two literal dots. - Breaking: an empty -Filter (&#39;&#39;) now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter &#39;*&#39; explicitly to keep that behavior from before. - A single dot (&#39;.&#39;) as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7&#39;s own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for &#34;*.?&#34; (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in &#34;..&#34; and are not part of either product&#39;s -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): - Changed the dot-rules table (pins the new values for &#34;Report.*&#34;, &#34;Report.&#34;, &#34;Rep*.&#34;, &#34;*.&#34;, and an empty -Filter) and the dedicated &#34;*.*.*&#34; test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and &#34;X.*&#34; rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
It 'Should apply a -Filter at every level of a recursive listing, directories included' {
$recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory
$paths = @(
'A.txt', 'B.log',
'Sub\C.txt', 'Sub\D.log',
'Sub\Sub2\E.txt', 'Sub\Sub2\F.log'
) | ForEach-Object -Process { Join-Path -Path $recurseFolder -ChildPath $_ }
Assert-TestSandboxPath -Sandbox $sandbox -Path $paths
New-Item -ItemType Directory -Path (Join-Path -Path $recurseFolder -ChildPath 'Sub\Sub2') -Force | Out-Null
foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Content' }
$result = @(Get-ChildItem2 -Path $recurseFolder -Recurse -Filter '*.txt' -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'A.txt,C.txt,E.txt'
}
It 'Should apply a dot -Filter together with -File and with -Directory' {
$folder = $filterTruthTableFolder
$files = @(Get-ChildItem2 -Path $folder -File -Filter '*.' -Force -ErrorAction Stop)
$directories = @(Get-ChildItem2 -Path $folder -Directory -Filter '*.' -Force -ErrorAction Stop)
($files.Name | Sort-Object) -join ',' | Should -BeExactly '1cb,a,ab,abc,abcdefgh,bca,bcabcabc,ccc'
($directories.Name | Sort-Object) -join ',' | Should -BeExactly 'bdir,fold'
}
It 'Should return every item for an empty -Filter, like no filter at all' {
$folder = $filterTruthTableFolder
$result = @(Get-ChildItem2 -Path $folder -Filter '' -Force -ErrorAction Stop)
$result | Should -HaveCount 22
}
It 'Should apply -Filter to a -Path that is a file' {
$file = Join-Path -Path $tree -ChildPath 'Root.txt'
$matching = @(Get-ChildItem2 -Path $file -Filter '*.txt' -ErrorAction Stop)
$other = @(Get-ChildItem2 -Path $file -Filter '*.log' -ErrorAction Stop)
$matching | Should -HaveCount 1
$matching[0].FullName | Should -Be $file
$other | Should -BeNullOrEmpty
}
It 'Should reject a null -Filter' {
{ Get-ChildItem2 -Path $tree -Filter $null -ErrorAction Stop } |
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*"
}
fix(items): reject a -Filter of more than 32,767 characters An independent review found that a very long -Filter pattern (for example, a run of 120,000 question marks, or 45,000 asterisks) crashes the whole host process with an uncatchable StackOverflowException: the matcher recurses roughly once per character of the pattern, and a long enough pattern exhausts the stack. A pattern this long serves no purpose that a shorter one could not also serve, and the review confirmed that patterns of 32,767 characters or fewer, of any shape (literal, &#34;?&#34; repeated, &#34;*&#34; repeated, &#34;*?&#34; repeated, &#34;*.&#34; repeated), do not come close to exhausting the stack. Added `[ValidateLength(0, 32767)]` next to the existing `[ValidateNotNull]` on -Filter, so that a pattern longer than 32,767 characters is rejected at parameter binding, before it ever reaches the matcher, with the same `ParameterArgumentValidationError` and `&#39;Filter&#39;` in the message as the existing null rejection; shorter values, including an empty one, are unaffected. Tests (Tests\ItemCmdlets.Tests.ps1, &#34;Get-ChildItem2&#34; &gt; &#34;Recursion, type filters, and depth&#34;): a -Filter of 32,768 characters now gives ParameterArgumentValidationError, and one of exactly 32,767 characters (&#34;?&#34; repeated, in a folder with one file) returns the right result without crashing. The crash itself was confirmed in a disposable child process (not through the shared Pester harness, to avoid taking that process down too) with the reviewer&#39;s probe (C:\Temp\ntfs-rc9\review-probes-r3\p9-longpattern.ps1 -N 120000 -Kind qm): the process terminated with a StackOverflowException before this fix, and after it, the same probe at the exact boundaries (32,767 and 32,768 characters, every pattern kind the probe offers) neither crashes nor is accepted past the limit. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
1 week ago
It 'Should reject a -Filter of more than 32,767 characters' {
$tooLong = '?' * 32768
{ Get-ChildItem2 -Path $tree -Filter $tooLong -ErrorAction Stop } |
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*"
}
# A pattern this long is not useful, but the cmdlet must not crash the host: before this test was added, a
# pattern of this length (or a long run of asterisks) recursed deep enough in the matcher to raise an
# uncatchable StackOverflowException and terminate the process.
It 'Should accept a -Filter of exactly 32,767 characters and return the right result' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMaxLength' -Directory
$file = Join-Path -Path $folder -ChildPath 'a'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'a'
$longest = '?' * 32767
$result = @(Get-ChildItem2 -Path $folder -Filter $longest -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].Name | Should -Be 'a'
}
It 'Should stop a recursive pipeline without recording an enumeration error' {
$result = @(Get-ChildItem2 -Path $tree -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 1)
$result | Should -HaveCount 1
$childErrors | Should -BeNullOrEmpty
}
# The second file comes from a sub folder, so the pipeline stops while the cmdlet is inside the recursion.
It 'Should stop a recursive pipeline inside a sub folder without recording an enumeration error' {
$result = @(Get-ChildItem2 -Path $tree -Recurse -File -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 2)
$result | Should -HaveCount 2
$childErrors | Should -BeNullOrEmpty
}
# A break or continue in a later pipeline stage passes through the cmdlet as an exception, which it must not
# report as a failed folder.
It 'Should end a recursive enumeration for <Keyword> in a later pipeline stage without recording an enumeration error' -ForEach @(
@{ Keyword = 'break' }
@{ Keyword = 'continue' }
) {
$names = [System.Collections.Generic.List[string]]::new()
foreach ($round in 1) {
Get-ChildItem2 -Path $tree -Recurse -File -ErrorVariable childErrors -ErrorAction SilentlyContinue | ForEach-Object -Process {
$names.Add($_.Name)
if ($Keyword -eq 'break') { break } else { continue }
}
}
$names | Should -HaveCount 1
$childErrors | Should -BeNullOrEmpty
}
}
Context 'Unreadable directories' {
It 'Should report the denied folder and continue with the next path' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'CannotList' -Directory
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'CanList' -Directory
$file = Join-Path -Path $next -ChildPath 'Next.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Next'
Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ 'S-1-1-0' = 'ReadData' }
$result = @(Get-ChildItem2 -Path $blocked, $next -ErrorVariable childErrors -ErrorAction SilentlyContinue)
$childErrors | Should -HaveCount 1
$childErrors[0].FullyQualifiedErrorId | Should -BeLike 'DirUnauthorizedAccessError,*'
$childErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied'
$childErrors[0].TargetObject | Should -Be $blocked
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $file
}
}
Context 'Link traversal' {
It 'Should return a junction itself but skip its contents with -SkipMountPoints' {
$root = New-TestSandboxItem -Sandbox $sandbox -Name 'JunctionListing' -Directory
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'JunctionTarget' -Directory
$file = Join-Path -Path $target -ChildPath 'Target.txt'
$link = Join-Path -Path $root -ChildPath 'Link'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file, $link
Set-Content -LiteralPath $file -Value 'Target'
New-Item -ItemType Junction -Path $link -Value $target | Out-Null
$result = @(Get-ChildItem2 -Path $root -Recurse -SkipMountPoints -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $link
Get-Content -LiteralPath $file | Should -Be 'Target'
}
It 'Should return a symbolic link itself but skip its contents with -SkipSymbolicLinks' -Skip:(-not $canCreateSymbolicLinks) {
$root = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicListing' -Directory
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicTarget' -Directory
$file = Join-Path -Path $target -ChildPath 'Target.txt'
$link = Join-Path -Path $root -ChildPath 'Link'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file, $link
Set-Content -LiteralPath $file -Value 'Target'
New-NTFSSymbolicLink -Path $link -Target $target -ErrorAction Stop
$result = @(Get-ChildItem2 -Path $root -Recurse -SkipSymbolicLinks -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $link
Get-Content -LiteralPath $file | Should -Be 'Target'
}
# A junction whose target is gone passes the existence check, but the folder behind it can't be opened. The
# error belongs to that folder, and the enumeration goes on with the next one.
It 'Should report a junction whose target was removed as a DirUnspecifiedError and continue with the next folder' {
$root = New-TestSandboxItem -Sandbox $sandbox -Name 'BrokenJunction' -Directory
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovedTarget' -Directory
$link = Join-Path -Path $root -ChildPath 'Broken'
$sibling = Join-Path -Path $root -ChildPath 'Sibling'
$file = Join-Path -Path $sibling -ChildPath 'Sibling.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link, $sibling, $file
New-Item -ItemType Directory -Path $sibling | Out-Null
Set-Content -LiteralPath $file -Value 'Sibling'
New-Item -ItemType Junction -Path $link -Value $target | Out-Null
Remove-Item -LiteralPath $target -Force
$result = @(Get-ChildItem2 -Path $root -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue)
$childErrors | Should -HaveCount 1
$childErrors[0].FullyQualifiedErrorId | Should -BeLike 'DirUnspecifiedError,*'
$childErrors[0].CategoryInfo.Category | Should -Be 'NotSpecified'
$childErrors[0].TargetObject | Should -Be $link
$childErrors[0].Exception | Should -BeOfType [System.IO.DirectoryNotFoundException]
@($result.FullName | Sort-Object) | Should -Be @(@($link, $sibling, $file) | Sort-Object)
Get-Content -LiteralPath $file | Should -Be 'Sibling'
}
}
Context 'Optional object properties' {
BeforeEach {
$settings = (Get-Module -Name NTFSSecurity).PrivateData
$savedMode = $settings['GetFileSystemModeProperty']
$savedHardLinks = $settings['IdentifyHardLinks']
}
AfterEach {
$settings['GetFileSystemModeProperty'] = $savedMode
$settings['IdentifyHardLinks'] = $savedHardLinks
}
It 'Should honor Mode and HardLinkCount enabled=<_>' -ForEach @($true, $false) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ObjectProperties' -Directory
$file = Join-Path -Path $folder -ChildPath 'File.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Properties'
[IO.File]::SetAttributes($file, [IO.FileAttributes]::Archive)
$settings['GetFileSystemModeProperty'] = $_
$settings['IdentifyHardLinks'] = $_
$item = Get-ChildItem2 -Path $file -ErrorAction Stop
if ($_) {
$item.Mode | Should -BeExactly '-a---'
$item.HardLinkCount | Should -Be 1
}
else {
$item.PSObject.Properties['Mode'] | Should -BeNullOrEmpty
$item.PSObject.Properties['HardLinkCount'] | Should -BeNullOrEmpty
}
}
It 'Should render read-only, hidden, and system bits in the Mode property' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ModeBits'
[IO.File]::SetAttributes($file, [IO.FileAttributes] 'ReadOnly, Hidden, System')
$settings['GetFileSystemModeProperty'] = $true
$item = Get-ChildItem2 -Path $file -Force -ErrorAction Stop
$item.Mode | Should -BeExactly '--rhs'
}
It 'Should render a folder with a d in the Mode property' {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'ModeFolder' -Directory
$folder = Join-Path -Path $parent -ChildPath 'Inner'
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder
New-Item -ItemType Directory -Path $folder | Out-Null
$settings['GetFileSystemModeProperty'] = $true
$item = Get-ChildItem2 -Path $parent -ErrorAction Stop
$item | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$item.Mode | Should -BeExactly 'd----'
}
It 'Should return an empty Mode for no object' {
[NTFSSecurity.FileSystemCodeMembers]::Mode($null) | Should -BeExactly ''
}
# Windows can't list the hard links of a file on a network share, (50) "The request is not supported". The cmdlet
# still returns the file, without HardLinkCount, and says why in a debug message. The test sets the preference,
# because -Debug would prompt in Windows PowerShell.
It 'Should return a file on a network share without HardLinkCount and say why in a debug message' -Skip:(-not $canUseAdminShare) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareProperties' -Directory
$file = Join-Path -Path $folder -ChildPath 'Share.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Share'
$sharePath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file
$settings['IdentifyHardLinks'] = $true
$DebugPreference = 'Continue'
$output = @(Get-ChildItem2 -Path $sharePath -ErrorVariable childErrors -ErrorAction SilentlyContinue 5>&1)
$childErrors | Should -BeNullOrEmpty
$items = @($output | Where-Object -FilterScript { $_ -isnot [Management.Automation.DebugRecord] })
$items | Should -HaveCount 1
$items[0].Name | Should -BeExactly 'Share.txt'
$items[0].PSObject.Properties['HardLinkCount'] | Should -BeNullOrEmpty
$messages = @($output | Where-Object -FilterScript { $_ -is [Management.Automation.DebugRecord] } | ForEach-Object -Process { $_.Message })
$messages | Should -Contain "Could not read hard links for '$sharePath'"
}
}
Context 'Default table view' {
BeforeAll {
$viewFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'View' -Directory
$blocked = Join-Path -Path $viewFolder -ChildPath 'Blocked.txt'
$inheriting = Join-Path -Path $viewFolder -ChildPath 'Inheriting.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $blocked, $inheriting
Set-Content -LiteralPath $blocked -Value 'Blocked'
Set-Content -LiteralPath $inheriting -Value 'Inheriting'
$acl = Get-Acl -LiteralPath $blocked
$acl.SetAccessRuleProtection($true, $true)
Set-Acl -LiteralPath $blocked -AclObject $acl
$lines = Get-ChildItem2 -Path $viewFolder | Out-String -Stream -Width 200
}
It 'Should show False in the Inherits column for a file whose inheritance is disabled' {
($lines | Where-Object -FilterScript { $_ -match 'Blocked\.txt\s*$' }) | Should -Match '\bFalse\b'
}
It 'Should show True in the Inherits column for a file that inherits' {
($lines | Where-Object -FilterScript { $_ -match 'Inheriting\.txt\s*$' }) | Should -Match '\bTrue\b'
}
}
Context 'With -Attributes' {
BeforeAll {
$attributeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'Attributes' -Directory
$hiddenFile = Join-Path -Path $attributeFolder -ChildPath 'Hidden.txt'
$readOnlyFile = Join-Path -Path $attributeFolder -ChildPath 'ReadOnly.txt'
$plainFile = Join-Path -Path $attributeFolder -ChildPath 'Plain.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $hiddenFile, $readOnlyFile, $plainFile
Set-Content -LiteralPath $hiddenFile, $readOnlyFile, $plainFile -Value 'Attributes'
(Get-Item -LiteralPath $hiddenFile -Force).Attributes = [IO.FileAttributes]::Hidden
(Get-Item -LiteralPath $readOnlyFile).Attributes = [IO.FileAttributes]::ReadOnly
}
# Before 5.0.0, the cmdlet returned only the items that had all the listed attributes (#5).
It 'Should return the items that have any of the listed attributes, like Get-ChildItem' {
$result = @(Get-ChildItem2 -Path $attributeFolder -Attributes Hidden, ReadOnly)
@($result.Name | Sort-Object) | Should -Be @('Hidden.txt', 'ReadOnly.txt')
}
# Before 5.0.0, an empty value applied no filter and returned hidden items as well.
It 'Should reject an empty value' {
{ Get-ChildItem2 -Path $attributeFolder -Attributes 0 -ErrorAction Stop } | Should -Throw -ErrorId 'AttributesEmpty,NTFSSecurity.GetChildItem2'
}
It 'Should return only the items with the attribute when one is listed' {
$result = @(Get-ChildItem2 -Path $attributeFolder -Attributes ReadOnly)
$result.Name | Should -Be 'ReadOnly.txt'
}
}
}
Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' {
BeforeEach {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Several' -Directory
$missing = Join-Path -Path $folder -ChildPath 'Missing.txt'
$first = Join-Path -Path $folder -ChildPath 'First.txt'
$second = Join-Path -Path $folder -ChildPath 'Second.txt'
$destination = Join-Path -Path $folder -ChildPath 'Destination'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $first, $second, $destination
Set-Content -LiteralPath $first -Value 'First'
Set-Content -LiteralPath $second -Value 'Second'
New-Item -ItemType Directory -Path $destination | Out-Null
}
# Before 5.0.0, the cmdlets stopped processing -Path at the first failing path.
It 'Remove-Item2 should continue after a path that does not exist' {
Remove-Item2 -Path $missing, $first -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
$first | Should -Not -Exist
}
It '<_> should continue after a path that does not exist' -ForEach @('Copy-Item2', 'Move-Item2') {
& $_ -Path $missing, $first -Destination $destination -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
Join-Path -Path $destination -ChildPath 'First.txt' | Should -Exist
}
It '<_> should continue after a file that exists at the destination' -ForEach @('Copy-Item2', 'Move-Item2') {
Set-Content -LiteralPath (Join-Path -Path $destination -ChildPath 'First.txt') -Value 'Existing'
& $_ -Path $first, $second -Destination $destination -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
Join-Path -Path $destination -ChildPath 'Second.txt' | Should -Exist
}
# Before 5.0.0, the verbose message named the source path as the destination.
It '<Command> should name the destination in the verbose message' -ForEach @(
@{ Command = 'Copy-Item2'; Verb = 'copied' }
@{ Command = 'Move-Item2'; Verb = 'moved' }
) {
$target = Join-Path -Path $destination -ChildPath 'First.txt'
$messages = & $Command -Path $first -Destination $destination -Verbose 4>&1
$messages.Message | Should -Contain ("File '{0}' {1} to '{2}'" -f $first, $Verb, $target)
}
It '<Command> should name the destination of a folder in the verbose message' -ForEach @(
@{ Command = 'Copy-Item2'; Verb = 'copied' }
@{ Command = 'Move-Item2'; Verb = 'moved' }
) {
$sourceFolder = Join-Path -Path $folder -ChildPath 'VerboseFolder'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder
New-Item -ItemType Directory -Path $sourceFolder | Out-Null
$target = Join-Path -Path $destination -ChildPath 'VerboseFolder'
$messages = & $Command -Path $sourceFolder -Destination $destination -Verbose 4>&1
$messages.Message | Should -Contain ("Directory '{0}' {1} to '{2}'" -f $sourceFolder, $Verb, $target)
}
# With -PassThru, both cmdlets return the item at the destination, as their pages say.
It 'Copy-Item2 -PassThru should return the copy' {
$result = Copy-Item2 -Path $first -Destination $destination -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'First.txt')
$first | Should -Exist
}
It 'Copy-Item2 -PassThru should return the copy of a folder' {
$sourceFolder = Join-Path -Path $folder -ChildPath 'SourceFolder'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder
New-Item -ItemType Directory -Path $sourceFolder | Out-Null
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'Inner.txt') -Value 'Inner'
$result = Copy-Item2 -Path $sourceFolder -Destination (Join-Path -Path $destination -ChildPath 'Copied') -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'Copied')
$sourceFolder | Should -Exist
}
It 'Move-Item2 -PassThru should return the item at its new location' {
$result = Move-Item2 -Path $first -Destination $destination -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'First.txt')
}
It 'Move-Item2 -PassThru should return a folder at its new location' {
$sourceFolder = Join-Path -Path $folder -ChildPath 'MovedFolder'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder
New-Item -ItemType Directory -Path $sourceFolder | Out-Null
$result = Move-Item2 -Path $sourceFolder -Destination $destination -PassThru $true
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'MovedFolder')
$sourceFolder | Should -Not -Exist
}
# Before 5.0.0-rc6, the check for an existing destination looked for a file only. For a folder whose name existed
# in the destination, the cmdlets failed in the middle with a CopyError or a MoveError, and Copy-Item2 could copy
# a part of the folder.
It '<_> should write DestinationFileAlreadyExists for a folder that exists at the destination and change nothing' -ForEach @('Copy-Item2', 'Move-Item2') {
$sourceFolder = Join-Path -Path $folder -ChildPath 'Conflict'
$existingFolder = Join-Path -Path $destination -ChildPath 'Conflict'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder, $existingFolder
New-Item -ItemType Directory -Path $sourceFolder, $existingFolder | Out-Null
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'A.txt') -Value 'New'
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'B.txt') -Value 'New'
Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') -Value 'Existing'
& $_ -Path $sourceFolder -Destination $destination -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike 'DestinationFileAlreadyExists,*'
$itemErrors[0].TargetObject | Should -Be $existingFolder
Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') | Should -Be 'Existing'
Join-Path -Path $existingFolder -ChildPath 'B.txt' | Should -Not -Exist
Join-Path -Path $sourceFolder -ChildPath 'A.txt' | Should -Exist
}
It 'Move-Item2 -Force should replace an existing file with PassThru=<_>' -ForEach @($false, $true) {
$target = Join-Path -Path $destination -ChildPath 'First.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $target
Set-Content -LiteralPath $target -Value 'Previous'
$expected = Get-Content -LiteralPath $first -Raw
$result = @(Move-Item2 -Path $first -Destination $destination -Force -PassThru $_ -ErrorAction Stop)
$first | Should -Not -Exist
Get-Content -LiteralPath $target -Raw | Should -BeExactly $expected
if ($_) {
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $target
}
else {
$result | Should -BeNullOrEmpty
}
}
It 'Copy-Item2 -Force should copy a folder into an existing folder of the same name and replace the files in both' {
$sourceFolder = Join-Path -Path $folder -ChildPath 'Merge'
$existingFolder = Join-Path -Path $destination -ChildPath 'Merge'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder, $existingFolder
New-Item -ItemType Directory -Path $sourceFolder, $existingFolder | Out-Null
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'A.txt') -Value 'New'
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'B.txt') -Value 'New'
Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') -Value 'Existing'
Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'C.txt') -Value 'Existing'
Copy-Item2 -Path $sourceFolder -Destination $destination -Force -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -BeNullOrEmpty
Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') | Should -Be 'New'
Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'B.txt') | Should -Be 'New'
Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'C.txt') | Should -Be 'Existing'
}
# Before 5.0.0-rc6, the error named the source item as the path that wasn't found, also when the folder of the
# destination was missing (#21).
It '<Command> should name the missing folder of the destination for a <Kind> and change nothing' -ForEach @(
@{ Command = 'Copy-Item2'; Kind = 'file'; ErrorId = 'CopyError' }
@{ Command = 'Copy-Item2'; Kind = 'folder'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; Kind = 'file'; ErrorId = 'MoveError' }
@{ Command = 'Move-Item2'; Kind = 'folder'; ErrorId = 'MoveError' }
) {
$source = $first
if ($Kind -eq 'folder') {
$source = Join-Path -Path $folder -ChildPath 'SourceFolder'
Assert-TestSandboxPath -Sandbox $sandbox -Path $source
New-Item -ItemType Directory -Path $source | Out-Null
Set-Content -LiteralPath (Join-Path -Path $source -ChildPath 'Inner.txt') -Value 'Inner'
}
$missingFolder = Join-Path -Path $folder -ChildPath 'MissingFolder'
$target = Join-Path -Path $missingFolder -ChildPath 'Item'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missingFolder, $target
& $Command -Path $source -Destination $target -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[0].Exception.Message | Should -BeLike "*'$missingFolder'*"
$itemErrors[0].TargetObject | Should -Be $target
$source | Should -Exist
$missingFolder | Should -Not -Exist
}
# Before 5.0.0, -PassThru wrote the item also when -WhatIf skipped the operation.
It '<_> should write nothing with -PassThru and -WhatIf' -ForEach @('Copy-Item2', 'Move-Item2', 'Remove-Item2') {
$parameters = @{ Path = $first; PassThru = $true; WhatIf = $true }
if ($_ -ne 'Remove-Item2') {
$parameters.Destination = $destination
}
$result = @(& $_ @parameters)
$result | Should -BeNullOrEmpty
$first | Should -Exist
}
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items - #41: for the root of a drive, the cmdlets read and changed the security descriptor of the drive, a device object. FileSystemSecurity2 now routes drive roots through the path-based AlphaFS methods, which keep the trailing backslash; the removal and inheritance helpers use it too. - #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor without the audit entries like Get-NTFSAudit, through one helper, and Get-NTFSEffectiveAccess names the cause that Windows reported instead of a missing Security privilege. - #108: Copy-Item2 and Move-Item2 check the destination only for an operation that runs; with -WhatIf, a verbose message names the conflict. - #111: Disable-Privileges skips the privileges that the token doesn&#39;t hold, the privilege messages are spelled right, and Get-FileHash2 declares the type name of its objects; 05-Releasing.md documents the release metadata tests. - rc3 review leftovers: Remove-NTFSAudit writes nothing for an item without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores the previous owner in a finally block and keeps an owner that the descriptor sets, and FileSystemSecurity2.Write with another item writes only the sections that were read. Each fix has a test that failed first, in Windows PowerShell 5.1 and PowerShell 7; writing a drive root was checked once on a temporary VHD. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
4 weeks ago
# Before 5.0.0-rc4, an existing destination file produced a real error also with -WhatIf, which only previews the
# operation, so -WhatIf -ErrorAction Stop stopped the preview (#108).
It '<_> should write no error with -WhatIf when the destination file exists' -ForEach @('Copy-Item2', 'Move-Item2') {
$existing = Join-Path -Path $destination -ChildPath 'First.txt'
Set-Content -LiteralPath $existing -Value 'Existing'
& $_ -Path $first -Destination $destination -WhatIf -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -BeNullOrEmpty
$first | Should -Exist
Get-Content -LiteralPath $existing | Should -Be 'Existing'
}
It '<_> should name the existing destination file in a verbose message with -WhatIf' -ForEach @('Copy-Item2', 'Move-Item2') {
$existing = Join-Path -Path $destination -ChildPath 'First.txt'
Set-Content -LiteralPath $existing -Value 'Existing'
$messages = & $_ -Path $first -Destination $destination -WhatIf -Verbose -ErrorAction SilentlyContinue 4>&1
@($messages | Where-Object -FilterScript { "$_" -like "*'$existing' already exists*" }) | Should -HaveCount 1
}
# Before 5.0.0-rc6, -WhatIf didn't tell that the operation would fail because the folder of the destination is
# missing.
It '<_> should name the missing folder of the destination in a verbose message with -WhatIf, and write no error' -ForEach @('Copy-Item2', 'Move-Item2') {
$missingFolder = Join-Path -Path $folder -ChildPath 'MissingFolder'
$target = Join-Path -Path $missingFolder -ChildPath 'Item'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missingFolder, $target
$messages = & $_ -Path $first -Destination $target -WhatIf -Verbose -ErrorVariable itemErrors -ErrorAction SilentlyContinue 4>&1
$itemErrors | Should -BeNullOrEmpty
@($messages | Where-Object -FilterScript { "$_" -like "*'$missingFolder' does not exist*" }) | Should -HaveCount 1
$first | Should -Exist
$missingFolder | Should -Not -Exist
}
# The folder of a destination on a share that doesn't exist is the share itself, which the error names.
It '<Command> should name the missing share of a UNC destination' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$missingShare = '\\localhost\NTFSSecurityMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)
$target = Join-Path -Path $missingShare -ChildPath 'Item.txt'
& $Command -Path $first -Destination $target -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[0].Exception.Message | Should -BeLike "*'$missingShare'*"
$first | Should -Exist
}
# A sharing violation is an IOException, which both cmdlets write as InvalidData; the error belongs to its source
# only, and no object comes out for it with -PassThru.
It '<Command> should write a <ErrorId> for a source that another process has locked and continue with the next path' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$stream = [IO.File]::Open($first, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
try {
$result = @(& $Command -Path $first, $second -Destination $destination -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue)
}
finally {
$stream.Dispose()
}
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
$itemErrors[0].TargetObject | Should -Be $first
$itemErrors[0].Exception | Should -BeOfType [System.IO.IOException]
$result | Should -HaveCount 1
$result[0].FullName | Should -Be (Join-Path -Path $destination -ChildPath 'Second.txt')
Join-Path -Path $destination -ChildPath 'First.txt' | Should -Not -Exist
Get-Content -LiteralPath $first | Should -Be 'First'
Get-Content -LiteralPath (Join-Path -Path $destination -ChildPath 'Second.txt') | Should -Be 'Second'
}
# Any other failure of Windows is not an IOException, and both cmdlets write it as NotSpecified. A deny entry for
# Everyone also applies to an administrator, who doesn't bypass the DACL without a backup privilege.
It '<Command> should write a <ErrorId> for each source when the destination folder denies new files' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$denied = Join-Path -Path $folder -ChildPath 'Denied'
Assert-TestSandboxPath -Sandbox $sandbox -Path $denied
New-Item -ItemType Directory -Path $denied | Out-Null
Add-TestDenyRule -Sandbox $sandbox -Path $denied -Rights @{ 'S-1-1-0' = 'CreateFiles' }
$result = @(& $Command -Path $first, $second -Destination $denied -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue)
$itemErrors | Should -HaveCount 2
for ($index = 0; $index -lt 2; $index++) {
$itemErrors[$index].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[$index].CategoryInfo.Category | Should -Be 'NotSpecified'
$itemErrors[$index].TargetObject | Should -Be @($first, $second)[$index]
$itemErrors[$index].Exception | Should -BeOfType [System.UnauthorizedAccessException]
}
$result | Should -BeNullOrEmpty
@(Get-ChildItem -LiteralPath $denied -Force) | Should -BeNullOrEmpty
Get-Content -LiteralPath $first | Should -Be 'First'
Get-Content -LiteralPath $second | Should -Be 'Second'
}
# A destination on a drive letter without a volume has no folder that the cmdlet could name, so Windows reports the
# drive as not ready, which AlphaFS raises as an IOException.
It '<Command> should write a <ErrorId> for a destination on a drive that does not exist and keep the source' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$used = @((Get-PSDrive -PSProvider FileSystem).Name) + @([System.IO.DriveInfo]::GetDrives() | ForEach-Object -Process { $_.Name.Substring(0, 1) })
fix: pass on what a later command raises, and return every item for -Filter *.* A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message. Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns. The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*. Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
3 weeks ago
# The lowest free letter: New-TestDriveMapping takes letters from Z downward, also in a run in parallel.
$letter = [char[]](68..90) | Where-Object -FilterScript { [string] $_ -notin $used } | Select-Object -First 1
if (-not $letter) {
Set-ItResult -Skipped -Because 'every drive letter is in use'
return
}
$result = @(& $Command -Path $first -Destination "${letter}:\" -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue)
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
$itemErrors[0].TargetObject | Should -Be $first
$itemErrors[0].Exception | Should -BeOfType [System.IO.IOException]
$result | Should -BeNullOrEmpty
Get-Content -LiteralPath $first | Should -Be 'First'
}
}
Describe 'Move-Item2' {
# Windows can't move a folder to another volume. Before 5.0.0-rc7, the cmdlet let AlphaFS emulate the move by
# copying and deleting, which failed for a folder with files with an error that named a file of the source, and
# which deleted an empty folder without creating it at the destination.
It 'Should refuse to move <Kind> folder to another volume and leave it in place' -Skip:(-not $canUseAdminShare) -ForEach @(
@{ Kind = 'an empty' }
@{ Kind = 'a non-empty' }
) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolume' -Directory
if ($Kind -eq 'a non-empty') {
Set-Content -LiteralPath (Join-Path -Path $source -ChildPath 'File.txt') -Value 'File'
}
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -ErrorVariable moveErrors -ErrorAction SilentlyContinue
$moveErrors | Should -HaveCount 1
$moveErrors[0].FullyQualifiedErrorId | Should -BeLike 'MoveError,*'
$moveErrors[0].CategoryInfo.Category | Should -Be 'InvalidOperation'
$moveErrors[0].Exception.Message | Should -BeLike "*'$source'*another volume*"
$moveErrors[0].TargetObject | Should -Be $source
$source | Should -Exist
$destination | Should -Not -Exist
}
It 'Should still move a file to another volume' -Skip:(-not $canUseAdminShare) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolumeFile'
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -ErrorAction Stop
$source | Should -Not -Exist
Get-Content -LiteralPath $destination | Should -Be 'CrossVolumeFile'
}
# With -Force, a file moves without CopyAllowed, which Windows refuses for another volume, as the cmdlet page says.
# The cmdlet writes that error of Windows, (17) "The system cannot move the file to a different disk drive", and
# not the error for a folder.
It 'Should write the error of Windows for a file that it moves with -Force to another volume' -Skip:(-not $canUseAdminShare) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolumeForce'
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -Force -ErrorVariable moveErrors -ErrorAction SilentlyContinue
$moveErrors | Should -HaveCount 1
$moveErrors[0].FullyQualifiedErrorId | Should -BeLike 'MoveError,*'
$moveErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
'0x{0:X8}' -f $moveErrors[0].Exception.HResult | Should -Be '0x80070011'
$source | Should -Exist
$destination | Should -Not -Exist
}
}
Describe 'Copy-Item2' {
Context 'When -Path is a folder with files and subfolders' {
BeforeAll {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'Source' -Directory
$sourceFile = Join-Path -Path $source -ChildPath 'File.txt'
$sourceSubfolderFile = Join-Path -Path $source -ChildPath 'Subfolder\Other.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFile, $sourceSubfolderFile
New-Item -ItemType Directory -Path (Split-Path -Path $sourceSubfolderFile -Parent) | Out-Null
Set-Content -LiteralPath $sourceFile -Value 'File'
Set-Content -LiteralPath $sourceSubfolderFile -Value 'Other'
}
It 'Should copy the folder with its files and subfolders' {
$destination = Join-Path -Path $sandbox -ChildPath ('Copy-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Copy-Item2 -Path $source -Destination $destination -ErrorVariable copyErrors -ErrorAction SilentlyContinue
$copyErrors | Should -BeNullOrEmpty
Join-Path -Path $destination -ChildPath 'File.txt' | Should -Exist
Join-Path -Path $destination -ChildPath 'Subfolder\Other.txt' | Should -Exist
}
}
}
Describe 'Relative paths' {
BeforeAll {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'RelativeParent' -Directory
$child = Join-Path -Path $parent -ChildPath 'Child'
$sibling = Join-Path -Path $parent -ChildPath 'Sibling'
$siblingFile = Join-Path -Path $sibling -ChildPath 'Sibling.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $child, $sibling, $siblingFile
New-Item -ItemType Directory -Path $child, $sibling | Out-Null
Set-Content -LiteralPath $siblingFile -Value 'Sibling'
}
It 'Get-Item2 should resolve <Path> against the current location' -ForEach @(
@{ Path = '.'; Expected = 'Child' }
@{ Path = '.\'; Expected = 'Child' }
@{ Path = '..'; Expected = 'Parent' }
@{ Path = '..\Sibling'; Expected = 'Sibling' }
@{ Path = '..\Sibling\Sibling.txt'; Expected = 'SiblingFile' }
@{ Path = '..\..'; Expected = 'Grandparent' }
) {
$expectedPath = switch ($Expected) {
'Child' { $child }
'Parent' { $parent }
'Sibling' { $sibling }
'SiblingFile' { $siblingFile }
'Grandparent' { Split-Path -Path $parent -Parent }
}
Push-Location -LiteralPath $child
try {
$result = @(Get-Item2 -Path $Path -ErrorAction Stop)
}
finally {
Pop-Location
}
$result | Should -HaveCount 1
$result[0].FullName.TrimEnd('\') | Should -Be $expectedPath
}
}
Describe 'Test-Path2' {
BeforeAll {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'TestPath' -Directory
$file = Join-Path -Path $folder -ChildPath 'File.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'File'
$missing = Join-Path -Path $folder -ChildPath 'Missing.txt'
$paths = @{ 'file' = $file; 'folder' = $folder; 'missing item' = $missing }
}
It 'Should return <Expected> for a <Kind> with -PathType <PathType>' -ForEach @(
@{ Kind = 'file'; PathType = 'Any'; Expected = $true }
@{ Kind = 'folder'; PathType = 'Any'; Expected = $true }
@{ Kind = 'missing item'; PathType = 'Any'; Expected = $false }
@{ Kind = 'file'; PathType = 'Leaf'; Expected = $true }
@{ Kind = 'folder'; PathType = 'Leaf'; Expected = $false }
@{ Kind = 'missing item'; PathType = 'Leaf'; Expected = $false }
@{ Kind = 'file'; PathType = 'Container'; Expected = $false }
@{ Kind = 'folder'; PathType = 'Container'; Expected = $true }
@{ Kind = 'missing item'; PathType = 'Container'; Expected = $false }
) {
$result = @(Test-Path2 -Path $paths[$Kind] -PathType $PathType -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0] | Should -BeOfType [bool]
$result[0] | Should -Be $Expected
}
It 'Should write one value per path in the order of the paths' {
$result = @(Test-Path2 -Path $file, $missing, $folder -ErrorAction Stop)
$result -join ',' | Should -Be 'True,False,True'
}
It 'Should take the items from the pipeline' {
$result = @(Get-ChildItem -LiteralPath $folder | Test-Path2 -PathType Leaf -ErrorAction Stop)
$result -join ',' | Should -Be 'True'
}
It 'Should resolve a relative path against the current location' {
$relative = Join-Path -Path (Split-Path -Path $folder -Leaf) -ChildPath 'File.txt'
Test-Path2 -Path $relative -ErrorAction Stop | Should -BeTrue
}
It 'Should find a folder whose path is longer than 260 characters' {
$longRoot = Join-Path -Path $folder -ChildPath 'Long'
Assert-TestSandboxPath -Sandbox $sandbox -Path $longRoot
$long = Join-Path -Path $longRoot -ChildPath (('A' * 100), ('B' * 100), ('C' * 100) -join '\')
Add-Type -Path (Join-Path -Path (Get-Module -Name NTFSSecurity).ModuleBase -ChildPath 'AlphaFS.dll')
[Alphaleonis.Win32.Filesystem.Directory]::CreateDirectory($long) | Out-Null
$long.Length | Should -BeGreaterThan 260
Test-Path2 -Path $long -PathType Container -ErrorAction Stop | Should -BeTrue
}
# Before 5.0.0-rc6, a path with a character that Windows doesn't allow in file names stopped the cmdlet with a
# terminating "Illegal characters in path" error in Windows PowerShell. Such an item can't exist, so the cmdlet
# writes $false like for any other missing item, as in PowerShell 7 and like Test-Path.
It 'Should return $false for a path with the character <_> and continue with the next path' -ForEach @('|', '<', '>', '"', '*', '?') {
$invalid = Join-Path -Path $folder -ChildPath ('a{0}b' -f $_)
$result = @(Test-Path2 -Path $invalid, $file -ErrorVariable testErrors -ErrorAction SilentlyContinue)
$testErrors | Should -BeNullOrEmpty
$result -join ',' | Should -Be 'False,True'
}
# PowerShell 7 accepts these characters and finds no item, so only Windows PowerShell rejects the path. Before
# 5.0.0-rc6, the cmdlet wrote $false for a rejected path without saying why. -Debug would prompt in Windows
# PowerShell, so the test sets the preference.
It 'Should say in a debug message why it writes $false for a path that Windows PowerShell rejects' -Skip:($PSVersionTable.PSEdition -ne 'Desktop') {
$invalid = Join-Path -Path $folder -ChildPath 'a|b'
$DebugPreference = 'Continue'
$output = @(Test-Path2 -Path $invalid -ErrorAction Stop 5>&1)
$messages = @($output | Where-Object -FilterScript { $_ -is [Management.Automation.DebugRecord] } |
Where-Object -Property Message -Like -Value '*is not a valid path*')
$messages | Should -HaveCount 1
$messages[0].Message.Contains("'$invalid'") | Should -BeTrue
$output | Where-Object -FilterScript { $_ -is [bool] } | Should -BeFalse
}
}
Describe 'Get-DiskSpace' {
BeforeAll {
$systemDrive = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive
}
It 'Should return the size of the system drive' {
$result = @(Get-DiskSpace -DriveLetter $env:SystemDrive -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.DiskSpaceInfo]
$result[0].DriveName | Should -Be ('{0}\' -f $env:SystemDrive)
$result[0].TotalNumberOfBytes | Should -Be $systemDrive.TotalSize
}
It 'Should report free space and clusters that fit the size' {
$result = Get-DiskSpace -DriveLetter $env:SystemDrive -ErrorAction Stop
$result.TotalNumberOfFreeBytes | Should -BeLessOrEqual $result.TotalNumberOfBytes
$result.FreeBytesAvailable | Should -BeLessOrEqual $result.TotalNumberOfFreeBytes
$result.ClusterSize | Should -Be ($result.BytesPerSector * $result.SectorsPerCluster)
$result.NumberOfFreeClusters | Should -BeLessOrEqual $result.TotalNumberOfClusters
}
It 'Should return the volumes with a size greater than zero without -DriveLetter' {
$result = @(Get-DiskSpace -WarningAction SilentlyContinue -ErrorAction Stop)
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_.TotalNumberOfBytes | Should -BeGreaterThan 0 }
$result.TotalNumberOfBytes | Should -Contain $systemDrive.TotalSize
}
It 'Should warn and return nothing for a drive letter without a volume' {
$used = @((Get-PSDrive -PSProvider FileSystem).Name) + @([System.IO.DriveInfo]::GetDrives() | ForEach-Object -Process { $_.Name.Substring(0, 1) })
fix: pass on what a later command raises, and return every item for -Filter *.* A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message. Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns. The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*. Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code. Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt; Co-authored-by: AI Assistant &lt;ai@example.com&gt;
3 weeks ago
# The lowest free letter: New-TestDriveMapping takes letters from Z downward, also in a run in parallel.
$letter = [char[]](68..90) | Where-Object -FilterScript { [string] $_ -notin $used } | Select-Object -First 1
if (-not $letter) {
Set-ItResult -Skipped -Because 'every drive letter is in use'
return
}
$result = @(Get-DiskSpace -DriveLetter "${letter}:" -WarningVariable spaceWarnings -WarningAction SilentlyContinue -ErrorVariable spaceErrors -ErrorAction SilentlyContinue)
$result | Should -BeNullOrEmpty
$spaceErrors | Should -BeNullOrEmpty
$spaceWarnings.Message | Should -Be "Could not get drive details for '${letter}:'"
}
It 'Should reject a drive letter without a colon' {
{ Get-DiskSpace -DriveLetter 'C' -ErrorAction Stop } |
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetDiskSpace'
}
}
Describe 'Get-ChildItem2 when recursive enumeration becomes denied' {
It 'Should name the failed recursion in verbose output and continue with the next path' {
$root = New-TestSandboxItem -Sandbox $sandbox -Name 'ChangingReadPermission' -Directory
$child = Join-Path -Path $root -ChildPath 'Child'
$first = Join-Path -Path $root -ChildPath 'First.txt'
$nested = Join-Path -Path $child -ChildPath 'Nested.txt'
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'NextRecursivePath' -Directory
$nextFile = Join-Path -Path $next -ChildPath 'Next.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $root, $child, $first, $nested, $nextFile
New-Item -ItemType Directory -Path $child | Out-Null
Set-Content -LiteralPath $first -Value 'First'
Set-Content -LiteralPath $nested -Value 'Nested'
Set-Content -LiteralPath $nextFile -Value 'Next'
$ownerBefore = (Get-Acl -LiteralPath $root).Owner
# The first file is emitted before the separate recursive directory enumeration opens the folder again.
$records = @(Get-ChildItem2 -Path $root, $next -File -Recurse -Verbose -ErrorVariable childErrors -ErrorAction SilentlyContinue 4>&1 |
ForEach-Object {
if ($_ -is [Alphaleonis.Win32.Filesystem.FileInfo] -and $_.FullName -eq $first) {
Add-TestDenyRule -Sandbox $sandbox -Path $root -Rights @{ 'S-1-1-0' = 'ReadData' }
}
$_
})
$childErrors | Should -BeNullOrEmpty
$files = @($records | Where-Object { $_ -is [Alphaleonis.Win32.Filesystem.FileInfo] })
@($files.FullName | Sort-Object) | Should -Be @(@($first, $nextFile) | Sort-Object)
$messages = @($records | Where-Object { $_ -is [System.Management.Automation.VerboseRecord] })
$messages.Message | Should -Contain "Cannot access folder '$root' for recursive operation"
(Get-Acl -LiteralPath $root).Owner | Should -BeExactly $ownerBefore
Get-Content -LiteralPath $nested | Should -BeExactly 'Nested'
}
}