diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5db9b63..0b9045b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -368,4 +368,7 @@ The format is based on
`localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name,
where the computer doesn't offer the remote access check
+- Fix [Get-ChildItem2](Docs/Cmdlets/Get-ChildItem2.md) with `-Hidden`,
+ which omitted the first hidden item unless `-Force` was also supplied
+
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD
diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md
index af729af..b2e3519 100644
--- a/Docs/Cmdlets/Get-ChildItem2.md
+++ b/Docs/Cmdlets/Get-ChildItem2.md
@@ -307,7 +307,7 @@ The `PrivateData` section of the module manifest `NTFSSecurity.psd1` contains tw
A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors.
-Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones.
+Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones. Earlier builds, including the 5.0.0 prereleases, could also omit the first hidden item with `-Hidden` unless `-Force` was explicitly supplied.
## RELATED LINKS
diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
index bb908de..49d915a 100644
--- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
+++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
@@ -279,7 +279,7 @@ namespace NTFSSecurity
continue;
}
- var writeItem = force.ToBool();
+ var writeItem = force.ToBool() || hidden.ToBool();
if (MyInvocation.BoundParameters.ContainsKey("Attributes"))
{
@@ -291,9 +291,6 @@ namespace NTFSSecurity
}
else
{
- if (hidden)
- force = true;
-
if ((current.Attributes & global::System.IO.FileAttributes.Hidden) != global::System.IO.FileAttributes.Hidden)
writeItem = true;
diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
index fe60c28..251b553 100644
--- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
+++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
@@ -3865,7 +3865,7 @@ PS C:\> Disable-Privileges
The default table view shows the `Mode`, `Inherits`, `LastWriteTime`, `Size(M)`, and `Name` columns. `Inherits` is `False` for an item whose access inheritance is disabled. Reading that value costs one access to the ACL of each displayed item, which slows down the display of large listings; to avoid it, select the properties you need, for example with `Format-Table -Property Mode, LastWriteTime, Length, Name`. Objects that you pipe to another command are not affected. Before 5.0.0, the column showed `True` for every item.
The `PrivateData` section of the module manifest `NTFSSecurity.psd1` contains two settings that this cmdlet reads when it starts. `GetFileSystemModeProperty` adds the calculated `Mode` property to every item. `IdentifyHardLinks` adds the `HardLinkCount` property to every file, which requires an extra call into the file system for each file and therefore slows down large listings noticeably. Set either value to `$false` in the manifest and import the module again if you prefer the faster enumeration over the additional properties.
A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors.
- Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones.
+ Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones. Earlier builds, including the 5.0.0 prereleases, could also omit the first hidden item with `-Hidden` unless `-Force` was explicitly supplied.
diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1
index 760d83b..3b565f2 100644
--- a/Tests/ItemCmdlets.Tests.ps1
+++ b/Tests/ItemCmdlets.Tests.ps1
@@ -11,6 +11,7 @@ BeforeDiscovery {
# A path on the administrative share of the drive of the sandboxes is another volume for Windows, like a share of a
# file server.
$canUseAdminShare = Test-AdminShareAvailable
+ $canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
}
BeforeAll {
@@ -62,6 +63,189 @@ Describe 'Get-ChildItem2' {
}
}
+ 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 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' }
+ }
+
+ It 'Should return the exact tree for ' -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 ',')
+ }
+
+ 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
+ }
+ }
+
+ 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'
+ }
+ }
+
+ 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'
+ }
+ }
+
Context 'Default table view' {
BeforeAll {
$viewFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'View' -Directory