From cc525f3ec766619c03ac5ed1ed68f714a9358686 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 08:30:25 +0200 Subject: [PATCH] fix: address the review of the -Attributes and Size alias changes An empty Get-ChildItem2 -Attributes value, such as 0 or None in PowerShell 7, matched every item and returned hidden items as well; it now stops the cmdlet with AttributesEmpty, as Get-ChildItem rejects it. The page says that the + and ! operators of Get-ChildItem aren't supported and that -Recurse still enters hidden folders, and the changelog says that a call with several attributes now returns more items. The type data test starts Windows PowerShell, where the import failed, from both CI legs and checks that LengthOnDisk is still there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 3 ++- CHANGELOG.md | 8 +++++-- Docs/Cmdlets/Get-ChildItem2.md | 4 ++-- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 10 ++++++++- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 6 +++--- Tests/ItemCmdlets.Tests.ps1 | 5 +++++ Tests/Manifest.Tests.ps1 | 22 +++++++++++++++----- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index 4997c55..97e21f9 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -190,6 +190,7 @@ Numbered as agreed with the maintainer; each is documented on its page. rights, an exact match as it is, otherwise without `Synchronize`. - Maintainer decisions of 2026-10-05: #5 (`Get-ChildItem2 -Attributes` matches any listed attribute) and #82 (no `Size` alias) ship in 5.0.0 as - breaking changes. + breaking changes. Their review added that an empty `-Attributes` value is + an error, as in `Get-ChildItem`. - Open bugs: #34 and #67 (writes owner and group, rc3 if a file server is available), #41 (drive root) and #90 (trailing space), both after 5.0.0. diff --git a/CHANGELOG.md b/CHANGELOG.md index 718cb6a..4e73cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,8 +51,12 @@ The format is based on `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules` - **Breaking:** `Get-ChildItem2 -Attributes` returns the items that have any of the listed attributes, like `Get-ChildItem`; it returned only the items - that had all of them. To get the old result, filter with `Where-Object`, - as the cmdlet page shows + that had all of them. A call that lists several attributes now returns + more items, including hidden and system items when those are in the + list, so review calls whose result is deleted or whose permissions are + changed. To get the old result, filter with `Where-Object`, as the cmdlet + page shows. An empty value, such as `0`, is now an error; it returned + every item, also the hidden ones ([#5](https://github.com/raandree/NTFSSecurity/issues/5)) - **Breaking:** remove the alias `Size` of `LengthOnDisk` from the files of `Get-ChildItem`, which made the import fail in Windows PowerShell when diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 0c43b87..af729af 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -69,7 +69,7 @@ Uses the `dir2` alias and returns the items of `C:\Data` that have the hidden or ### -Attributes -Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. To get only the items that have all of them, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. +Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones. ```yaml Type: FileAttributes @@ -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`, and `-Attributes` returned only the items that had all the listed attributes. +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. ## RELATED LINKS diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 246df25..bb908de 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -132,6 +132,14 @@ namespace NTFSSecurity { base.BeginProcessing(); + // An empty value would match every item, also the hidden ones; Get-ChildItem rejects it as well. + if (MyInvocation.BoundParameters.ContainsKey("Attributes") && attributes == 0) + { + ThrowTerminatingError(new ErrorRecord( + new ArgumentException("Specify at least one file attribute for the Attributes parameter."), + "AttributesEmpty", ErrorCategory.InvalidArgument, attributes)); + } + if (paths.Count == 0) { paths = new List() { GetCurrentLocation() }; @@ -276,7 +284,7 @@ namespace NTFSSecurity if (MyInvocation.BoundParameters.ContainsKey("Attributes")) { // Like Get-ChildItem, an item matches when it has any of the listed attributes (#5). - if (attributes != 0 && (current.Attributes & attributes) == 0) + if ((current.Attributes & attributes) == 0) continue; writeItem = true; diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index e8d1d7b..5cd0dd8 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -3524,7 +3524,7 @@ PS C:\> Disable-Privileges Attributes - Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. To get only the items that have all of them, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. + Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones. ReadOnly @@ -3668,7 +3668,7 @@ PS C:\> Disable-Privileges Attributes - Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. To get only the items that have all of them, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. + Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones. FileAttributes @@ -3857,7 +3857,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`, and `-Attributes` returned only the items that had all the listed attributes. + 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. diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 1308cd8..4d181d6 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -98,6 +98,11 @@ Describe 'Get-ChildItem2' { @($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) diff --git a/Tests/Manifest.Tests.ps1 b/Tests/Manifest.Tests.ps1 index 1aaf613..491287e 100644 --- a/Tests/Manifest.Tests.ps1 +++ b/Tests/Manifest.Tests.ps1 @@ -67,17 +67,29 @@ Describe 'Module manifest of NTFSSecurity' { } Describe 'Type data of NTFSSecurity' { + BeforeDiscovery { + # Only Windows PowerShell fails to import type data that conflicts with an existing member, so both CI legs + # start it. + $windowsPowerShell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe' + } + + BeforeAll { + $windowsPowerShell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe' + } + # Before 5.0.0, the types file added the alias Size to System.IO.FileInfo, so the import failed when another module # had added a member with that name (#82). The module is imported in a child process, because type data stays in a # session. - It 'Should import after another module added a Size member to System.IO.FileInfo' { + It 'Should import in Windows PowerShell after another module added a Size member to System.IO.FileInfo' -Skip:(-not (Test-Path -LiteralPath $windowsPowerShell)) { $manifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' - $executable = (Get-Process -Id $PID).Path + $manifestPath | Should -Exist + $quotedPath = $manifestPath.Replace("'", "''") $command = 'Update-TypeData -TypeName System.IO.FileInfo -MemberType AliasProperty -MemberName Size -Value Length -Force; ' + - ("Import-Module -Name '{0}' -ErrorAction Stop; 'IMPORTED'" -f $manifestPath.Replace("'", "''")) + ("Import-Module -Name '{0}' -ErrorAction Stop; " -f $quotedPath) + + ("if ((Get-Item -LiteralPath '{0}').PSObject.Properties['LengthOnDisk']) {{ 'IMPORTED' }}" -f $quotedPath) - $output = & $executable -NoProfile -NonInteractive -Command $command 2>&1 + $output = & $windowsPowerShell -NoProfile -NonInteractive -Command $command 2>&1 $output | Select-Object -Last 1 | Should -Be 'IMPORTED' } -} \ No newline at end of file +}