diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf3063e..fa997d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -123,6 +123,13 @@ The format is based on
after it was read or a folder above it that the user cannot read: the text
read `unknown paren`, and the explicit entries showed it as well. An
inherited entry now shows `unknown parent`, and an explicit entry no source
+- Fix `Remove-Item2`, `Copy-Item2`, `Move-Item2`, `Set-NTFSOwner`,
+ `Set-NTFSSecurityDescriptor`, `Get-NTFSSecurityDescriptor`,
+ `Get-NTFSSimpleAccess`, `Get-DiskSpace`, and `Get-ChildItem2` below the
+ first folder, which went on with the next item when a later command ended
+ the pipeline: a `break` or `continue` or `Select-Object -First` became an
+ error of the item, so that `Remove-Item2 -PassThru | Select-Object -First 1`
+ removed every item. They now stop and write no error
- Fix `Get-Help`, which showed only the syntax: ship the help file
`en-US\NTFSSecurity.dll-Help.xml` generated from the cmdlet documentation,
including the links that `Get-Help -Online` opens, instead of the outdated
diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs
index 07e7040..9258d5a 100644
--- a/NTFSSecurity/BaseCmdlets.cs
+++ b/NTFSSecurity/BaseCmdlets.cs
@@ -8,6 +8,37 @@ using System.Collections;
namespace NTFSSecurity
{
+ ///
+ /// Recognizes what a later command in the pipeline raises to end the pipeline or the loop around it: the end of the
+ /// pipeline, for example for Select-Object -First, and a break or continue in a script block. These exceptions pass
+ /// through a cmdlet while it writes an object. A catch for the failures of an item must pass them on: reported as
+ /// the error of that item, they would end nothing, and the cmdlet would go on with the next item.
+ ///
+ internal static class PipelineControl
+ {
+ ///
+ /// Whether the exception ends the pipeline or the loop around it. PowerShell doesn't make the exceptions of break
+ /// and continue public, so they are recognized by the name of their base type.
+ ///
+ internal static bool IsEnd(Exception exception)
+ {
+ if (exception is PipelineStoppedException)
+ {
+ return true;
+ }
+
+ for (var type = exception.GetType(); type != null; type = type.BaseType)
+ {
+ if (type.FullName == "System.Management.Automation.FlowControlException")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
public class BaseCmdlet : PSCmdlet
{
protected List paths = new List();
diff --git a/NTFSSecurity/ItemCmdlets/CopyItem2.cs b/NTFSSecurity/ItemCmdlets/CopyItem2.cs
index 064f888..3541113 100644
--- a/NTFSSecurity/ItemCmdlets/CopyItem2.cs
+++ b/NTFSSecurity/ItemCmdlets/CopyItem2.cs
@@ -149,6 +149,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "CopyError", ErrorCategory.NotSpecified, resolvedPath));
}
}
diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
index 49d915a..ac8a3da 100644
--- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
+++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs
@@ -244,8 +244,15 @@ namespace NTFSSecurity
{
throw ex;
}
- catch (Exception)
+ catch (Exception ex)
{
+ // Not what a later command raises to end the pipeline or the loop around it, which this catch
+ // would hide; the verbose message is for a folder that can't be listed.
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteVerbose(string.Format("Cannot access folder '{0}' for recursive operation", di));
}
}
@@ -260,11 +267,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
- //System.Management.Automation.BreakException or System.Management.Automation.ContinueException cannot be caught due to its protection level in PowerShell v2
- if (ex.GetType().FullName == "System.Management.Automation.BreakException" | ex.GetType().FullName == "System.Management.Automation.ContinueException")
+ if (PipelineControl.IsEnd(ex))
{
- throw ex;
+ throw;
}
+
WriteError(new ErrorRecord(ex, "DirUnspecifiedError", ErrorCategory.NotSpecified, di.FullName));
}
}
diff --git a/NTFSSecurity/ItemCmdlets/GetDiskSpace.cs b/NTFSSecurity/ItemCmdlets/GetDiskSpace.cs
index c5af08d..7a66305 100644
--- a/NTFSSecurity/ItemCmdlets/GetDiskSpace.cs
+++ b/NTFSSecurity/ItemCmdlets/GetDiskSpace.cs
@@ -1,4 +1,5 @@
using Alphaleonis.Win32.Filesystem;
+using System;
using System.Linq;
using System.Management.Automation;
@@ -43,8 +44,13 @@ namespace NTFSSecurity
this.WriteObject(diskSpaceInfo);
}
}
- catch
+ catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
this.WriteWarning(string.Format("Could not get drive details for '{0}'", letter));
}
}
diff --git a/NTFSSecurity/ItemCmdlets/MoveItem2.cs b/NTFSSecurity/ItemCmdlets/MoveItem2.cs
index fe7a61a..8c8a112 100644
--- a/NTFSSecurity/ItemCmdlets/MoveItem2.cs
+++ b/NTFSSecurity/ItemCmdlets/MoveItem2.cs
@@ -160,6 +160,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "MoveError", ErrorCategory.NotSpecified, resolvedPath));
}
}
diff --git a/NTFSSecurity/ItemCmdlets/RemoveItem2.cs b/NTFSSecurity/ItemCmdlets/RemoveItem2.cs
index 59db095..788b4fb 100644
--- a/NTFSSecurity/ItemCmdlets/RemoveItem2.cs
+++ b/NTFSSecurity/ItemCmdlets/RemoveItem2.cs
@@ -102,6 +102,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "DeleteError", ErrorCategory.NotSpecified, path));
}
}
diff --git a/NTFSSecurity/OwnerCmdlets/SetOwner.cs b/NTFSSecurity/OwnerCmdlets/SetOwner.cs
index 705e250..cd69449 100644
--- a/NTFSSecurity/OwnerCmdlets/SetOwner.cs
+++ b/NTFSSecurity/OwnerCmdlets/SetOwner.cs
@@ -87,6 +87,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "SetOwnerError", ErrorCategory.WriteError, path));
continue;
}
diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs
index 4d2f634..270e37e 100644
--- a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs
+++ b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs
@@ -64,12 +64,22 @@ namespace NTFSSecurity
}
catch (Exception ex2)
{
+ if (PipelineControl.IsEnd(ex2))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path));
continue;
}
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path));
}
}
diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs
index b3a6100..ef4c70d 100644
--- a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs
+++ b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs
@@ -81,6 +81,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.ReadError, sd.Item));
}
}
diff --git a/NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs b/NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
index 0da312e..61fd4eb 100644
--- a/NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
+++ b/NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
@@ -111,6 +111,11 @@ namespace NTFSSecurity
}
catch (Exception ex)
{
+ if (PipelineControl.IsEnd(ex))
+ {
+ throw;
+ }
+
WriteError(new ErrorRecord(ex, "ReadError", ErrorCategory.OpenError, p));
}
}
diff --git a/Tests/PipelineControl.Tests.ps1 b/Tests/PipelineControl.Tests.ps1
new file mode 100644
index 0000000..b69d9af
--- /dev/null
+++ b/Tests/PipelineControl.Tests.ps1
@@ -0,0 +1,333 @@
+<#
+ Tests how the cmdlets of the module built in NTFSSecurity\bin\Release behave when a later command in the pipeline
+ ends it: a break or continue in a script block, or Select-Object -First. The exception that carries it passes through
+ the cmdlet while it writes an object. A catch for the failures of an item must not report it as an error of that item
+ and go on with the next one: a cmdlet that removes, copies, moves, or changes items would change them all, although
+ the caller ended the pipeline. Every test works on files and folders in a sandbox.
+#>
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
+ 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
+)]
+param ()
+
+BeforeDiscovery {
+ Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
+ $canReadAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
+
+ $names = @(
+ 'Get-ChildItem2', 'Get-DiskSpace', 'Get-FileHash2', 'Get-Item2', 'Get-NTFSAccess', 'Get-NTFSEffectiveAccess',
+ 'Get-NTFSHardLink', 'Get-NTFSInheritance', 'Get-NTFSOrphanedAccess', 'Get-NTFSOwner', 'Get-NTFSSecurityDescriptor',
+ 'Get-NTFSSimpleAccess', 'Get-Privileges', 'Test-Path2', 'Add-NTFSAccess', 'Remove-NTFSAccess',
+ 'Disable-NTFSAccessInheritance', 'Enable-NTFSAccessInheritance', 'Set-NTFSInheritance', 'Set-NTFSOwner',
+ 'Set-NTFSSecurityDescriptor', 'Copy-Item2', 'Move-Item2', 'Remove-Item2'
+ )
+ $auditNames = @(
+ 'Get-NTFSAudit', 'Get-NTFSOrphanedAudit', 'Add-NTFSAudit', 'Remove-NTFSAudit', 'Disable-NTFSAuditInheritance',
+ 'Enable-NTFSAuditInheritance'
+ )
+ $loopCases = foreach ($name in $names) {
+ foreach ($keyword in 'break', 'continue') {
+ @{ Name = $name; Keyword = $keyword }
+ }
+ }
+ $stopCases = foreach ($name in $names) {
+ @{ Name = $name }
+ }
+ $auditLoopCases = foreach ($name in $auditNames) {
+ foreach ($keyword in 'break', 'continue') {
+ @{ Name = $name; Keyword = $keyword }
+ }
+ }
+ $auditStopCases = foreach ($name in $auditNames) {
+ @{ Name = $name }
+ }
+}
+
+BeforeAll {
+ Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
+ $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
+ Import-Module -Name $modulePath -Force -ErrorAction Stop
+ $sandbox = New-TestSandbox -Name 'PipelineControl'
+ Push-Location -LiteralPath $sandbox
+
+ $sidType = [System.Security.Principal.SecurityIdentifier]
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
+ $orphan = 'S-1-5-21-1-2-3-1001'
+
+ function New-Pair {
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
+ 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the sandbox.'
+ )]
+ param ([switch] $Directory)
+
+ @{
+ First = New-TestSandboxItem -Sandbox $sandbox -Name 'First' -Directory:$Directory
+ Second = New-TestSandboxItem -Sandbox $sandbox -Name 'Second' -Directory:$Directory
+ }
+ }
+
+ function Test-ExplicitEntry {
+ param ([string] $Path, [string] $Account)
+
+ @((Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, $sidType) |
+ Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account }).Count -gt 0
+ }
+
+ # Each case runs one command over the two items of its context. Untouched tells whether the second item is as it
+ # was, which it is only when the command stopped after the first one.
+ $cases = @{
+ 'Get-ChildItem2' = @{
+ # The first file is two levels below the folder, so the exception passes the frames of the recursion.
+ Prepare = {
+ $top = New-TestSandboxItem -Sandbox $sandbox -Name 'Tree' -Directory
+ foreach ($relative in 'A\B\Two.txt', 'C\Three.txt') {
+ $file = Join-Path -Path $top -ChildPath $relative
+ Assert-TestSandboxPath -Sandbox $sandbox -Path $file
+ New-Item -ItemType Directory -Path (Split-Path -Path $file -Parent) -Force | Out-Null
+ Set-Content -LiteralPath $file -Value 'Tree'
+ }
+ @{ Top = $top }
+ }
+ Run = { param ($Context) Get-ChildItem2 -Path $Context.Top -Recurse -File -ErrorAction SilentlyContinue }
+ }
+ 'Get-DiskSpace' = @{
+ Prepare = { @{} }
+ Run = { Get-DiskSpace -ErrorAction SilentlyContinue }
+ }
+ 'Get-FileHash2' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-FileHash2 -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-Item2' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-Item2 -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSAccess' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSAccess -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSEffectiveAccess' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSEffectiveAccess -Path $Context.First, $Context.Second -WarningAction SilentlyContinue -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSHardLink' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSHardLink -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSInheritance' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSInheritance -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSOrphanedAccess' = @{
+ Prepare = {
+ $context = New-Pair
+ Add-NTFSAccess -Path $context.First, $context.Second -Account $orphan -AccessRights ReadData -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Get-NTFSOrphanedAccess -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSOwner' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSOwner -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSSecurityDescriptor' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Get-NTFSSecurityDescriptor -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSSimpleAccess' = @{
+ Prepare = { New-Pair -Directory }
+ Run = { param ($Context) Get-NTFSSimpleAccess -Path $Context.First, $Context.Second -IncludeRootFolder:$false -ErrorAction SilentlyContinue }
+ }
+ 'Get-Privileges' = @{
+ Prepare = { @{} }
+ Run = { Get-Privileges -ErrorAction SilentlyContinue }
+ }
+ 'Test-Path2' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Test-Path2 -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Add-NTFSAccess' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Add-NTFSAccess -Path $Context.First, $Context.Second -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Test-ExplicitEntry -Path $Context.Second -Account 'S-1-1-0') }
+ }
+ 'Remove-NTFSAccess' = @{
+ Prepare = {
+ $context = New-Pair
+ Add-NTFSAccess -Path $context.First, $context.Second -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Remove-NTFSAccess -Path $Context.First, $Context.Second -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) Test-ExplicitEntry -Path $Context.Second -Account 'S-1-1-0' }
+ }
+ 'Disable-NTFSAccessInheritance' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Disable-NTFSAccessInheritance -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Get-Acl -LiteralPath $Context.Second).AreAccessRulesProtected }
+ }
+ 'Enable-NTFSAccessInheritance' = @{
+ Prepare = {
+ $context = New-Pair
+ Disable-NTFSAccessInheritance -Path $context.First, $context.Second -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Enable-NTFSAccessInheritance -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) (Get-Acl -LiteralPath $Context.Second).AreAccessRulesProtected }
+ }
+ 'Set-NTFSInheritance' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Set-NTFSInheritance -Path $Context.First, $Context.Second -AccessInheritanceEnabled $false -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Get-Acl -LiteralPath $Context.Second).AreAccessRulesProtected }
+ }
+ 'Set-NTFSOwner' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $currentUser -PassThru -ErrorAction SilentlyContinue }
+ }
+ 'Set-NTFSSecurityDescriptor' = @{
+ Prepare = {
+ $context = New-Pair
+ $context.Descriptors = @(Get-NTFSSecurityDescriptor -Path $context.First, $context.Second -ErrorAction Stop)
+ Add-NTFSAccess -SecurityDescriptor $context.Descriptors -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Test-ExplicitEntry -Path $Context.Second -Account 'S-1-1-0') }
+ }
+ 'Copy-Item2' = @{
+ Prepare = {
+ $context = New-Pair
+ $context.Destination = New-TestSandboxItem -Sandbox $sandbox -Name 'CopyTo' -Directory
+ $context
+ }
+ Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath (Split-Path -Path $Context.Second -Leaf))) }
+ }
+ 'Move-Item2' = @{
+ Prepare = {
+ $context = New-Pair
+ $context.Destination = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveTo' -Directory
+ $context
+ }
+ Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
+ }
+ 'Remove-Item2' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
+ }
+ 'Get-NTFSAudit' = @{
+ Prepare = {
+ $context = New-Pair
+ Add-NTFSAudit -Path $context.First, $context.Second -Account 'S-1-1-0' -AccessRights Delete -AuditFlags Success -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Get-NTFSAudit -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Get-NTFSOrphanedAudit' = @{
+ Prepare = {
+ $context = New-Pair
+ Add-NTFSAudit -Path $context.First, $context.Second -Account $orphan -AccessRights Delete -AuditFlags Success -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Get-NTFSOrphanedAudit -Path $Context.First, $Context.Second -ErrorAction SilentlyContinue }
+ }
+ 'Add-NTFSAudit' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Add-NTFSAudit -Path $Context.First, $Context.Second -Account 'S-1-1-0' -AccessRights Delete -AuditFlags Success -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) @(Get-NTFSAudit -Path $Context.Second -ErrorAction Stop).Count -eq 0 }
+ }
+ 'Remove-NTFSAudit' = @{
+ # The entry of the orphan goes; the one of Everyone stays, so that there is an object to write.
+ Prepare = {
+ $context = New-Pair
+ foreach ($account in $orphan, 'S-1-1-0') {
+ Add-NTFSAudit -Path $context.First, $context.Second -Account $account -AccessRights Delete -AuditFlags Success -ErrorAction Stop
+ }
+ $context
+ }
+ Run = { param ($Context) Remove-NTFSAudit -Path $Context.First, $Context.Second -Account $orphan -AccessRights Delete -AuditFlags Success -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) @(Get-NTFSAudit -Path $Context.Second -Account $orphan -ErrorAction Stop).Count -gt 0 }
+ }
+ 'Disable-NTFSAuditInheritance' = @{
+ Prepare = { New-Pair }
+ Run = { param ($Context) Disable-NTFSAuditInheritance -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) (Get-NTFSInheritance -Path $Context.Second -ErrorAction Stop).AuditInheritanceEnabled }
+ }
+ 'Enable-NTFSAuditInheritance' = @{
+ Prepare = {
+ $context = New-Pair
+ Disable-NTFSAuditInheritance -Path $context.First, $context.Second -ErrorAction Stop
+ $context
+ }
+ Run = { param ($Context) Enable-NTFSAuditInheritance -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
+ Untouched = { param ($Context) -not (Get-NTFSInheritance -Path $Context.Second -ErrorAction Stop).AuditInheritanceEnabled }
+ }
+ }
+
+ # The command writes its first object, and the break or continue of the later command ends the loop around the
+ # pipeline before the next statement of the loop runs.
+ function Assert-LoopControl {
+ param ([string] $Name, [string] $Keyword)
+
+ $case = $cases[$Name]
+ $context = & $case.Prepare
+ $emitted = 0
+ $reachedEnd = $false
+ $Error.Clear()
+ foreach ($round in 1) {
+ & $case.Run $context | ForEach-Object -Process {
+ $emitted++
+ if ($Keyword -eq 'break') { break } else { continue }
+ }
+ $reachedEnd = $true
+ }
+
+ $emitted | Should -Be 1
+ $reachedEnd | Should -BeFalse
+ $Error.Count | Should -Be 0
+ if ($case.Untouched) {
+ (& $case.Untouched $context) | Should -BeTrue
+ }
+ }
+
+ function Assert-PipelineStop {
+ param ([string] $Name)
+
+ $case = $cases[$Name]
+ $context = & $case.Prepare
+ $Error.Clear()
+
+ $result = @(& $case.Run $context | Select-Object -First 1)
+
+ $result | Should -HaveCount 1
+ $Error.Count | Should -Be 0
+ if ($case.Untouched) {
+ (& $case.Untouched $context) | Should -BeTrue
+ }
+ }
+}
+
+AfterAll {
+ Pop-Location
+ Remove-TestSandbox -Sandbox $sandbox
+ Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
+}
+
+Describe 'A later command that ends the pipeline' {
+ It ' should leave the loop for after its first object and change nothing else' -ForEach $loopCases {
+ Assert-LoopControl -Name $Name -Keyword $Keyword
+ }
+
+ It ' should stop after the first object for Select-Object -First 1 and change nothing else' -ForEach $stopCases {
+ Assert-PipelineStop -Name $Name
+ }
+
+ It ' should leave the loop for after its first object and change nothing else' -Skip:(-not $canReadAudit) -ForEach $auditLoopCases {
+ Assert-LoopControl -Name $Name -Keyword $Keyword
+ }
+
+ It ' should stop after the first object for Select-Object -First 1 and change nothing else' -Skip:(-not $canReadAudit) -ForEach $auditStopCases {
+ Assert-PipelineStop -Name $Name
+ }
+}