The -Stream parameter now works with directories (#13941)

This commit is contained in:
Kyle H
2020-12-07 17:31:55 -08:00
committed by GitHub
parent 86ca45681d
commit 909ca89be9
7 changed files with 253 additions and 79 deletions
@@ -1311,35 +1311,36 @@ namespace Microsoft.PowerShell.Commands
// If we want to retrieve the file streams, retrieve them.
if (retrieveStreams)
{
if (!isContainer)
foreach (string desiredStream in dynamicParameters.Stream)
{
foreach (string desiredStream in dynamicParameters.Stream)
// See that it matches the name specified
WildcardPattern p = WildcardPattern.Get(desiredStream, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
bool foundStream = false;
foreach (AlternateStreamData stream in AlternateDataStreamUtilities.GetStreams(result.FullName))
{
// See that it matches the name specified
WildcardPattern p = WildcardPattern.Get(desiredStream, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
bool foundStream = false;
foreach (AlternateStreamData stream in AlternateDataStreamUtilities.GetStreams(result.FullName))
if (!p.IsMatch(stream.Stream))
{
if (!p.IsMatch(stream.Stream)) { continue; }
string outputPath = result.FullName + ":" + stream.Stream;
WriteItemObject(stream, outputPath, isContainer);
foundStream = true;
continue;
}
if ((!WildcardPattern.ContainsWildcardCharacters(desiredStream)) && (!foundStream))
{
string errorMessage = StringUtil.Format(
FileSystemProviderStrings.AlternateDataStreamNotFound, desiredStream, result.FullName);
Exception e = new FileNotFoundException(errorMessage, result.FullName);
string outputPath = result.FullName + ":" + stream.Stream;
// Alternate data streams can never be containers.
WriteItemObject(stream, outputPath, isContainer: false);
foundStream = true;
}
WriteError(new ErrorRecord(
e,
"AlternateDataStreamNotFound",
ErrorCategory.ObjectNotFound,
path));
}
if ((!WildcardPattern.ContainsWildcardCharacters(desiredStream)) && (!foundStream))
{
string errorMessage = StringUtil.Format(
FileSystemProviderStrings.AlternateDataStreamNotFound, desiredStream, result.FullName);
Exception e = new FileNotFoundException(errorMessage, result.FullName);
WriteError(new ErrorRecord(
e,
"AlternateDataStreamNotFound",
ErrorCategory.ObjectNotFound,
path));
}
}
}
@@ -6663,7 +6664,14 @@ namespace Microsoft.PowerShell.Commands
try
{
if (Directory.Exists(path))
// Get-Content will write a non-terminating error if the target is a directory.
// On Windows, the streamName must be null or empty for it to write the error. Otherwise, the
// alternate data stream is not a directory, even if it's set on a directory.
if (Directory.Exists(path)
#if !UNIX
&& string.IsNullOrEmpty(streamName)
#endif
)
{
string errMsg = StringUtil.Format(SessionStateStrings.GetContainerContentException, path);
ErrorRecord error = new ErrorRecord(new InvalidOperationException(errMsg), "GetContainerContentException", ErrorCategory.InvalidOperation, null);
@@ -6818,7 +6826,14 @@ namespace Microsoft.PowerShell.Commands
try
{
if (Directory.Exists(path))
// Add-Content and Set-Content will write a non-terminating error if the target is a directory.
// On Windows, the streamName must be null or empty for it to write the error. Otherwise, the
// alternate data stream is not a directory, even if it's set on a directory.
if (Directory.Exists(path)
#if !UNIX
&& string.IsNullOrEmpty(streamName)
#endif
)
{
string errMsg = StringUtil.Format(SessionStateStrings.WriteContainerContentException, path);
ErrorRecord error = new ErrorRecord(new InvalidOperationException(errMsg), "WriteContainerContentException", ErrorCategory.InvalidOperation, null);
@@ -6896,13 +6911,6 @@ namespace Microsoft.PowerShell.Commands
path = NormalizePath(path);
if (Directory.Exists(path))
{
string errorMsg = StringUtil.Format(SessionStateStrings.ClearDirectoryContent, path);
WriteError(new ErrorRecord(new NotSupportedException(errorMsg), "ClearDirectoryContent", ErrorCategory.InvalidOperation, path));
return;
}
try
{
#if !UNIX
@@ -6954,6 +6962,26 @@ namespace Microsoft.PowerShell.Commands
clearStream = false;
}
#endif
// On Windows, determine if our argument is a directory only after we determine if
// we're being asked to work with an alternate data stream, because directories can have
// alternate data streams on them that are not child items. These alternate data streams
// must be treated as data streams, even if they're attached to directories. However,
// if asked to work with a directory without a data stream specified, write a non-terminating
// error instead of clearing all child items of the directory. (On non-Windows, alternate
// data streams don't exist, so in that environment always write the error when addressing
// a directory.)
if (Directory.Exists(path)
#if !UNIX
&& !clearStream
#endif
)
{
string errorMsg = StringUtil.Format(SessionStateStrings.ClearDirectoryContent, path);
WriteError(new ErrorRecord(new NotSupportedException(errorMsg), "ClearDirectoryContent", ErrorCategory.InvalidOperation, path));
return;
}
#if !UNIX
if (clearStream)
{
FileStream fileStream = null;
@@ -8625,7 +8653,22 @@ namespace System.Management.Automation.Internal
SafeFindHandle handle = NativeMethods.FindFirstStreamW(
path, NativeMethods.StreamInfoLevels.FindStreamInfoStandard,
findStreamData, 0);
if (handle.IsInvalid) throw new Win32Exception();
if (handle.IsInvalid)
{
int error = Marshal.GetLastWin32Error();
// Directories don't normally have alternate streams, so this is not an exceptional state.
// If a directory has no alternate data streams, FindFirstStreamW returns ERROR_HANDLE_EOF.
if (error == NativeMethods.ERROR_HANDLE_EOF)
{
return alternateStreams;
}
// An unexpected error was returned, that we don't know how to interpret. The most helpful
// thing we can do at this point is simply throw the raw Win32 exception.
throw new Win32Exception(error);
}
try
{
@@ -8760,6 +8803,7 @@ namespace System.Management.Automation.Internal
internal static class NativeMethods
{
internal const int ERROR_HANDLE_EOF = 38;
internal const int ERROR_INVALID_PARAMETER = 87;
internal enum StreamInfoLevels { FindStreamInfoStandard = 0 }
@@ -5,6 +5,7 @@ Describe "Add-Content cmdlet tests" -Tags "CI" {
BeforeAll {
$file1 = "file1.txt"
Setup -File "$file1"
$streamContent = "ShouldWork"
}
Context "Add-Content should actually add content" {
@@ -47,6 +48,29 @@ Describe "Add-Content cmdlet tests" -Tags "CI" {
{ Add-Content -Path . -Value "WriteContainerContentException" -ErrorAction Stop } | Should -Throw -ErrorId "WriteContainerContentException,Microsoft.PowerShell.Commands.AddContentCommand"
}
Context "Add-Content should work with alternate data streams on Windows" {
BeforeAll {
if (!$isWindows) {
return
}
$ADSTestDir = "addcontentadstest"
$ADSTestFile = "addcontentads.txt"
$streamContent = "This is a test stream."
Setup -Directory "$ADSTestDir"
Setup -File "$ADSTestFile"
}
It "Should add an alternate data stream on a directory" -Skip:(!$IsWindows) {
Add-Content -Path TestDrive:\$ADSTestDir -Stream Add-Content-Test-Stream -Value $streamContent -ErrorAction Stop
Get-Content -Path TestDrive:\$ADSTestDir -Stream Add-Content-Test-Stream | Should -BeExactly $streamContent
}
It "Should add an alternate data stream on a file" -Skip:(!$IsWindows) {
Add-Content -Path TestDrive:\$ADSTestFile -Stream Add-Content-Test-Stream -Value $streamContent -ErrorAction Stop
Get-Content -Path TestDrive:\$ADSTestFile -Stream Add-Content-Test-Stream | Should -BeExactly $streamContent
}
}
#[BugId(BugDatabase.WindowsOutOfBandReleases, 906022)]
It "should throw 'NotSupportedException' when you add-content to an unsupported provider" -Skip:($IsLinux -Or $IsMacOS) {
{ Add-Content -Path HKLM:\\software\\microsoft -Value "ShouldNotWorkBecausePathIsUnsupported" -ErrorAction Stop } | Should -Throw -ErrorId "NotSupported,Microsoft.PowerShell.Commands.AddContentCommand"
@@ -51,6 +51,8 @@ Describe "Clear-Content cmdlet tests" -Tags "CI" {
Setup -File "$file3" -Content $content2
$streamContent = "content for alternate stream"
$streamName = "altStream1"
$dirName = "clearcontent"
Setup -Directory "$dirName"
}
Context "Clear-Content should actually clear content" {
@@ -75,32 +77,50 @@ Describe "Clear-Content cmdlet tests" -Tags "CI" {
$cci.SupportsShouldProcess | Should -BeTrue
}
It "Alternate streams should be cleared with clear-content" -Skip:(!$IsWindows) {
# make sure that the content is correct
# this is here rather than BeforeAll because only windows can write to an alternate stream
Set-Content -Path "TestDrive:/$file3" -Stream $streamName -Value $streamContent
Get-Content -Path "TestDrive:/$file3" | Should -BeExactly $content2
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeExactly $streamContent
Clear-Content -Path "TestDrive:/$file3" -Stream $streamName
Get-Content -Path "TestDrive:/$file3" | Should -BeExactly $content2
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeNullOrEmpty
}
Context "Clear-Content should work with alternate data streams on Windows" {
It "Alternate streams should be cleared with Clear-Content on a file" -Skip:(!$IsWindows) {
It "the '-Stream' dynamic parameter is visible to get-command in the filesystem" -Skip:(!$IsWindows) {
try {
Push-Location -Path TestDrive:
(Get-Command Clear-Content -Stream foo).parameters.keys -eq "stream" | Should -Be "stream"
}
finally {
Pop-Location
}
}
Set-Content -Path "TestDrive:/$file3" -Stream $streamName -Value $streamContent
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeExactly $streamContent
It "the '-Stream' dynamic parameter should not be visible to get-command in the function provider" {
Push-Location -Path function:
{ Get-Command Clear-Content -Stream $streamName } |
Should -Throw -ErrorId "NamedParameterNotFound,Microsoft.PowerShell.Commands.GetCommandCommand"
Pop-Location
Clear-Content -Path "TestDrive:/$file3" -Stream $streamName -ErrorAction Stop
$result = Get-Item -Path "TestDrive:/$file3" -Stream $streamName
$result | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
$result.length | Should -Be 0
}
It "Alternate streams should be cleared with Clear-Content on a directory" -Skip:(!$IsWindows) {
Set-Content -Path "TestDrive:/$dirName" -Stream $streamName -Value $streamContent
Get-Content -Path "TestDrive:/$dirName" -Stream $streamName | Should -BeExactly $streamContent
Clear-Content -Path "TestDrive:/$dirName" -Stream $streamName -ErrorAction Stop
$result = Get-Item -Path "TestDrive:/$dirName" -Stream $streamName
$result | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
$result.length | Should -Be 0
}
It "the '-Stream' dynamic parameter is visible to get-command in the filesystem" -Skip:(!$IsWindows) {
try {
Push-Location -Path TestDrive:
(Get-Command Clear-Content -Stream foo).parameters.keys -eq "Stream" | Should -BeExactly "Stream"
}
finally {
Pop-Location
}
}
It "the '-Stream' dynamic parameter should not be visible to get-command in the function provider" -Skip:(!$IsWindows) {
try {
Push-Location -Path function:
{ Get-Command Clear-Content -Stream $streamName } |
Should -Throw -ErrorId "NamedParameterNotFound,Microsoft.PowerShell.Commands.GetCommandCommand"
}
finally {
Pop-Location
}
}
}
}
@@ -221,25 +221,27 @@ Describe "Get-Content" -Tags "CI" {
$expected = 'He', 'o,', '', 'Wor', "d${nl}He", 'o2,', '', 'Wor', "d2${nl}"
for ($i = 0; $i -lt $result.Length ; $i++) { $result[$i] | Should -BeExactly $expected[$i]}
}
Context "Alternate Data Stream support on Windows" {
It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) {
Set-Content "${testPath}:Stream" -Value "Foo"
{ Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" }
Get-Content "${testPath}:Stream" | Should -BeExactly "Foo"
Get-Content $testPath | Should -BeExactly $testString
}
It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) {
Set-Content "${testPath}:Stream" -Value "Foo"
{ Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" }
Get-Content "${testPath}:Stream" | Should -BeExactly "Foo"
Get-Content $testPath | Should -BeExactly $testString
}
It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) {
Set-Content -Path $testPath -Stream hello -Value World
Get-Content -Path $testPath | Should -BeExactly $testString
Get-Content -Path $testPath -Stream hello | Should -BeExactly "World"
$item = Get-Item -Path $testPath -Stream hello
$item | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
$item.Stream | Should -BeExactly "hello"
Clear-Content -Path $testPath -Stream hello
Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty
Remove-Item -Path $testPath -Stream hello
{ Get-Content -Path $testPath -Stream hello | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" }
It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) {
Set-Content -Path $testPath -Stream hello -Value World
Get-Content -Path $testPath | Should -BeExactly $testString
Get-Content -Path $testPath -Stream hello | Should -BeExactly "World"
$item = Get-Item -Path $testPath -Stream hello
$item | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
$item.Stream | Should -BeExactly "hello"
Clear-Content -Path $testPath -Stream hello
Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty
Remove-Item -Path $testPath -Stream hello
{ Get-Content -Path $testPath -Stream hello -ErrorAction stop} | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand"
}
}
It "Should support colons in filename on Linux/Mac" -Skip:($IsWindows) {
@@ -112,16 +112,38 @@ Describe "Get-Item" -Tags "CI" {
return
}
$altStreamPath = "$TESTDRIVE/altStream.txt"
$altStreamDirectory = "$TESTDRIVE/altstreamdir"
$noAltStreamDirectory = "$TESTDRIVE/noaltstreamdir"
$stringData = "test data"
$streamName = "test"
$item = New-Item -type file $altStreamPath
$absentStreamName = "noExist"
$null = New-Item -type file $altStreamPath
Set-Content -Path $altStreamPath -Stream $streamName -Value $stringData
$null = New-Item -type directory $altStreamDirectory
Set-Content -Path $altStreamDirectory -Stream $streamName -Value $stringData
$null = New-Item -type directory $noAltStreamDirectory
}
It "Should find an alternate stream if present" -Skip:$skipNotWindows {
It "Should find an alternate stream on a file if present" -Skip:$skipNotWindows {
$result = Get-Item $altStreamPath -Stream $streamName
$result.Length | Should -Be ($stringData.Length + [Environment]::NewLine.Length)
$result.Stream | Should -Be $streamName
}
It "Should error if it cannot find alternate stream on an existing file" -Skip:$skipNotWindows {
{ Get-Item $altStreamPath -Stream $absentStreamName -ErrorAction Stop } | Should -Throw -ErrorId "AlternateDataStreamNotFound,Microsoft.PowerShell.Commands.GetItemCommand"
}
It "Should find an alternate stream on a directory if present, and it should not be a container" -Skip:$skipNotWindows {
$result = Get-Item $altStreamDirectory -Stream $streamName
$result.Length | Should -Be ($stringData.Length + [Environment]::NewLine.Length )
$result.Stream | Should -Be $streamName
$result.PSIsContainer | Should -BeExactly $false
}
It "Should not find an alternate stream on a directory if not present" -Skip:$skipNotWindows {
{ Get-Item $noAltStreamDirectory -Stream $absentStreamName -ErrorAction Stop } | Should -Throw -ErrorId "AlternateDataStreamNotFound,Microsoft.PowerShell.Commands.GetItemCommand"
}
It "Should find zero alt streams and not fail on a directory with a wildcard stream name if no alt streams are present" -Skip:$skipNotWindows {
$result = Get-Item $noAltStreamDirectory -Stream * -ErrorAction Stop
$result | Should -BeExactly $null
}
}
Context "Registry Provider" {
@@ -7,9 +7,7 @@ Describe "Remove-Item" -Tags "CI" {
Context "File removal Tests" {
BeforeEach {
New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
Test-Path $testfilepath | Should -BeTrue
}
It "Should be able to be called on a regular file without error using the Path parameter" {
@@ -138,4 +136,35 @@ Describe "Remove-Item" -Tags "CI" {
Test-Path $testdirectory | Should -BeFalse
}
}
Context "Alternate Data Streams should be supported on Windows" {
BeforeAll {
if (!$IsWindows) {
return
}
$fileName = "ADStest.txt"
$streamName = "teststream"
$dirName = "ADStestdir"
$fileContent =" This is file content."
$streamContent = "datastream content here"
$streamfile = Join-Path -Path $testpath -ChildPath $fileName
$streamdir = Join-Path -Path $testpath -ChildPath $dirName
$null = New-Item -Path $streamfile -ItemType "File" -force
Add-Content -Path $streamfile -Value $fileContent
Add-Content -Path $streamfile -Stream $streamName -Value $streamContent
$null = New-Item -Path $streamdir -ItemType "Directory" -Force
Add-Content -Path $streamdir -Stream $streamName -Value $streamContent
}
It "Should completely remove a datastream from a file" -Skip:(!$IsWindows) {
Get-Item -Path $streamfile -Stream $streamName | Should -Not -BeNullOrEmpty
Remove-Item -Path $streamfile -Stream $streamName
Get-Item -Path $streamfile -Stream $streamName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
}
It "Should completely remove a datastream from a directory" -Skip:(!$IsWindows) {
Get-Item -Path $streamdir -Stream $streamName | Should -Not -BeNullOrEmpty
Remove-Item -Path $streamdir -Stream $streamName
Get-Item -Path $streamdir -Stream $streamname -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
}
}
}
@@ -72,6 +72,39 @@ Describe "Set-Content cmdlet tests" -Tags "CI" {
$result[1] | Should -BeExactly "world"
}
}
Context "Set-Content should work with alternate data streams on Windows" {
BeforeAll {
if ( -Not $IsWindows )
{
return
}
$altStreamPath = "$TESTDRIVE/altStream.txt"
$altStreamPath2 = "$TESTDRIVE/altStream2.txt"
$altStreamDirectory = "$TESTDRIVE/altstreamdir"
$altStreamDirectory2 = "$TESTDRIVE/altstream2dir"
$stringData = "test data"
$streamName = "test"
$absentStreamName = "noExist"
$item = New-Item -type file $altStreamPath
$altstreamdiritem = New-Item -type directory $altStreamDirectory
}
It "Should create a new data stream on a file" -Skip:(-Not $IsWindows) {
Set-Content -Path $altStreamPath -Stream $streamName -Value $stringData
Get-Content -Path $altStreamPath -Stream $streamName | Should -BeExactly $stringData
}
It "Should create a new data stream on a file using colon syntax" -Skip:(-Not $IsWindows) {
Set-Content -Path ${altStreamPath2}:${streamName} -Value $stringData
Get-Content -Path ${altStreamPath2} -Stream $streamName | Should -BeExactly $stringData
}
It "Should create a new data stream on a directory" -Skip:(-Not $IsWindows) {
Set-Content -Path $altStreamDirectory -Stream $streamName -Value $stringData
Get-Content -Path $altStreamDirectory -Stream $streamName | Should -BeExactly $stringData
}
It "Should create a new data stream on a directory using colon syntax" -Skip:(-Not $IsWindows) {
Set-Content -Path ${altStreamDirectory2}:${streamName} -Value $stringData
Get-Content -Path ${altStreamDirectory2} -Stream ${streamName} | Should -BeExactly $stringData
}
}
}
Describe "Set-Content should work for PSDrive with UNC path as root" -Tags @('CI', 'RequireAdminOnWindows') {