Skip to content

Commit 909ca89

Browse files
authored
The -Stream parameter now works with directories (#13941)
1 parent 86ca456 commit 909ca89

File tree

7 files changed

+252
-78
lines changed

7 files changed

+252
-78
lines changed

src/System.Management.Automation/namespaces/FileSystemProvider.cs

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,35 +1311,36 @@ protected override void GetItem(string path)
13111311
// If we want to retrieve the file streams, retrieve them.
13121312
if (retrieveStreams)
13131313
{
1314-
if (!isContainer)
1314+
foreach (string desiredStream in dynamicParameters.Stream)
13151315
{
1316-
foreach (string desiredStream in dynamicParameters.Stream)
1317-
{
1318-
// See that it matches the name specified
1319-
WildcardPattern p = WildcardPattern.Get(desiredStream, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
1320-
bool foundStream = false;
1316+
// See that it matches the name specified
1317+
WildcardPattern p = WildcardPattern.Get(desiredStream, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
1318+
bool foundStream = false;
13211319

1322-
foreach (AlternateStreamData stream in AlternateDataStreamUtilities.GetStreams(result.FullName))
1320+
foreach (AlternateStreamData stream in AlternateDataStreamUtilities.GetStreams(result.FullName))
1321+
{
1322+
if (!p.IsMatch(stream.Stream))
13231323
{
1324-
if (!p.IsMatch(stream.Stream)) { continue; }
1325-
1326-
string outputPath = result.FullName + ":" + stream.Stream;
1327-
WriteItemObject(stream, outputPath, isContainer);
1328-
foundStream = true;
1324+
continue;
13291325
}
13301326

1331-
if ((!WildcardPattern.ContainsWildcardCharacters(desiredStream)) && (!foundStream))
1332-
{
1333-
string errorMessage = StringUtil.Format(
1334-
FileSystemProviderStrings.AlternateDataStreamNotFound, desiredStream, result.FullName);
1335-
Exception e = new FileNotFoundException(errorMessage, result.FullName);
1336-
1337-
WriteError(new ErrorRecord(
1338-
e,
1339-
"AlternateDataStreamNotFound",
1340-
ErrorCategory.ObjectNotFound,
1341-
path));
1342-
}
1327+
string outputPath = result.FullName + ":" + stream.Stream;
1328+
// Alternate data streams can never be containers.
1329+
WriteItemObject(stream, outputPath, isContainer: false);
1330+
foundStream = true;
1331+
}
1332+
1333+
if ((!WildcardPattern.ContainsWildcardCharacters(desiredStream)) && (!foundStream))
1334+
{
1335+
string errorMessage = StringUtil.Format(
1336+
FileSystemProviderStrings.AlternateDataStreamNotFound, desiredStream, result.FullName);
1337+
Exception e = new FileNotFoundException(errorMessage, result.FullName);
1338+
1339+
WriteError(new ErrorRecord(
1340+
e,
1341+
"AlternateDataStreamNotFound",
1342+
ErrorCategory.ObjectNotFound,
1343+
path));
13431344
}
13441345
}
13451346
}
@@ -6663,7 +6664,14 @@ public IContentReader GetContentReader(string path)
66636664

66646665
try
66656666
{
6666-
if (Directory.Exists(path))
6667+
// Get-Content will write a non-terminating error if the target is a directory.
6668+
// On Windows, the streamName must be null or empty for it to write the error. Otherwise, the
6669+
// alternate data stream is not a directory, even if it's set on a directory.
6670+
if (Directory.Exists(path)
6671+
#if !UNIX
6672+
&& string.IsNullOrEmpty(streamName)
6673+
#endif
6674+
)
66676675
{
66686676
string errMsg = StringUtil.Format(SessionStateStrings.GetContainerContentException, path);
66696677
ErrorRecord error = new ErrorRecord(new InvalidOperationException(errMsg), "GetContainerContentException", ErrorCategory.InvalidOperation, null);
@@ -6818,7 +6826,14 @@ public IContentWriter GetContentWriter(string path)
68186826

68196827
try
68206828
{
6821-
if (Directory.Exists(path))
6829+
// Add-Content and Set-Content will write a non-terminating error if the target is a directory.
6830+
// On Windows, the streamName must be null or empty for it to write the error. Otherwise, the
6831+
// alternate data stream is not a directory, even if it's set on a directory.
6832+
if (Directory.Exists(path)
6833+
#if !UNIX
6834+
&& string.IsNullOrEmpty(streamName)
6835+
#endif
6836+
)
68226837
{
68236838
string errMsg = StringUtil.Format(SessionStateStrings.WriteContainerContentException, path);
68246839
ErrorRecord error = new ErrorRecord(new InvalidOperationException(errMsg), "WriteContainerContentException", ErrorCategory.InvalidOperation, null);
@@ -6896,13 +6911,6 @@ public void ClearContent(string path)
68966911

68976912
path = NormalizePath(path);
68986913

6899-
if (Directory.Exists(path))
6900-
{
6901-
string errorMsg = StringUtil.Format(SessionStateStrings.ClearDirectoryContent, path);
6902-
WriteError(new ErrorRecord(new NotSupportedException(errorMsg), "ClearDirectoryContent", ErrorCategory.InvalidOperation, path));
6903-
return;
6904-
}
6905-
69066914
try
69076915
{
69086916
#if !UNIX
@@ -6954,6 +6962,26 @@ public void ClearContent(string path)
69546962
clearStream = false;
69556963
}
69566964

6965+
#endif
6966+
// On Windows, determine if our argument is a directory only after we determine if
6967+
// we're being asked to work with an alternate data stream, because directories can have
6968+
// alternate data streams on them that are not child items. These alternate data streams
6969+
// must be treated as data streams, even if they're attached to directories. However,
6970+
// if asked to work with a directory without a data stream specified, write a non-terminating
6971+
// error instead of clearing all child items of the directory. (On non-Windows, alternate
6972+
// data streams don't exist, so in that environment always write the error when addressing
6973+
// a directory.)
6974+
if (Directory.Exists(path)
6975+
#if !UNIX
6976+
&& !clearStream
6977+
#endif
6978+
)
6979+
{
6980+
string errorMsg = StringUtil.Format(SessionStateStrings.ClearDirectoryContent, path);
6981+
WriteError(new ErrorRecord(new NotSupportedException(errorMsg), "ClearDirectoryContent", ErrorCategory.InvalidOperation, path));
6982+
return;
6983+
}
6984+
#if !UNIX
69576985
if (clearStream)
69586986
{
69596987
FileStream fileStream = null;
@@ -8625,7 +8653,22 @@ internal static List<AlternateStreamData> GetStreams(string path)
86258653
SafeFindHandle handle = NativeMethods.FindFirstStreamW(
86268654
path, NativeMethods.StreamInfoLevels.FindStreamInfoStandard,
86278655
findStreamData, 0);
8628-
if (handle.IsInvalid) throw new Win32Exception();
8656+
8657+
if (handle.IsInvalid)
8658+
{
8659+
int error = Marshal.GetLastWin32Error();
8660+
8661+
// Directories don't normally have alternate streams, so this is not an exceptional state.
8662+
// If a directory has no alternate data streams, FindFirstStreamW returns ERROR_HANDLE_EOF.
8663+
if (error == NativeMethods.ERROR_HANDLE_EOF)
8664+
{
8665+
return alternateStreams;
8666+
}
8667+
8668+
// An unexpected error was returned, that we don't know how to interpret. The most helpful
8669+
// thing we can do at this point is simply throw the raw Win32 exception.
8670+
throw new Win32Exception(error);
8671+
}
86298672

86308673
try
86318674
{
@@ -8760,6 +8803,7 @@ internal static void SetZoneOfOrigin(string path, SecurityZone securityZone)
87608803
internal static class NativeMethods
87618804
{
87628805
internal const int ERROR_HANDLE_EOF = 38;
8806+
internal const int ERROR_INVALID_PARAMETER = 87;
87638807

87648808
internal enum StreamInfoLevels { FindStreamInfoStandard = 0 }
87658809

test/powershell/Modules/Microsoft.PowerShell.Management/Add-Content.Tests.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Describe "Add-Content cmdlet tests" -Tags "CI" {
55
BeforeAll {
66
$file1 = "file1.txt"
77
Setup -File "$file1"
8+
$streamContent = "ShouldWork"
89
}
910

1011
Context "Add-Content should actually add content" {
@@ -47,6 +48,29 @@ Describe "Add-Content cmdlet tests" -Tags "CI" {
4748
{ Add-Content -Path . -Value "WriteContainerContentException" -ErrorAction Stop } | Should -Throw -ErrorId "WriteContainerContentException,Microsoft.PowerShell.Commands.AddContentCommand"
4849
}
4950

51+
Context "Add-Content should work with alternate data streams on Windows" {
52+
BeforeAll {
53+
if (!$isWindows) {
54+
return
55+
}
56+
$ADSTestDir = "addcontentadstest"
57+
$ADSTestFile = "addcontentads.txt"
58+
$streamContent = "This is a test stream."
59+
Setup -Directory "$ADSTestDir"
60+
Setup -File "$ADSTestFile"
61+
}
62+
63+
It "Should add an alternate data stream on a directory" -Skip:(!$IsWindows) {
64+
Add-Content -Path TestDrive:\$ADSTestDir -Stream Add-Content-Test-Stream -Value $streamContent -ErrorAction Stop
65+
Get-Content -Path TestDrive:\$ADSTestDir -Stream Add-Content-Test-Stream | Should -BeExactly $streamContent
66+
}
67+
68+
It "Should add an alternate data stream on a file" -Skip:(!$IsWindows) {
69+
Add-Content -Path TestDrive:\$ADSTestFile -Stream Add-Content-Test-Stream -Value $streamContent -ErrorAction Stop
70+
Get-Content -Path TestDrive:\$ADSTestFile -Stream Add-Content-Test-Stream | Should -BeExactly $streamContent
71+
}
72+
}
73+
5074
#[BugId(BugDatabase.WindowsOutOfBandReleases, 906022)]
5175
It "should throw 'NotSupportedException' when you add-content to an unsupported provider" -Skip:($IsLinux -Or $IsMacOS) {
5276
{ Add-Content -Path HKLM:\\software\\microsoft -Value "ShouldNotWorkBecausePathIsUnsupported" -ErrorAction Stop } | Should -Throw -ErrorId "NotSupported,Microsoft.PowerShell.Commands.AddContentCommand"

test/powershell/Modules/Microsoft.PowerShell.Management/Clear-Content.Tests.ps1

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Describe "Clear-Content cmdlet tests" -Tags "CI" {
5151
Setup -File "$file3" -Content $content2
5252
$streamContent = "content for alternate stream"
5353
$streamName = "altStream1"
54+
$dirName = "clearcontent"
55+
Setup -Directory "$dirName"
5456
}
5557

5658
Context "Clear-Content should actually clear content" {
@@ -75,32 +77,50 @@ Describe "Clear-Content cmdlet tests" -Tags "CI" {
7577
$cci.SupportsShouldProcess | Should -BeTrue
7678
}
7779

78-
It "Alternate streams should be cleared with clear-content" -Skip:(!$IsWindows) {
79-
# make sure that the content is correct
80-
# this is here rather than BeforeAll because only windows can write to an alternate stream
81-
Set-Content -Path "TestDrive:/$file3" -Stream $streamName -Value $streamContent
82-
Get-Content -Path "TestDrive:/$file3" | Should -BeExactly $content2
83-
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeExactly $streamContent
84-
Clear-Content -Path "TestDrive:/$file3" -Stream $streamName
85-
Get-Content -Path "TestDrive:/$file3" | Should -BeExactly $content2
86-
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeNullOrEmpty
87-
}
80+
Context "Clear-Content should work with alternate data streams on Windows" {
81+
It "Alternate streams should be cleared with Clear-Content on a file" -Skip:(!$IsWindows) {
82+
83+
Set-Content -Path "TestDrive:/$file3" -Stream $streamName -Value $streamContent
84+
Get-Content -Path "TestDrive:/$file3" -Stream $streamName | Should -BeExactly $streamContent
85+
86+
Clear-Content -Path "TestDrive:/$file3" -Stream $streamName -ErrorAction Stop
87+
88+
$result = Get-Item -Path "TestDrive:/$file3" -Stream $streamName
89+
$result | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
90+
$result.length | Should -Be 0
91+
}
8892

89-
It "the '-Stream' dynamic parameter is visible to get-command in the filesystem" -Skip:(!$IsWindows) {
90-
try {
91-
Push-Location -Path TestDrive:
92-
(Get-Command Clear-Content -Stream foo).parameters.keys -eq "stream" | Should -Be "stream"
93+
It "Alternate streams should be cleared with Clear-Content on a directory" -Skip:(!$IsWindows) {
94+
Set-Content -Path "TestDrive:/$dirName" -Stream $streamName -Value $streamContent
95+
96+
Get-Content -Path "TestDrive:/$dirName" -Stream $streamName | Should -BeExactly $streamContent
97+
Clear-Content -Path "TestDrive:/$dirName" -Stream $streamName -ErrorAction Stop
98+
99+
$result = Get-Item -Path "TestDrive:/$dirName" -Stream $streamName
100+
$result | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
101+
$result.length | Should -Be 0
93102
}
94-
finally {
95-
Pop-Location
103+
104+
It "the '-Stream' dynamic parameter is visible to get-command in the filesystem" -Skip:(!$IsWindows) {
105+
try {
106+
Push-Location -Path TestDrive:
107+
(Get-Command Clear-Content -Stream foo).parameters.keys -eq "Stream" | Should -BeExactly "Stream"
108+
}
109+
finally {
110+
Pop-Location
111+
}
96112
}
97-
}
98113

99-
It "the '-Stream' dynamic parameter should not be visible to get-command in the function provider" {
100-
Push-Location -Path function:
101-
{ Get-Command Clear-Content -Stream $streamName } |
102-
Should -Throw -ErrorId "NamedParameterNotFound,Microsoft.PowerShell.Commands.GetCommandCommand"
103-
Pop-Location
114+
It "the '-Stream' dynamic parameter should not be visible to get-command in the function provider" -Skip:(!$IsWindows) {
115+
try {
116+
Push-Location -Path function:
117+
{ Get-Command Clear-Content -Stream $streamName } |
118+
Should -Throw -ErrorId "NamedParameterNotFound,Microsoft.PowerShell.Commands.GetCommandCommand"
119+
}
120+
finally {
121+
Pop-Location
122+
}
123+
}
104124
}
105125
}
106126

test/powershell/Modules/Microsoft.PowerShell.Management/Get-Content.Tests.ps1

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -221,25 +221,27 @@ Describe "Get-Content" -Tags "CI" {
221221
$expected = 'He', 'o,', '', 'Wor', "d${nl}He", 'o2,', '', 'Wor', "d2${nl}"
222222
for ($i = 0; $i -lt $result.Length ; $i++) { $result[$i] | Should -BeExactly $expected[$i]}
223223
}
224+
225+
Context "Alternate Data Stream support on Windows" {
226+
It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) {
227+
Set-Content "${testPath}:Stream" -Value "Foo"
228+
{ Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" }
229+
Get-Content "${testPath}:Stream" | Should -BeExactly "Foo"
230+
Get-Content $testPath | Should -BeExactly $testString
231+
}
224232

225-
It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) {
226-
Set-Content "${testPath}:Stream" -Value "Foo"
227-
{ Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" }
228-
Get-Content "${testPath}:Stream" | Should -BeExactly "Foo"
229-
Get-Content $testPath | Should -BeExactly $testString
230-
}
231-
232-
It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) {
233-
Set-Content -Path $testPath -Stream hello -Value World
234-
Get-Content -Path $testPath | Should -BeExactly $testString
235-
Get-Content -Path $testPath -Stream hello | Should -BeExactly "World"
236-
$item = Get-Item -Path $testPath -Stream hello
237-
$item | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
238-
$item.Stream | Should -BeExactly "hello"
239-
Clear-Content -Path $testPath -Stream hello
240-
Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty
241-
Remove-Item -Path $testPath -Stream hello
242-
{ Get-Content -Path $testPath -Stream hello | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" }
233+
It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) {
234+
Set-Content -Path $testPath -Stream hello -Value World
235+
Get-Content -Path $testPath | Should -BeExactly $testString
236+
Get-Content -Path $testPath -Stream hello | Should -BeExactly "World"
237+
$item = Get-Item -Path $testPath -Stream hello
238+
$item | Should -BeOfType System.Management.Automation.Internal.AlternateStreamData
239+
$item.Stream | Should -BeExactly "hello"
240+
Clear-Content -Path $testPath -Stream hello
241+
Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty
242+
Remove-Item -Path $testPath -Stream hello
243+
{ Get-Content -Path $testPath -Stream hello -ErrorAction stop} | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand"
244+
}
243245
}
244246

245247
It "Should support colons in filename on Linux/Mac" -Skip:($IsWindows) {

0 commit comments

Comments
 (0)