Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3256,7 +3256,7 @@ protected override void ProcessRecord()
try
{
System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(providerPath);
if (!Platform.IsWindows && di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
{
shouldRecurse = false;
treatAsFile = true;
Expand Down
14 changes: 3 additions & 11 deletions src/System.Management.Automation/namespaces/FileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2863,15 +2863,6 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool
continueRemoval = ShouldProcess(directory.FullName, action);
}

//if this is a reparse point and force is not specified then warn user but dont remove the directory.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removed. It seems like the -Force parameter was required before removing and was Windows only. So I assume removing this requirement makes Windows and Linux behavior the same. Please make this clear in the PR description.

Also if we remove this code we should also remove the associated FileSystemProviderStrings.DirectoryReparsePoint message string since it is no longer used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the PR description and removed the unused DirectoryReparsePoint message string.

if (Platform.IsWindows && ((directory.Attributes & FileAttributes.ReparsePoint) != 0) && !Force)
{
String error = StringUtil.Format(FileSystemProviderStrings.DirectoryReparsePoint, directory.FullName);
Exception e = new IOException(error);
WriteError(new ErrorRecord(e, "DirectoryNotEmpty", ErrorCategory.WriteError, directory));
return;
}

if ((directory.Attributes & FileAttributes.ReparsePoint) != 0)
{
bool success = InternalSymbolicLinkLinkCodeMethods.DeleteJunction(directory.FullName);
Expand Down Expand Up @@ -3327,13 +3318,14 @@ protected override bool HasChildItems(string path)
path = NormalizePath(path);

// First check to see if it is a directory

try
{
DirectoryInfo directory = new DirectoryInfo(path);

// If the above didn't throw an exception, check to
// see if it contains any directories
// see if we should proceed and if it contains any children
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit surprising. What case does this cover where an exception is not thrown but Attributes is not Directory? Please add a comment to clarify.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DirectoryInfo constructor will succeed if the given path does not exist or if it actually refers to a file. In the former case the Exists property will be false, in the latter the Attributes property will lack the Directory attribute.

The myriad other ways the constructor will fail are explicitly called out in the various catch blocks.

if ((directory.Attributes & FileAttributes.Directory) != FileAttributes.Directory)
return false;

result = DirectoryInfoHasChildItems(directory);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -216,9 +216,6 @@
<data name="DirectoryExist" xml:space="preserve">
<value>An item with the specified name {0} already exists.</value>
</data>
<data name="DirectoryReparsePoint" xml:space="preserve">
<value>{0} is an NTFS junction point. Use the Force parameter to delete or modify this object.</value>
</data>
<data name="BasePathLengthError" xml:space="preserve">
<value>The path length is too short. The character length of a path cannot be less than the character length of the basePath.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,186 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" {
}
}

Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your description mentions that the -Recurse parameter switch was ignored. But I don't see any tests to verify that it is fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test using the -Recurse parameter when removing a symlink to a directory

BeforeAll {
# on macOS, the /tmp directory is a symlink, so we'll resolve it here
$TestPath = $TestDrive
if ($IsOSX)
{
$item = Get-Item $TestPath
$dirName = $item.BaseName
$item = Get-Item $item.PSParentPath
if ($item.LinkType -eq "SymbolicLink")
{
$TestPath = Join-Path $item.Target $dirName
}
}

$realFile = Join-Path $TestPath "file.txt"
$nonFile = Join-Path $TestPath "not-a-file"
$fileContent = "some text"
$realDir = Join-Path $TestPath "subdir"
$nonDir = Join-Path $TestPath "not-a-dir"
$hardLinkToFile = Join-Path $TestPath "hard-to-file.txt"
$symLinkToFile = Join-Path $TestPath "sym-link-to-file.txt"
$symLinkToDir = Join-Path $TestPath "sym-link-to-dir"
$symLinkToNothing = Join-Path $TestPath "sym-link-to-nowhere"
$dirSymLinkToDir = Join-Path $TestPath "symd-link-to-dir"
$junctionToDir = Join-Path $TestPath "junction-to-dir"

New-Item -ItemType File -Path $realFile -Value $fileContent >$null
New-Item -ItemType Directory -Path $realDir >$null
}

Context "New-Item and hard/symbolic links" {
It "New-Item can create a hard link to a file" {
New-Item -ItemType HardLink -Path $hardLinkToFile -Value $realFile
Test-Path $hardLinkToFile | Should Be $true
$link = Get-Item -Path $hardLinkToFile
$link.LinkType | Should BeExactly "HardLink"
Get-Content -Path $hardLinkToFile | Should be $fileContent
}
It "New-Item can create symbolic link to file" {
New-Item -ItemType SymbolicLink -Path $symLinkToFile -Value $realFile
Test-Path $symLinkToFile | Should Be $true
$real = Get-Item -Path $realFile
$link = Get-Item -Path $symLinkToFile
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
Get-Content -Path $symLinkToFile | Should be $fileContent
}
It "New-Item can create a symbolic link to nothing" {
New-Item -ItemType SymbolicLink -Path $symLinkToNothing -Value $nonFile
Test-Path $symLinkToNothing | Should Be $true
$link = Get-Item -Path $symLinkToNothing
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $nonFile
}
It "New-Item can create a symbolic link to a directory" -Skip:($IsWindows) {
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
Test-Path $symLinkToDir | Should Be $true
$real = Get-Item -Path $realDir
$link = Get-Item -Path $symLinkToDir
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
}
It "New-Item can create a directory symbolic link to a directory" -Skip:(-Not $IsWindows) {
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
Test-Path $symLinkToDir | Should Be $true
$real = Get-Item -Path $realDir
$link = Get-Item -Path $symLinkToDir
$link | Should BeOfType System.IO.DirectoryInfo
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
}
It "New-Item can create a directory junction to a directory" -Skip:(-Not $IsWindows) {
New-Item -ItemType Junction -Path $junctionToDir -Value $realDir
Test-Path $junctionToDir | Should Be $true
}
}

Context "Remove-Item and hard/symbolic links" {
BeforeAll {
$testCases = @(
@{
Name = "Remove-Item can remove a hard link to a file"
Link = $hardLinkToFile
Target = $realFile
}
@{
Name = "Remove-Item can remove a symbolic link to a file"
Link = $symLinkToFile
Target = $realFile
}
)

# New-Item on Windows will not create a "plain" symlink to a directory
$unixTestCases = @(
@{
Name = "Remove-Item can remove a symbolic link to a directory on Unix"
Link = $symLinkToDir
Target = $realDir
}
)

# Junctions and directory symbolic links are Windows and NTFS only
$windowsTestCases = @(
@{
Name = "Remove-Item can remove a symbolic link to a directory on Windows"
Link = $symLinkToDir
Target = $realDir
}
@{
Name = "Remove-Item can remove a directory symbolic link to a directory on Windows"
Link = $dirSymLinkToDir
Target = $realDir
}
@{
Name = "Remove-Item can remove a junction to a directory"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"on Windows" skipped 😄

Link = $junctionToDir
Target = $realDir
}
)

function TestRemoveItem
{
Param (
[string]$Link,
[string]$Target
)

Remove-Item -Path $Link -ErrorAction SilentlyContinue >$null
Test-Path -Path $Link | Should Be $false
Test-Path -Path $Target | Should Be $true
}
}

It "<Name>" -TestCases $testCases {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)

TestRemoveItem $Link $Target
}

It "<Name>" -TestCases $unixTestCases -Skip:($IsWindows) {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)

TestRemoveItem $Link $Target
}

It "<Name>" -TestCases $windowsTestCases -Skip:(-not $IsWindows) {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)

TestRemoveItem $Link $Target
}

It "Remove-Item ignores -Recurse switch when deleting symlink to directory" {
$folder = Join-Path $TestDrive "folder"
$file = Join-Path $TestDrive "folder" "file"
$link = Join-Path $TestDrive "sym-to-folder"
New-Item -ItemType Directory -Path $folder >$null
New-Item -ItemType File -Path $file -Value "some content" >$null
New-Item -ItemType SymbolicLink -Path $link -value $folder >$null
$childA = Get-Childitem $folder
Remove-Item -Path $link -Recurse
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use -Force and -ErrorAction SilentlyContinue or maybe -ErrorAction Continue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite clear on this one. This test shows that when we use -Recurse when deleting a symlink, it is ignored. Remove-Item should not fail in this circumstance, it should just remove the symlink without even trying to recurse into the linked-to directory.

$childB = Get-ChildItem $folder
$childB.Count | Should Be 1
$childB.Count | Should BeExactly $childA.Count
$childB.Name | Should BeExactly $childA.Name
}
}
}

Describe "Copy-Item can avoid copying an item onto itself" -Tags "CI", "RequireAdminOnWindows" {
BeforeAll {
Expand Down