From 4dc832f658fa7675c6a453c81bd52b64fc5e6c54 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:42:38 +0200 Subject: [PATCH 01/11] fix: keep paths with braces out of the message format strings The message overloads of BaseCmdletWithPrivControl hide the methods of Cmdlet, so every message went through string.Format, also one without arguments. A path with braces in it, such as C:\Data\{Archive}, then stopped the cmdlet with a FormatException (#3). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 3 +++ NTFSSecurity/BaseCmdlets.cs | 8 +++++--- Tests/Access.Tests.ps1 | 9 +++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f4c0b7..3400c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,5 +169,8 @@ The format is based on `RIPEMD160` and `MACTripleDES`, which .NET lacks there, now stop the cmdlet with an error that names the algorithm and points to Windows PowerShell 5.1 +- Fix a `FormatException` in the cmdlets for a path with braces, such as + `C:\Data\{Archive}`: their messages formatted the path a second time + ([#3](https://github.com/raandree/NTFSSecurity/issues/3)) [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 8cdb84d..2aaf5da 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -337,18 +337,20 @@ namespace NTFSSecurity WriteDebug("The privilige 'Security' was disabled."); } + // These overloads hide the single-argument methods of Cmdlet, so a message without arguments must not be + // formatted: a path with braces in it would make string.Format throw (#3). protected void WriteWarning(string text, params string[] args) { - base.WriteWarning(string.Format(text, args)); + base.WriteWarning(args == null || args.Length == 0 ? text : string.Format(text, args)); } protected void WriteVerbose(string text, params string[] args) { - base.WriteVerbose(string.Format(text, args)); + base.WriteVerbose(args == null || args.Length == 0 ? text : string.Format(text, args)); } protected void WriteDebug(string text, params string[] args) { - base.WriteDebug(string.Format(text, args)); + base.WriteDebug(args == null || args.Length == 0 ? text : string.Format(text, args)); } } } \ No newline at end of file diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 5582a81..1008201 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -94,6 +94,15 @@ Describe 'Get-NTFSEffectiveAccess' { } Describe 'Get-NTFSOrphanedAccess' { + # Before 5.0.0, a path with braces stopped the cmdlet with a FormatException (#3). + It 'Should read a folder whose name contains braces' { + $braces = Join-Path -Path $sandbox -ChildPath ('{{Braces}}-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) + Assert-TestSandboxPath -Sandbox $sandbox -Path $braces + [IO.Directory]::CreateDirectory($braces) | Out-Null + + { Get-NTFSOrphanedAccess -Path $braces -ErrorAction Stop } | Should -Not -Throw + } + BeforeAll { $orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Orphaned' Assert-TestSandboxPath -Sandbox $sandbox -Path $orphanedFile From b45ac11b029c4814b5c21dba25df692b4c2ad74c Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:42:38 +0200 Subject: [PATCH 02/11] fix: read the current location from the session, not from $PWD A variable named PWD in the scope of the caller, such as a loop variable, hid the automatic variable, and every cmdlet failed with a NullReferenceException, also for an absolute path. The cmdlets now read the current file system location from the session state, and only for a relative path (#86). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 5 ++++ NTFSSecurity/BaseCmdlets.cs | 54 +++++++++++++------------------------ Tests/Owner.Tests.ps1 | 33 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3400c1f..4a1891b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,5 +172,10 @@ The format is based on - Fix a `FormatException` in the cmdlets for a path with braces, such as `C:\Data\{Archive}`: their messages formatted the path a second time ([#3](https://github.com/raandree/NTFSSecurity/issues/3)) +- Fix a `NullReferenceException` in every cmdlet when a variable named + `PWD` in the scope of the caller, such as a loop variable, hid the + automatic variable; the cmdlets now read the current location from the + session, and only for a relative path + ([#86](https://github.com/raandree/NTFSSecurity/issues/86)) [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 2aaf5da..36aad2f 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -26,32 +26,7 @@ namespace NTFSSecurity #region GetFileSystemInfo protected System.IO.FileSystemInfo GetFileSystemInfo(string path) { - string currentLocation = GetVariableValue("PWD").ToString(); - - if (path == ".") - { - path = currentLocation; - } - if (path.StartsWith("..")) - { - path = System.IO.Path.Combine( - string.Join("\\", currentLocation.Split('\\').Take(currentLocation.Split('\\').Count() - path.Split('\\').Count(s => s == "..")).ToArray()), - string.Join("\\", path.Split('\\').Where(e => e != "..").ToArray())); - } - else if (path.StartsWith(".")) - { - //combine . and .\path\subpath - path = System.IO.Path.Combine(currentLocation, path.Substring(2)); - } - else if (path.StartsWith("\\")) - { - //do nothing - } - else - { - ////combine . and \path\subpath or path\subpath - path = System.IO.Path.Combine(currentLocation, path.Substring(0)); - } + path = GetRelativePath(path); if (System.IO.File.Exists(path)) { @@ -114,18 +89,17 @@ namespace NTFSSecurity #region GetRelativePath protected string GetRelativePath(string path) { - string currentLocation = GetVariableValue("PWD").ToString(); - if (string.IsNullOrEmpty(path)) { - path = currentLocation; + path = GetCurrentLocation(); } else if (path == ".") { - path = currentLocation; + path = GetCurrentLocation(); } else if (path.StartsWith("..")) { + var currentLocation = GetCurrentLocation(); path = System.IO.Path.Combine( string.Join("\\", currentLocation.Split('\\').Take(currentLocation.Split('\\').Count() - path.Split('\\').Count(s => s == "..")).ToArray()), string.Join("\\", path.Split('\\').Where(e => e != "..").ToArray())); @@ -133,20 +107,28 @@ namespace NTFSSecurity else if (path.StartsWith(".")) { //combine . and .\path\subpath - path = System.IO.Path.Combine(currentLocation, path.Substring(2)); + path = System.IO.Path.Combine(GetCurrentLocation(), path.Substring(2)); } - else if (path.StartsWith("\\")) + else if (path.StartsWith("\\") || System.IO.Path.IsPathRooted(path)) { - //do nothing + //an absolute path needs no location } else { - ////combine . and \path\subpath or path\subpath - path = System.IO.Path.Combine(currentLocation, path); + ////combine . and path\subpath + path = System.IO.Path.Combine(GetCurrentLocation(), path); } return path; } + + // Read from the session state, not from $PWD: a variable named PWD in the caller's scope shadows the + // automatic variable and can be $null (#86). In a location of another provider, such as the registry, this + // is the last file system location. + private string GetCurrentLocation() + { + return SessionState.Path.CurrentFileSystemLocation.ProviderPath; + } #endregion #region InvokeAsOwner @@ -353,4 +335,4 @@ namespace NTFSSecurity base.WriteDebug(args == null || args.Length == 0 ? text : string.Format(text, args)); } } -} \ No newline at end of file +} diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 index 9c5d993..7ad1608 100644 --- a/Tests/Owner.Tests.ps1 +++ b/Tests/Owner.Tests.ps1 @@ -4,6 +4,9 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' )] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidAssignmentToAutomaticVariable', '', Justification = 'A test shadows $PWD on purpose (#86).' +)] param () BeforeDiscovery { @@ -52,3 +55,33 @@ Describe 'Get-NTFSOwner' { } } } + +Describe 'Current location' { + BeforeAll { + function Invoke-WithShadowedPwd { + param ([scriptblock] $Command) + + $PWD = $null + & $Command + } + } + + # Before 5.0.0, a variable named PWD in the scope of the caller, such as a loop variable, made every cmdlet + # fail with a NullReferenceException, also for an absolute path (#86). + It 'Should ignore a variable named PWD for an absolute path' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Pwd' + + $result = Invoke-WithShadowedPwd -Command { Get-NTFSOwner -Path $file -ErrorAction Stop } + + $result.FullName | Should -Be $file + } + + It 'Should resolve a relative path against the current location despite a variable named PWD' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'PwdRelative' + $name = Split-Path -Path $file -Leaf + + $result = Invoke-WithShadowedPwd -Command { Get-NTFSOwner -Path $name -ErrorAction Stop } + + $result.FullName | Should -Be $file + } +} From 3d75e784aed39e1001d22154895fd5a53fa669f1 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:42:39 +0200 Subject: [PATCH 03/11] fix: bind file and folder objects passed by position as their full path Windows PowerShell binds an object that is passed by position to a string parameter through ToString, which returns only the name of a child item, so the cmdlets looked for it in the current location. The path parameters now convert file and folder objects to their full path (#88). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 4 ++ NTFSSecurity/AccessCmdlets/AddAccess.cs | 1 + NTFSSecurity/AccessCmdlets/ClearAccess.cs | 1 + NTFSSecurity/AccessCmdlets/GetAccess.cs | 1 + .../AccessCmdlets/GetEffectiveAccess.cs | 1 + NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 1 + NTFSSecurity/AuditCmdlets/AddAudit.cs | 1 + NTFSSecurity/AuditCmdlets/ClearAudit.cs | 1 + NTFSSecurity/AuditCmdlets/GetAudit.cs | 1 + NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 1 + NTFSSecurity/BaseCmdlets.cs | 53 ++++++++++++++++++- .../DisableAccessInheritance.cs | 1 + .../DisableAuditInheritance.cs | 1 + .../EnableAccessInheritance.cs | 1 + .../EnableAuditInheritance.cs | 1 + .../InheritanceCmdlets/GetInheritance.cs | 1 + .../InheritanceCmdlets/SetInheritance.cs | 1 + NTFSSecurity/ItemCmdlets/CopyItem2.cs | 2 + NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 1 + NTFSSecurity/ItemCmdlets/GetItem2.cs | 1 + NTFSSecurity/ItemCmdlets/MoveItem2.cs | 2 + NTFSSecurity/ItemCmdlets/RemoveItem2.cs | 1 + NTFSSecurity/LinkCmdlets/GetHardLink.cs | 1 + NTFSSecurity/LinkCmdlets/NewHardLink.cs | 2 + NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs | 2 + NTFSSecurity/MiscCmdlets/GetFileHash2.cs | 1 + NTFSSecurity/OwnerCmdlets/GetOwner.cs | 1 + NTFSSecurity/OwnerCmdlets/SetOwner.cs | 1 + NTFSSecurity/PathCmdlets/TestPath2.cs | 1 + .../GetSecurityDescriptor.cs | 1 + Tests/Owner.Tests.ps1 | 24 +++++++++ 31 files changed, 112 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1891b..ab9003a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,5 +177,9 @@ The format is based on automatic variable; the cmdlets now read the current location from the session, and only for a relative path ([#86](https://github.com/raandree/NTFSSecurity/issues/86)) +- Fix file and folder objects passed by position, such as + `Get-NTFSOwner $folder`, which Windows PowerShell bound as the name of the + item, so the cmdlets looked for it in the current location + ([#88](https://github.com/raandree/NTFSSecurity/issues/88)) [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 4770aa9..f503aaa 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -23,6 +23,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AccessCmdlets/ClearAccess.cs b/NTFSSecurity/AccessCmdlets/ClearAccess.cs index 9e96a39..1e4a1c6 100644 --- a/NTFSSecurity/AccessCmdlets/ClearAccess.cs +++ b/NTFSSecurity/AccessCmdlets/ClearAccess.cs @@ -13,6 +13,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AccessCmdlets/GetAccess.cs b/NTFSSecurity/AccessCmdlets/GetAccess.cs index f010e18..1239306 100644 --- a/NTFSSecurity/AccessCmdlets/GetAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetAccess.cs @@ -20,6 +20,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs index bf1334c..96eb1db 100644 --- a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs @@ -20,6 +20,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index bd4c04d..a1c566b 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -24,6 +24,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index d5a11f2..f8b8584 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -23,6 +23,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AuditCmdlets/ClearAudit.cs b/NTFSSecurity/AuditCmdlets/ClearAudit.cs index 622e5d4..a6f7614 100644 --- a/NTFSSecurity/AuditCmdlets/ClearAudit.cs +++ b/NTFSSecurity/AuditCmdlets/ClearAudit.cs @@ -13,6 +13,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AuditCmdlets/GetAudit.cs b/NTFSSecurity/AuditCmdlets/GetAudit.cs index c569212..3c6c58c 100644 --- a/NTFSSecurity/AuditCmdlets/GetAudit.cs +++ b/NTFSSecurity/AuditCmdlets/GetAudit.cs @@ -20,6 +20,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index c9b618b..9d88c2b 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -24,6 +24,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 36aad2f..3ccf04f 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -335,4 +335,55 @@ namespace NTFSSecurity base.WriteDebug(args == null || args.Length == 0 ? text : string.Format(text, args)); } } -} + + /// + /// Converts file and folder objects to their full path. Windows PowerShell binds an object that is passed by + /// position to a string parameter through ToString, which returns only the name of a child item, so the cmdlet + /// resolved it against the current location (#88). + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public sealed class FileSystemPathTransformationAttribute : ArgumentTransformationAttribute + { + /// + /// Returns the full path of a file or folder object, or of each one in a collection, and any other value + /// unchanged. + /// + /// The engine APIs of the session. + /// The argument to transform. + /// The transformed argument. + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + var input = inputData is PSObject ? ((PSObject)inputData).BaseObject : inputData; + + if (input is string || !(input is IEnumerable)) + { + return ToPath(inputData); + } + + var result = new List(); + foreach (var item in (IEnumerable)input) + { + result.Add(ToPath(item)); + } + + return result.ToArray(); + } + + private static object ToPath(object value) + { + var baseObject = value is PSObject ? ((PSObject)value).BaseObject : value; + + if (baseObject is System.IO.FileSystemInfo) + { + return ((System.IO.FileSystemInfo)baseObject).FullName; + } + + if (baseObject is Alphaleonis.Win32.Filesystem.FileSystemInfo) + { + return ((Alphaleonis.Win32.Filesystem.FileSystemInfo)baseObject).FullName; + } + + return value; + } + } +} \ No newline at end of file diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs index 8204ba6..bdb964d 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs index 610acb3..6505e1c 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs @@ -16,6 +16,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs index 8db7bc6..16ea497 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs index a5f23cc..0c1a362 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs index 07d510c..fe44035 100644 --- a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs @@ -13,6 +13,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs index ecc4459..16ef6e6 100644 --- a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs @@ -16,6 +16,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/ItemCmdlets/CopyItem2.cs b/NTFSSecurity/ItemCmdlets/CopyItem2.cs index e0434c2..8481cc1 100644 --- a/NTFSSecurity/ItemCmdlets/CopyItem2.cs +++ b/NTFSSecurity/ItemCmdlets/CopyItem2.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } @@ -26,6 +27,7 @@ namespace NTFSSecurity } [Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)] + [FileSystemPathTransformation] public string Destination { get { return destination; } diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 96f08f1..9394b15 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -33,6 +33,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/ItemCmdlets/GetItem2.cs b/NTFSSecurity/ItemCmdlets/GetItem2.cs index f45d82c..4b3c662 100644 --- a/NTFSSecurity/ItemCmdlets/GetItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetItem2.cs @@ -13,6 +13,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/ItemCmdlets/MoveItem2.cs b/NTFSSecurity/ItemCmdlets/MoveItem2.cs index 4594d2f..c83c9e2 100644 --- a/NTFSSecurity/ItemCmdlets/MoveItem2.cs +++ b/NTFSSecurity/ItemCmdlets/MoveItem2.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } @@ -26,6 +27,7 @@ namespace NTFSSecurity } [Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)] + [FileSystemPathTransformation] public string Destination { get { return destination; } diff --git a/NTFSSecurity/ItemCmdlets/RemoveItem2.cs b/NTFSSecurity/ItemCmdlets/RemoveItem2.cs index b3caa91..59db095 100644 --- a/NTFSSecurity/ItemCmdlets/RemoveItem2.cs +++ b/NTFSSecurity/ItemCmdlets/RemoveItem2.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/LinkCmdlets/GetHardLink.cs b/NTFSSecurity/LinkCmdlets/GetHardLink.cs index fab966e..37f0d33 100644 --- a/NTFSSecurity/LinkCmdlets/GetHardLink.cs +++ b/NTFSSecurity/LinkCmdlets/GetHardLink.cs @@ -14,6 +14,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/LinkCmdlets/NewHardLink.cs b/NTFSSecurity/LinkCmdlets/NewHardLink.cs index cec9d5b..3ce89c6 100644 --- a/NTFSSecurity/LinkCmdlets/NewHardLink.cs +++ b/NTFSSecurity/LinkCmdlets/NewHardLink.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string Path { get { return paths[0]; } @@ -27,6 +28,7 @@ namespace NTFSSecurity [Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] + [FileSystemPathTransformation] public string Target { get { return target; } diff --git a/NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs b/NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs index 64ee2a1..091ae09 100644 --- a/NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs +++ b/NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string Path { get { return paths[0]; } @@ -27,6 +28,7 @@ namespace NTFSSecurity [Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] + [FileSystemPathTransformation] public string Target { get { return target; } diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index 113d8f0..9a9acb7 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -16,6 +16,7 @@ namespace NTFSSecurity [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/OwnerCmdlets/GetOwner.cs b/NTFSSecurity/OwnerCmdlets/GetOwner.cs index 3c7b524..e2c41f3 100644 --- a/NTFSSecurity/OwnerCmdlets/GetOwner.cs +++ b/NTFSSecurity/OwnerCmdlets/GetOwner.cs @@ -12,6 +12,7 @@ namespace NTFSSecurity.OwnerCmdlets [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/OwnerCmdlets/SetOwner.cs b/NTFSSecurity/OwnerCmdlets/SetOwner.cs index 360b6c5..705e250 100644 --- a/NTFSSecurity/OwnerCmdlets/SetOwner.cs +++ b/NTFSSecurity/OwnerCmdlets/SetOwner.cs @@ -15,6 +15,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/PathCmdlets/TestPath2.cs b/NTFSSecurity/PathCmdlets/TestPath2.cs index 8bb4dfd..e25409f 100644 --- a/NTFSSecurity/PathCmdlets/TestPath2.cs +++ b/NTFSSecurity/PathCmdlets/TestPath2.cs @@ -13,6 +13,7 @@ namespace NTFSSecurity [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs index 4d5e3f4..16a0f9d 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs @@ -12,6 +12,7 @@ namespace NTFSSecurity [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("FullName")] + [FileSystemPathTransformation] public string[] Path { get { return paths.ToArray(); } diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 index 7ad1608..026dec3 100644 --- a/Tests/Owner.Tests.ps1 +++ b/Tests/Owner.Tests.ps1 @@ -85,3 +85,27 @@ Describe 'Current location' { $result.FullName | Should -Be $file } } + +Describe 'File and folder objects as arguments' { + # Before 5.0.0, Windows PowerShell bound a folder object that was passed by position as its name, which the + # cmdlets resolved against the current location (#88). + It 'Should take a folder object by position' { + $parent = New-TestSandboxItem -Sandbox $sandbox -Name 'Parent' -Directory + $child = Join-Path -Path $parent -ChildPath 'Child' + Assert-TestSandboxPath -Sandbox $sandbox -Path $child + New-Item -ItemType Directory -Path $child | Out-Null + $folder = Get-ChildItem -LiteralPath $parent -Directory + + $result = Get-NTFSOwner $folder -ErrorAction Stop + + $result.FullName | Should -Be $child + } + + It 'Should take file objects through the pipeline as before' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Piped' + + $result = Get-Item -LiteralPath $file | Get-NTFSOwner + + $result.FullName | Should -Be $file + } +} \ No newline at end of file From 4c97719834a7262fb2b3c942018d320658868d5d Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:49:47 +0200 Subject: [PATCH 04/11] docs: answer recurring questions in an FAQ and correct two cmdlet pages The new FAQ page answers the questions that the issues ask again and again and links the pages with the details. The Get-NTFSEffectiveAccess page said that a security descriptor produces no result, and the Copy-Item2 page now says that -PassThru returns the copy; tests pin both -PassThru objects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Copy-Item2.md | 4 +- Docs/Cmdlets/Get-NTFSEffectiveAccess.md | 2 +- Docs/FAQ.md | 64 ++++++++++++++++++++ Docs/README.md | 3 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 6 +- Tests/ItemCmdlets.Tests.ps1 | 13 ++++ 6 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 Docs/FAQ.md diff --git a/Docs/Cmdlets/Copy-Item2.md b/Docs/Cmdlets/Copy-Item2.md index 0d78423..9e7b0b6 100644 --- a/Docs/Cmdlets/Copy-Item2.md +++ b/Docs/Cmdlets/Copy-Item2.md @@ -178,11 +178,11 @@ You can pipe an object that has a `Destination` property to supply the target of ### Alphaleonis.Win32.Filesystem.FileInfo -By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied. +By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied, pointing at the copy. ### Alphaleonis.Win32.Filesystem.DirectoryInfo -With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied. +With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied, pointing at the copy. ## NOTES diff --git a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md index c10b202..0385c01 100644 --- a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md +++ b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md @@ -164,7 +164,7 @@ One or more paths of files or folders, piped by value or by the property `FullNa ### Security2.FileSystemSecurity2[] -Security descriptors are accepted by the parameter binder but produce no result in this cmdlet. +One or more security descriptors that `Get-NTFSSecurityDescriptor` returned. The cmdlet calculates the effective access from the descriptor in memory instead of reading the item again. ### Security2.IdentityReference2 diff --git a/Docs/FAQ.md b/Docs/FAQ.md new file mode 100644 index 0000000..8f31dc8 --- /dev/null +++ b/Docs/FAQ.md @@ -0,0 +1,64 @@ +# Frequently asked questions + +Answers to questions that come up again and again in the issues. For the +background, read [Concepts](Concepts.md); for longer scripts, read +[Examples](Examples.md). + +## The module fails to load with HRESULT 0x80131515 + +Windows blocks the assemblies of a ZIP file that was downloaded from the +internet. Install the module from the PowerShell Gallery instead, as +described in [Installation](README.md#installation), or unblock the files of +a downloaded copy with `Get-ChildItem -Recurse | Unblock-File` before you +import it. + +## Access is denied, although I am an administrator + +Run PowerShell elevated. Only an elevated session holds the Backup, +Restore, Take Ownership, and Security privileges, which the cmdlets enable +to read and change items that your account has no rights on. Reading or +changing audit entries always needs the Security privilege. See +[Privileges](Concepts.md#privileges) and the module setting +`EnablePrivileges` in [Module settings](Concepts.md#module-settings). + +## Get-NTFSEffectiveAccess shows other rights than Explorer + +`Get-NTFSEffectiveAccess` calculates the rights that the NTFS permissions +of the item grant to one account. It doesn't include share permissions, +which Explorer adds for a path on a file share, and the account must be +resolvable on the computer that runs the cmdlet. See +[Get-NTFSEffectiveAccess](Cmdlets/Get-NTFSEffectiveAccess.md). + +## Get-ChildItem2 -Recurse runs in a loop through junctions + +A junction can point to a folder above it. Use `-SkipMountPoints` and +`-SkipSymbolicLinks` to leave out junctions and symbolic links when you +walk a tree, for example before you pipe the items to `Get-NTFSAccess`. +See [Get-ChildItem2](Cmdlets/Get-ChildItem2.md). + +## How do I restore permissions that I exported? + +`Get-NTFSAccess` returns the account, the rights, the type, and the +inheritance and propagation flags of each entry. `Add-NTFSAccess` binds +`-Account`, `-AccessRights`, `-AccessType`, `-InheritanceFlags`, and +`-PropagationFlags` by property name, so the objects, or the rows of a CSV +file with these columns and the path, can be piped back to it. See +[Add-NTFSAccess](Cmdlets/Add-NTFSAccess.md). + +## How do I apply the same audit entries to a whole folder tree? + +Add the entry to the top folder only. By default, `Add-NTFSAudit` applies +it to the folder, its subfolders, and its files, so the items below inherit +it. To remove other entries from the items below, use `Clear-NTFSAudit`, +and use `Enable-NTFSAuditInheritance` where inheritance is blocked. +Changing audit entries needs an elevated session. See +[Add-NTFSAudit](Cmdlets/Add-NTFSAudit.md). + +## Can I use a PowerShell drive in a path? + +No. The cmdlets read and write the file system directly through the AlphaFS +library, not through the PowerShell providers, so they don't know drives +that `New-PSDrive` created, or drives of other providers such as `HKLM:`. +Use the file system path instead, such as `C:\Data` or `\\server\share`. A +relative path is resolved against the current file system location. See +[Long paths](Concepts.md#long-paths). diff --git a/Docs/README.md b/Docs/README.md index 8d6bcff..956b1c0 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -72,7 +72,8 @@ Get-NTFSAccess -Path C:\Windows Read [Concepts](Concepts.md) for the background and [Examples](Examples.md) for common tasks. Every cmdlet has a reference page with all parameters and -examples; see the [cmdlet list](#cmdlets). +examples; see the [cmdlet list](#cmdlets). The [FAQ](FAQ.md) answers +questions that come up again and again. ## Cmdlets diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index fd81345..9b8a512 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -2166,7 +2166,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Alphaleonis.Win32.Filesystem.FileInfo - By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied. + By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied, pointing at the copy. @@ -2174,7 +2174,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Alphaleonis.Win32.Filesystem.DirectoryInfo - With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied. + With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied, pointing at the copy. @@ -5117,7 +5117,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd Security2.FileSystemSecurity2[] - Security descriptors are accepted by the parameter binder but produce no result in this cmdlet. + One or more security descriptors that `Get-NTFSSecurityDescriptor` returned. The cmdlet calculates the effective access from the descriptor in memory instead of reading the item again. diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index c629536..4586755 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -129,6 +129,19 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' { $messages.Message | Should -Contain ("File '{0}' {1} to '{2}'" -f $first, $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 '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') + } # 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 } From 9ad6ff7b93fb8a05211f9a99fcdfa5e7d4806ec2 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:55:43 +0200 Subject: [PATCH 05/11] chore(memory-bank): record the issue fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/activeContext.md | 6 +++++- .memory-bank/progress.md | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index b8c6f8b..b153a85 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -61,6 +61,10 @@ the PRs, and tags `5.0.0-rc2` after the merges. 395 passed, 26 skipped; PowerShell 7 366 passed, 55 skipped (421 tests). `Get-FileHash2` tests now run in PowerShell 7 as well. +- `ai/issue-fixes` fixes #3, #86, and #88 and adds `Docs/FAQ.md`: Windows + PowerShell 402 passed, 26 skipped; PowerShell 7 373 passed, 55 skipped + (428 tests). + ## Next step -The bugs from the issue triage (`ai/issue-fixes`), then 5.0.0-rc2. +5.0.0-rc2 on `ai/release-5.0.0-rc2`. diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index 5c15c67..39e21ef 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -179,3 +179,11 @@ Numbered as agreed with the maintainer; each is documented on its page. `MACTripleDES` uses a random key (verified), so it is deprecated. - Review of group E: the changelog now marks the `Set-NTFSInheritance` change as breaking and warns that it leaves broader access in place. + +#### Issue triage (fixes on `ai/issue-fixes`, not merged) + +- Fixed: #3 (braces in a path), #86 (`$PWD` shadowed), #88 (an object + passed by position); `Docs/FAQ.md` answers the recurring questions. +- Open bugs: #5 (`-Attributes` matches all, `Get-ChildItem` any; needs a + decision), #17 (`GenericAll`), #34 and #67 (writes owner and group), + #41 (drive root), #82 (`Size` type data), #90 (trailing space). From f660d24bbc107f050ea398f5fe93efe044e47eaa Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 03:23:36 +0200 Subject: [PATCH 06/11] fix: address the review of the issue fixes Nine cmdlets still read the PWD variable for the default location when -Path was omitted, so #86 remained for that form; they now use GetCurrentLocation. Copy-Item2 writes the object that CopyTo returns for the copy instead of relying on AlphaFS to update the source object, and a test covers a folder. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 5 +++-- NTFSSecurity/AccessCmdlets/GetAccess.cs | 2 +- .../AccessCmdlets/GetEffectiveAccess.cs | 2 +- NTFSSecurity/AuditCmdlets/GetAudit.cs | 2 +- NTFSSecurity/BaseCmdlets.cs | 11 +++++++---- .../InheritanceCmdlets/GetInheritance.cs | 2 +- NTFSSecurity/ItemCmdlets/CopyItem2.cs | 8 +++++--- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 2 +- NTFSSecurity/ItemCmdlets/GetItem2.cs | 2 +- NTFSSecurity/LinkCmdlets/GetHardLink.cs | 2 +- NTFSSecurity/PathCmdlets/TestPath2.cs | 2 +- .../GetSecurityDescriptor.cs | 2 +- Tests/ItemCmdlets.Tests.ps1 | 12 ++++++++++++ Tests/Owner.Tests.ps1 | 19 ++++++++++++++++++- 14 files changed, 54 insertions(+), 19 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index 39e21ef..51331ec 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -182,8 +182,9 @@ Numbered as agreed with the maintainer; each is documented on its page. #### Issue triage (fixes on `ai/issue-fixes`, not merged) -- Fixed: #3 (braces in a path), #86 (`$PWD` shadowed), #88 (an object - passed by position); `Docs/FAQ.md` answers the recurring questions. +- Fixed: #3 (braces in a path), #86 (`$PWD` shadowed, also for the default + location of nine cmdlets, found by the review), #88 (an object passed by + position); `Docs/FAQ.md` answers the recurring questions. - Open bugs: #5 (`-Attributes` matches all, `Get-ChildItem` any; needs a decision), #17 (`GenericAll`), #34 and #67 (writes owner and group), #41 (drive root), #82 (`Size` type data), #90 (trailing space). diff --git a/NTFSSecurity/AccessCmdlets/GetAccess.cs b/NTFSSecurity/AccessCmdlets/GetAccess.cs index 1239306..95af170 100644 --- a/NTFSSecurity/AccessCmdlets/GetAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetAccess.cs @@ -74,7 +74,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } } diff --git a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs index 96eb1db..1e63063 100644 --- a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs @@ -109,7 +109,7 @@ namespace NTFSSecurity } // Like the other cmdlets, use the current location when -Path is omitted. - var targets = paths.Count > 0 ? paths : new List() { GetVariableValue("PWD").ToString() }; + var targets = paths.Count > 0 ? paths : new List() { GetCurrentLocation() }; foreach (var path in targets) { diff --git a/NTFSSecurity/AuditCmdlets/GetAudit.cs b/NTFSSecurity/AuditCmdlets/GetAudit.cs index 3c6c58c..fabe976 100644 --- a/NTFSSecurity/AuditCmdlets/GetAudit.cs +++ b/NTFSSecurity/AuditCmdlets/GetAudit.cs @@ -74,7 +74,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } } diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 3ccf04f..0571c96 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -122,10 +122,13 @@ namespace NTFSSecurity return path; } - // Read from the session state, not from $PWD: a variable named PWD in the caller's scope shadows the - // automatic variable and can be $null (#86). In a location of another provider, such as the registry, this - // is the last file system location. - private string GetCurrentLocation() + /// + /// Returns the current file system location of the session. It is read from the session state, not from + /// $PWD, which a variable named PWD in the scope of the caller can hide (#86). In a location of another + /// provider, such as the registry, this is the last file system location. + /// + /// The provider path of the current file system location. + protected string GetCurrentLocation() { return SessionState.Path.CurrentFileSystemLocation.ProviderPath; } diff --git a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs index fe44035..9b02228 100644 --- a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs @@ -42,7 +42,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } } diff --git a/NTFSSecurity/ItemCmdlets/CopyItem2.cs b/NTFSSecurity/ItemCmdlets/CopyItem2.cs index 8481cc1..0775bc7 100644 --- a/NTFSSecurity/ItemCmdlets/CopyItem2.cs +++ b/NTFSSecurity/ItemCmdlets/CopyItem2.cs @@ -97,12 +97,13 @@ namespace NTFSSecurity try { var processed = false; + FileSystemInfo copy = null; if (item is FileInfo) { if (ShouldProcess(resolvedPath, "Copy File")) { - ((FileInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath); + copy = ((FileInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath); WriteVerbose(string.Format("File '{0}' copied to '{1}'", resolvedPath, actualDestination)); processed = true; } @@ -114,14 +115,15 @@ namespace NTFSSecurity // AlphaFS 2.2 copies into an existing folder only and otherwise fails with a // DirectoryNotFoundException for the first file. Directory.CreateDirectory(actualDestination); - ((DirectoryInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath); + copy = ((DirectoryInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath); WriteVerbose(string.Format("Directory '{0}' copied to '{1}'", resolvedPath, actualDestination)); processed = true; } } + // Write the object for the copy that CopyTo returns, not the source item. if (passThru && processed) - WriteObject(item); + WriteObject(copy); } catch (System.IO.IOException ex) { diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 9394b15..70aaa2b 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -134,7 +134,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } wildcard = new WildcardPattern(filter, WildcardOptions.Compiled | WildcardOptions.IgnoreCase); diff --git a/NTFSSecurity/ItemCmdlets/GetItem2.cs b/NTFSSecurity/ItemCmdlets/GetItem2.cs index 4b3c662..c2fe9e8 100644 --- a/NTFSSecurity/ItemCmdlets/GetItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetItem2.cs @@ -30,7 +30,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode"); diff --git a/NTFSSecurity/LinkCmdlets/GetHardLink.cs b/NTFSSecurity/LinkCmdlets/GetHardLink.cs index 37f0d33..7f96b1a 100644 --- a/NTFSSecurity/LinkCmdlets/GetHardLink.cs +++ b/NTFSSecurity/LinkCmdlets/GetHardLink.cs @@ -31,7 +31,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode"); diff --git a/NTFSSecurity/PathCmdlets/TestPath2.cs b/NTFSSecurity/PathCmdlets/TestPath2.cs index e25409f..b443d73 100644 --- a/NTFSSecurity/PathCmdlets/TestPath2.cs +++ b/NTFSSecurity/PathCmdlets/TestPath2.cs @@ -37,7 +37,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths = new List() { GetVariableValue("PWD").ToString() }; + paths = new List() { GetCurrentLocation() }; } } diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs index 16a0f9d..4d2f634 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs @@ -29,7 +29,7 @@ namespace NTFSSecurity if (paths.Count == 0) { - paths.Add(GetVariableValue("PWD").ToString()); + paths.Add(GetCurrentLocation()); } } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 4586755..336091a 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -137,6 +137,18 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' { $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 diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 index 026dec3..6e169ac 100644 --- a/Tests/Owner.Tests.ps1 +++ b/Tests/Owner.Tests.ps1 @@ -58,6 +58,8 @@ Describe 'Get-NTFSOwner' { Describe 'Current location' { BeforeAll { + # The command runs in a child scope of this function, so the cmdlets see its $PWD = $null through the scope + # chain, as in the report. function Invoke-WithShadowedPwd { param ([scriptblock] $Command) @@ -84,6 +86,21 @@ Describe 'Current location' { $result.FullName | Should -Be $file } + + It '<_> should use the current location without -Path despite a variable named PWD' -ForEach @( + 'Get-NTFSAccess', 'Get-NTFSAudit', 'Get-NTFSEffectiveAccess', 'Get-NTFSInheritance', 'Get-ChildItem2', + 'Get-Item2', 'Get-NTFSSecurityDescriptor' + ) { + $cmdlet = $_ + + { Invoke-WithShadowedPwd -Command { & $cmdlet -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } } | + Should -Not -Throw + } + + It 'Get-NTFSHardLink should report the folder of the current location, not a NullReferenceException' { + { Invoke-WithShadowedPwd -Command { Get-NTFSHardLink -ErrorAction SilentlyContinue } } | + Should -Throw -ExpectedMessage '*must be a file*' + } } Describe 'File and folder objects as arguments' { @@ -108,4 +125,4 @@ Describe 'File and folder objects as arguments' { $result.FullName | Should -Be $file } -} \ No newline at end of file +} From fbab6aa292c253f0e873ab9ee0c514759546667c Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 03:39:52 +0200 Subject: [PATCH 07/11] fix: remove access entries with generic rights such as GenericAll FileSystemSecurity.RemoveAccessRule rebuilds a rule that doesn't match an entry exactly and rejects generic rights then, so removing an entry with GENERIC_ALL failed with "The value '269484032' is not valid". Windows keeps generic rights in the inherit-only entries of folders. Such a rule is now removed through ModifyAccessRule, without the added Synchronize right (#17). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 7 ++- CHANGELOG.md | 4 ++ Docs/Cmdlets/Remove-NTFSAccess.md | 2 + NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 1 + ...AccessRule2.RemoveFileSystemAccessRules.cs | 59 +++++++++++-------- Tests/Access.Tests.ps1 | 19 ++++++ 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index 51331ec..a6f4ec4 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -184,7 +184,8 @@ Numbered as agreed with the maintainer; each is documented on its page. - Fixed: #3 (braces in a path), #86 (`$PWD` shadowed, also for the default location of nine cmdlets, found by the review), #88 (an object passed by - position); `Docs/FAQ.md` answers the recurring questions. + position), #17 (an entry with `GenericAll`); `Docs/FAQ.md` answers the + recurring questions. - Open bugs: #5 (`-Attributes` matches all, `Get-ChildItem` any; needs a - decision), #17 (`GenericAll`), #34 and #67 (writes owner and group), - #41 (drive root), #82 (`Size` type data), #90 (trailing space). + decision), #34 and #67 (writes owner and group), #41 (drive root), #82 + (`Size` type data), #90 (trailing space). diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9003a..06aeafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -181,5 +181,9 @@ The format is based on `Get-NTFSOwner $folder`, which Windows PowerShell bound as the name of the item, so the cmdlets looked for it in the current location ([#88](https://github.com/raandree/NTFSSecurity/issues/88)) +- Fix `Remove-NTFSAccess` for an entry with a generic right such as + `GenericAll`, which Windows keeps in the inherit-only entries of folders; + it failed with "The value '269484032' is not valid" + ([#17](https://github.com/raandree/NTFSSecurity/issues/17)) [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index e2b4c18..56090b6 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -304,6 +304,8 @@ If the ACL of an item cannot be written because access is denied, the cmdlet tri Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged. +An entry with a generic right, such as `GenericAll`, can be removed, for example by piping it from `Get-NTFSAccess`. Windows keeps generic rights in the inherit-only entries of folders. Before 5.0.0, the cmdlet failed for such an entry with the error "The value '269484032' is not valid for this usage of the type FileSystemRights". + Before 5.0.0, the `-RemoveSpecific` switch was missing, although version 4.1 had introduced it. A path that does not exist produces the non-terminating error `ReadFileError`, and the cmdlet continues with the next path. Before 5.0.0, the cmdlet also wrote a misleading `RemoveAceError` for that path, and with `-PassThru` it stopped with a `NullReferenceException`. diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 9b8a512..888514c 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -8416,6 +8416,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged. + An entry with a generic right, such as `GenericAll`, can be removed, for example by piping it from `Get-NTFSAccess`. Windows keeps generic rights in the inherit-only entries of folders. Before 5.0.0, the cmdlet failed for such an entry with the error "The value '269484032' is not valid for this usage of the type FileSystemRights". Before 5.0.0, the `-RemoveSpecific` switch was missing, although version 4.1 had introduced it. A path that does not exist produces the non-terminating error `ReadFileError`, and the cmdlet continues with the next path. Before 5.0.0, the cmdlet also wrote a misleading `RemoveAceError` for that path, and with `-PassThru` it stopped with a `NullReferenceException`. diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs index ae33da2..f217ed6 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs @@ -6,9 +6,34 @@ namespace Security2 { public partial class FileSystemAccessRule2 { + // Generic rights, such as GENERIC_ALL, appear in inherit-only entries. FileSystemSecurity.RemoveAccessRule + // rebuilds a rule that doesn't match exactly and rejects generic rights then, so such a rule is removed + // through ModifyAccessRule, which works on the access mask (#17). + private static bool HasGenericRights(FileSystemRights2 rights) + { + return ((long)rights & 0xF0000000L) != 0; + } + + private static void RemoveRule(FileSystemSecurity sd, FileSystemAccessRule ace, bool removeSpecific) + { + if (HasGenericRights((FileSystemRights2)(int)ace.FileSystemRights)) + { + bool modified; + sd.ModifyAccessRule(removeSpecific ? AccessControlModification.RemoveSpecific : AccessControlModification.Remove, ace, out modified); + } + else if (removeSpecific) + { + sd.RemoveAccessRuleSpecific(ace); + } + else + { + sd.RemoveAccessRule(ace); + } + } + public static void RemoveFileSystemAccessRule(FileSystemInfo item, IdentityReference2 account, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, bool removeSpecific = false) { - if (type == AccessControlType.Allow) + if (type == AccessControlType.Allow && !HasGenericRights(rights)) rights = rights | FileSystemRights2.Synchronize; FileSystemAccessRule ace = null; @@ -19,10 +44,7 @@ namespace Security2 var sd = file.GetAccessControl(AccessControlSections.Access); ace = (FileSystemAccessRule)sd.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type); - if (removeSpecific) - sd.RemoveAccessRuleSpecific(ace); - else - sd.RemoveAccessRule(ace); + RemoveRule(sd, ace, removeSpecific); file.SetAccessControl(sd); } @@ -33,10 +55,7 @@ namespace Security2 var sd = directory.GetAccessControl(AccessControlSections.Access); ace = (FileSystemAccessRule)sd.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type); - if (removeSpecific) - sd.RemoveAccessRuleSpecific(ace); - else - sd.RemoveAccessRule(ace); + RemoveRule(sd, ace, removeSpecific); directory.SetAccessControl(sd); } @@ -85,10 +104,7 @@ namespace Security2 var file = (FileInfo)item; var sd = file.GetAccessControl(AccessControlSections.Access); - if (removeSpecific) - sd.RemoveAccessRuleSpecific(ace); - else - sd.RemoveAccessRule(ace); + RemoveRule(sd, ace, removeSpecific); file.SetAccessControl(sd); } @@ -98,10 +114,7 @@ namespace Security2 var sd = directory.GetAccessControl(AccessControlSections.Access); - if (removeSpecific) - sd.RemoveAccessRuleSpecific(ace); - else - sd.RemoveAccessRule(ace); + RemoveRule(sd, ace, removeSpecific); directory.SetAccessControl(sd); } @@ -109,23 +122,17 @@ namespace Security2 public static FileSystemAccessRule2 RemoveFileSystemAccessRule(FileSystemSecurity2 sd, IdentityReference2 account, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, bool removeSpecific = false) { - if (type == AccessControlType.Allow) + if (type == AccessControlType.Allow && !HasGenericRights(rights)) rights = rights | FileSystemRights2.Synchronize; var ace = (FileSystemAccessRule)sd.SecurityDescriptor.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type); if (sd.IsFile) { - if (removeSpecific) - ((FileSecurity)sd.SecurityDescriptor).RemoveAccessRuleSpecific(ace); - else - ((FileSecurity)sd.SecurityDescriptor).RemoveAccessRule(ace); + RemoveRule(((FileSecurity)sd.SecurityDescriptor), ace, removeSpecific); } else { - if (removeSpecific) - ((DirectorySecurity)sd.SecurityDescriptor).RemoveAccessRuleSpecific(ace); - else - ((DirectorySecurity)sd.SecurityDescriptor).RemoveAccessRule(ace); + RemoveRule(((DirectorySecurity)sd.SecurityDescriptor), ace, removeSpecific); } return ace; diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 1008201..a3e1f62 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -259,6 +259,25 @@ Describe 'Add-NTFSAccess' { } } +Describe 'Remove-NTFSAccess with generic rights' { + # Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is not + # valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in + # inherit-only entries of folders. + It 'Should remove an inherit-only GenericAll entry that Get-NTFSAccess returned' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory + Assert-TestSandboxPath -Sandbox $sandbox -Path $folder + $acl = Get-Acl -LiteralPath $folder + $acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?[A-Z]*)', 'D:${flags}(A;OICIIO;GA;;;S-1-5-32-546)')) + Set-Acl -LiteralPath $folder -AclObject $acl + (Get-Acl -LiteralPath $folder).Sddl | Should -Match '\(A;OICIIO;GA;;;BG\)' + + Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited | + Remove-NTFSAccess -ErrorVariable removeErrors -ErrorAction SilentlyContinue + + $removeErrors | Should -BeNullOrEmpty + (Get-Acl -LiteralPath $folder).Sddl | Should -Not -Match ';;;BG\)' + } +} Describe 'Security descriptor parameter sets' { BeforeAll { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParameterSets' -Directory From ada006f5f017f1c2fa5ac614c4ee149f00bf1412 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 04:01:33 +0200 Subject: [PATCH 08/11] fix: remove generic rights the way FileSystemSecurity removes other rights The fix for #17 stopped adding Synchronize to an Allow rule with generic rights, which bypassed the exact match of FileSystemSecurity.RemoveAccessRule: -AccessRights GenericAll left a Synchronize-only entry behind when the entry also had Synchronize. RemoveRule now does what FileSystemSecurity does, without its validation: it removes a rule that matches an entry exactly as it is, and otherwise without Synchronize. It covers every mask that .NET rejects, not only the generic rights. The tests cover -RemoveSpecific, a Deny entry, a partial generic mask, and that nothing else is removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 2 + ...AccessRule2.RemoveFileSystemAccessRules.cs | 63 ++++++++------ Tests/Access.Tests.ps1 | 86 +++++++++++++++---- 3 files changed, 105 insertions(+), 46 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index a6f4ec4..d492941 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -186,6 +186,8 @@ Numbered as agreed with the maintainer; each is documented on its page. location of nine cmdlets, found by the review), #88 (an object passed by position), #17 (an entry with `GenericAll`); `Docs/FAQ.md` answers the recurring questions. +- Review of #17: generic rights are removed the way .NET removes other + rights, an exact match as it is, otherwise without `Synchronize`. - Open bugs: #5 (`-Attributes` matches all, `Get-ChildItem` any; needs a decision), #34 and #67 (writes owner and group), #41 (drive root), #82 (`Size` type data), #90 (trailing space). diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs index f217ed6..71d171e 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs @@ -1,39 +1,55 @@ using Alphaleonis.Win32.Filesystem; using System.Collections.Generic; +using System.Linq; using System.Security.AccessControl; +using System.Security.Principal; namespace Security2 { public partial class FileSystemAccessRule2 { - // Generic rights, such as GENERIC_ALL, appear in inherit-only entries. FileSystemSecurity.RemoveAccessRule - // rebuilds a rule that doesn't match exactly and rejects generic rights then, so such a rule is removed - // through ModifyAccessRule, which works on the access mask (#17). - private static bool HasGenericRights(FileSystemRights2 rights) + // Rights that FileSystemAccessRule.AccessMaskFromRights rejects, such as the generic rights, which Windows + // keeps in the inherit-only entries of folders (#17). + private static bool HasUnsupportedRights(int accessMask) { - return ((long)rights & 0xF0000000L) != 0; + return accessMask < 0 || accessMask > (int)FileSystemRights.FullControl; } + // FileSystemSecurity.RemoveAccessRule removes a rule that matches an entry exactly as it is, and otherwise + // rebuilds it without the Synchronize right, which fails for unsupported rights. For those, do the same + // without rebuilding the rule. private static void RemoveRule(FileSystemSecurity sd, FileSystemAccessRule ace, bool removeSpecific) { - if (HasGenericRights((FileSystemRights2)(int)ace.FileSystemRights)) - { - bool modified; - sd.ModifyAccessRule(removeSpecific ? AccessControlModification.RemoveSpecific : AccessControlModification.Remove, ace, out modified); - } - else if (removeSpecific) - { - sd.RemoveAccessRuleSpecific(ace); - } - else + var accessMask = (int)ace.FileSystemRights; + + if (!HasUnsupportedRights(accessMask)) { - sd.RemoveAccessRule(ace); + if (removeSpecific) + sd.RemoveAccessRuleSpecific(ace); + else + sd.RemoveAccessRule(ace); + + return; } - } + var sid = (SecurityIdentifier)ace.IdentityReference.Translate(typeof(SecurityIdentifier)); + var exactMatch = sd.GetAccessRules(true, true, typeof(SecurityIdentifier)) + .OfType() + .Any(rule => (int)rule.FileSystemRights == accessMask && + rule.IdentityReference == sid && + rule.AccessControlType == ace.AccessControlType); + + var ruleToRemove = exactMatch ? ace : (FileSystemAccessRule)sd.AccessRuleFactory(sid, + accessMask & ~(int)FileSystemRights.Synchronize, false, ace.InheritanceFlags, ace.PropagationFlags, ace.AccessControlType); + + // Like RemoveAccessRule, ignore whether an entry was changed: removing an entry that doesn't exist is not + // an error. + bool modified; + sd.ModifyAccessRule(removeSpecific ? AccessControlModification.RemoveSpecific : AccessControlModification.Remove, ruleToRemove, out modified); + } public static void RemoveFileSystemAccessRule(FileSystemInfo item, IdentityReference2 account, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, bool removeSpecific = false) { - if (type == AccessControlType.Allow && !HasGenericRights(rights)) + if (type == AccessControlType.Allow) rights = rights | FileSystemRights2.Synchronize; FileSystemAccessRule ace = null; @@ -122,18 +138,11 @@ namespace Security2 public static FileSystemAccessRule2 RemoveFileSystemAccessRule(FileSystemSecurity2 sd, IdentityReference2 account, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, bool removeSpecific = false) { - if (type == AccessControlType.Allow && !HasGenericRights(rights)) + if (type == AccessControlType.Allow) rights = rights | FileSystemRights2.Synchronize; var ace = (FileSystemAccessRule)sd.SecurityDescriptor.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type); - if (sd.IsFile) - { - RemoveRule(((FileSecurity)sd.SecurityDescriptor), ace, removeSpecific); - } - else - { - RemoveRule(((DirectorySecurity)sd.SecurityDescriptor), ace, removeSpecific); - } + RemoveRule(sd.SecurityDescriptor, ace, removeSpecific); return ace; } diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index a3e1f62..d89bd74 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -208,6 +208,73 @@ Describe 'Remove-NTFSAccess' { } } + Context 'With a generic right' { + # Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is + # not valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in + # inherit-only entries of folders. + BeforeAll { + $guests = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-546' + + function New-GenericRightFolder { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the sandbox.' + )] + param ([string] $Entry) + + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory + Assert-TestSandboxPath -Sandbox $sandbox -Path $folder + $acl = Get-Acl -LiteralPath $folder + $acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?[A-Z]*)', ('D:${flags}' + $Entry))) + Set-Acl -LiteralPath $folder -AclObject $acl + $folder + } + + function Get-GuestsRule { + param ([string] $Path) + + (Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | + Where-Object -Property IdentityReference -EQ -Value $guests + } + } + + It 'Should remove an inherit-only entry that Get-NTFSAccess returned, and nothing else' -ForEach @( + @{ Case = 'Allow'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $false } + @{ Case = 'Allow with -RemoveSpecific'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $true } + @{ Case = 'Deny'; Entry = '(D;OICIIO;GA;;;BG)'; Specific = $false } + ) { + $folder = New-GenericRightFolder -Entry $Entry + $before = (Get-Acl -LiteralPath $folder).Sddl + $before | Should -Match ([regex]::Escape($Entry)) + + Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited | + Remove-NTFSAccess -RemoveSpecific:$Specific -ErrorVariable removeErrors -ErrorAction SilentlyContinue + + $removeErrors | Should -BeNullOrEmpty + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before.Replace($Entry, '') + } + + It 'Should remove only the requested generic right from an entry with two' { + $folder = New-GenericRightFolder -Entry '(A;OICIIO;0x90000000;;;BG)' + + Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericRead -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue + + $removeErrors | Should -BeNullOrEmpty + $rule = Get-GuestsRule -Path $folder + $rule | Should -HaveCount 1 + [int] $rule.FileSystemRights | Should -Be 0x10000000 + } + + # A rule that matches the entry exactly is removed as it is, with the Synchronize right that the module adds + # to an Allow rule; otherwise an entry with only Synchronize would be left behind. + It 'Should remove an entry with GenericAll and Synchronize when -AccessRights names GenericAll' { + $folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BG)' + + Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue + + $removeErrors | Should -BeNullOrEmpty + Get-GuestsRule -Path $folder | Should -BeNullOrEmpty + } + } Context 'With -RemoveSpecific' { BeforeEach { $removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory @@ -259,25 +326,6 @@ Describe 'Add-NTFSAccess' { } } -Describe 'Remove-NTFSAccess with generic rights' { - # Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is not - # valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in - # inherit-only entries of folders. - It 'Should remove an inherit-only GenericAll entry that Get-NTFSAccess returned' { - $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory - Assert-TestSandboxPath -Sandbox $sandbox -Path $folder - $acl = Get-Acl -LiteralPath $folder - $acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?[A-Z]*)', 'D:${flags}(A;OICIIO;GA;;;S-1-5-32-546)')) - Set-Acl -LiteralPath $folder -AclObject $acl - (Get-Acl -LiteralPath $folder).Sddl | Should -Match '\(A;OICIIO;GA;;;BG\)' - - Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited | - Remove-NTFSAccess -ErrorVariable removeErrors -ErrorAction SilentlyContinue - - $removeErrors | Should -BeNullOrEmpty - (Get-Acl -LiteralPath $folder).Sddl | Should -Not -Match ';;;BG\)' - } -} Describe 'Security descriptor parameter sets' { BeforeAll { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParameterSets' -Directory From 55be4d7842ed3d45d58752c52c029e135ec3f6b3 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 08:09:16 +0200 Subject: [PATCH 09/11] feat!: match any listed attribute in Get-ChildItem2 -Attributes Get-ChildItem2 -Attributes returned only the items that had all the listed attributes, so -Attributes Hidden, ReadOnly returned nothing where Get-ChildItem returns both kinds of items. It now returns the items that have any of them (#5). BREAKING CHANGE: to get only the items with all the listed attributes, filter the result with Where-Object, as the cmdlet page shows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 5 ++++ Docs/Cmdlets/Get-ChildItem2.md | 8 +++--- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 3 ++- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 10 ++++---- Tests/ItemCmdlets.Tests.ps1 | 26 ++++++++++++++++++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06aeafb..4814538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,11 @@ The format is based on before. To remove the entries, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `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 + ([#5](https://github.com/raandree/NTFSSecurity/issues/5)) ### Deprecated diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 5444ce2..0c43b87 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -57,19 +57,19 @@ PS C:\> Get-ChildItem2 -Path C:\Data -Recurse -Depth 1 -Filter '*.log' Returns the log files in `C:\Data` and in its immediate subfolders. Without `-Depth`, the command would descend through the entire tree. -### Example 4: List hidden system files +### Example 4: List hidden or system files ```PowerShell PS C:\> dir2 -Path C:\Data -Attributes Hidden, System ``` -Uses the `dir2` alias and returns the items of `C:\Data` that have both the hidden and the system attribute. +Uses the `dir2` alias and returns the items of `C:\Data` that have the hidden or the system attribute, like `Get-ChildItem -Attributes Hidden, System`. ## PARAMETERS ### -Attributes -Specifies a set of file attributes. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes 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`. 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`. ```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`. +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. ## RELATED LINKS diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 70aaa2b..246df25 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -275,7 +275,8 @@ namespace NTFSSecurity if (MyInvocation.BoundParameters.ContainsKey("Attributes")) { - if ((current.Attributes & attributes) != attributes) + // Like Get-ChildItem, an item matches when it has any of the listed attributes (#5). + if (attributes != 0 && (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 888514c..e8d1d7b 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. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes 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`. 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`. ReadOnly @@ -3668,7 +3668,7 @@ PS C:\> Disable-Privileges Attributes - Specifies a set of file attributes. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes 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`. 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`. 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`. + 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. @@ -3883,10 +3883,10 @@ PS C:\> Disable-Privileges - ------------- Example 4: List hidden system files ------------- + ------------ Example 4: List hidden or system files ------------ PS C:\> dir2 -Path C:\Data -Attributes Hidden, System - Uses the `dir2` alias and returns the items of `C:\Data` that have both the hidden and the system attribute. + Uses the `dir2` alias and returns the items of `C:\Data` that have the hidden or the system attribute, like `Get-ChildItem -Attributes Hidden, System`. diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 336091a..1308cd8 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -78,6 +78,32 @@ Describe 'Get-ChildItem2' { ($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') + } + + 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' { From 5b8ae9f19a5700188d89f98e1b004063ac7cb344 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 08:10:54 +0200 Subject: [PATCH 10/11] feat!: remove the Size alias that blocked the import next to other modules The types file added the alias Size of LengthOnDisk to System.IO.FileInfo. Type data can't replace an existing member, so in Windows PowerShell the import failed when another module had added a Size member first (#82). BREAKING CHANGE: use LengthOnDisk instead of Size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 8 +++++--- CHANGELOG.md | 4 ++++ Docs/Concepts.md | 2 +- NTFSSecurity/NTFSSecurity.types.ps1xml | 4 ---- Tests/Manifest.Tests.ps1 | 16 ++++++++++++++++ 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index d492941..4997c55 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -188,6 +188,8 @@ Numbered as agreed with the maintainer; each is documented on its page. recurring questions. - Review of #17: generic rights are removed the way .NET removes other rights, an exact match as it is, otherwise without `Synchronize`. -- Open bugs: #5 (`-Attributes` matches all, `Get-ChildItem` any; needs a - decision), #34 and #67 (writes owner and group), #41 (drive root), #82 - (`Size` type data), #90 (trailing space). +- 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. +- 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 4814538..718cb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ The format is based on that had all of them. To get the old result, filter with `Where-Object`, as the cmdlet page shows ([#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 + another module had added a `Size` member; use `LengthOnDisk` + ([#82](https://github.com/raandree/NTFSSecurity/issues/82)) ### Deprecated diff --git a/Docs/Concepts.md b/Docs/Concepts.md index 1852861..9ba01cf 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -234,7 +234,7 @@ the AlphaFS objects that the `*-Item2` cmdlets return. | --- | --- | --- | --- | | `Owner` | Property | Files and folders | The owner of the item. | | `IsInheritanceBlocked` | Property | Files and folders | `$true` if the item does not inherit access entries. | -| `LengthOnDisk` | Property | Files | The file size rounded up to whole clusters of the volume. `Size` is an alias. | +| `LengthOnDisk` | Property | Files | The file size rounded up to whole clusters of the volume. Before 5.0.0, `Size` was an alias. | | `EnableInheritance()` | Method | Files and folders | Turns on access inheritance. | | `DisableInheritance()` | Method | Files and folders | Turns off access inheritance. Pass `$false` to drop the inherited entries instead of copying them. | | `GetHash()` | Method | Files | Returns the SHA1 hash of the file as a hexadecimal string. | diff --git a/NTFSSecurity/NTFSSecurity.types.ps1xml b/NTFSSecurity/NTFSSecurity.types.ps1xml index 4944423..681e8fc 100644 --- a/NTFSSecurity/NTFSSecurity.types.ps1xml +++ b/NTFSSecurity/NTFSSecurity.types.ps1xml @@ -23,10 +23,6 @@ [Math]::Ceiling($this.Length / ($driveInfo.BytesPerSector * $driveInfo.SectorsPerCluster)) * ($driveInfo.BytesPerSector * $driveInfo.SectorsPerCluster) - - Size - LengthOnDisk - EnableInheritance