<#
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 ( )
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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'
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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.
test: cover token handles, drive-root writes, name filters, and descriptor APIs
New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.
Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Filter = 'Page.ht?' ; Expected = @ ( 'Page.htm' ) }
@ { Filter = 'Page2.h*' ; Expected = @ ( 'Page2.html' ) }
@ { Filter = 'Page2.' ; Expected = @ ( ) }
test: cover token handles, drive-root writes, name filters, and descriptor APIs
New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.
Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory
foreach ( $name in 'Page.htm' , 'Page2.html' ) {
$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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
( $result . Name | Sort-Object ) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm'
}
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The cmdlet now has one matcher, not two (Decision 27, item 4): every dot in -Filter whose single next
# 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1
# applies to Get-ChildItem -Filter, in both editions (Decision 27, item 4): Report.* also returns Report,
# 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.
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Filter = 'Report.*' ; Expected = 'Report,Report.txt' ; Outcome = 'the names with and without a dot' }
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
@ { 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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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' }
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
) {
$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 "DOS" 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 "*", and the new matcher decides
alone.
What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
instead of only names with two literal dots.
- Breaking: an empty -Filter ('') 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 '*'
explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
Get-ChildItem -Filter for a pattern that ends in a dot (which there
returns nothing) or for "*.?" (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 ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.
Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
"Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
"*.*.*" 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 "X.*" 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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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 ',' )
}
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'* "
}
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'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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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'
}
}
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$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
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$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
fix(items): refuse to move a folder to another volume in Move-Item2
Windows can't move a folder to another volume. Move-Item2 moved folders
with CopyAllowed, so AlphaFS copied and deleted them instead: an empty
folder was deleted without being created at the destination, and a
folder with files failed with an error that named one of its files.
Folders now move without CopyAllowed, and the cmdlet writes a MoveError
with the category InvalidOperation that names the folder and the
destination, and leaves the folder in place. A file still moves to
another volume; with -Force, it keeps the error of Windows, (17).
The tests move to \\localhost\C$, which Windows treats as another volume,
and run only elevated.
Decision 22, item 6: an assumption in autopilot, flagged for the
maintainer's review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$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
}
}
}
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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
}
}
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Test-Path2 -Path $long -PathType Container -ErrorAction Stop | Should -BeTrue
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
# 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
}
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
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 <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
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
fix: return false from Test-Path2 for a path with invalid characters
In Windows PowerShell, .NET rejects a path with a character that Windows
doesn't allow in names, such as | or <, and Test-Path2 stopped with the
terminating error "Illegal characters in path". Such an item can't
exist, so the cmdlet now writes false, as in PowerShell 7 and like
Test-Path. Add tests for every -PathType, long paths, relative paths,
and the pipeline, and for Get-DiskSpace, which had no tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
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'
}
}