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
14 changes: 14 additions & 0 deletions src/System.Management.Automation/CoreCLR/CorePsPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,16 @@ internal static bool NonWindowsIsSameFileSystemItem(string pathOne, string pathT
return Unix.NativeMethods.IsSameFileSystemItem(pathOne, pathTwo);
}

internal static bool NonWindowsGetInodeData(string path, out System.ValueTuple<UInt64, UInt64> inodeData)
{
UInt64 device = 0UL;
UInt64 inode = 0UL;
var result = Unix.NativeMethods.GetInodeData(path, out device, out inode);

inodeData = (device, inode);
return result == 0;
}

internal static bool NonWindowsIsExecutable(string path)
{
return Unix.NativeMethods.IsExecutable(path);
Expand Down Expand Up @@ -797,6 +807,10 @@ internal static extern int CreateHardLink([MarshalAs(UnmanagedType.LPStr)]string
[return: MarshalAs(UnmanagedType.I1)]
internal static extern bool IsSameFileSystemItem([MarshalAs(UnmanagedType.LPStr)]string filePathOne,
[MarshalAs(UnmanagedType.LPStr)]string filePathTwo);

[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
internal static extern int GetInodeData([MarshalAs(UnmanagedType.LPStr)]string path,
out UInt64 device, out UInt64 inode);
}
}
}
Expand Down
145 changes: 129 additions & 16 deletions src/System.Management.Automation/namespaces/FileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,8 +1539,19 @@ private void GetPathItems(
if (isDirectory)
{
DirectoryInfo directory = new DirectoryInfo(path);
InodeTracker tracker = null;

if (recurse)
{
GetChildDynamicParameters fspDynamicParam = DynamicParameters as GetChildDynamicParameters;
if (fspDynamicParam != null && fspDynamicParam.FollowSymlink)
{
tracker = new InodeTracker(directory.FullName);
}
}

// Enumerate the directory
Dir(directory, recurse, depth, nameOnly, returnContainers);
Dir(directory, recurse, depth, nameOnly, returnContainers, tracker);
}
else
{
Expand Down Expand Up @@ -1609,7 +1620,8 @@ private void Dir(
bool recurse,
uint depth,
bool nameOnly,
ReturnContainers returnContainers)
ReturnContainers returnContainers,
InodeTracker tracker) // tracker will be non-null only if the user invoked the -FollowSymLinks and -Recurse switch parameters.
{
List<IEnumerable<FileSystemInfo>> target = new List<IEnumerable<FileSystemInfo>>();

Expand Down Expand Up @@ -1788,25 +1800,35 @@ private void Dir(
return;
}

// Once the recursion process has begun by being in this function,
// we do not want to further recurse into directory symbolic links
// so as to prevent the possibility of an endless symlink loop.
// This is the behavior of both the Unix 'ls' command and the Windows
// 'DIR' command.
if (!InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(recursiveDirectory))
bool hidden = false;
if (!Force)
{
bool hidden = false;
if (!Force)
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
}

// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
// default hidden attribute filter.
if (Force || !hidden || isFilterHiddenSpecified || isSwitchFilterHiddenSpecified)
{
// We only want to recurse into symlinks if
// a) the user has asked to with the -FollowSymLinks switch parameter and
// b) the directory pointed to by the symlink has not already been visited,
// preventing symlink loops.
if (tracker == null)
{
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(recursiveDirectory))
{
continue;
}
}

// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
// default hidden attribute filter.
if (Force || !hidden || isFilterHiddenSpecified || isSwitchFilterHiddenSpecified)
else if (!tracker.TryVisitPath(recursiveDirectory.FullName))
{
Dir(recursiveDirectory, recurse, depth - 1, nameOnly, returnContainers);
WriteWarning(StringUtil.Format(FileSystemProviderStrings.AlreadyListedDirectory,
recursiveDirectory.FullName));
continue;
}

Dir(recursiveDirectory, recurse, depth - 1, nameOnly, returnContainers, tracker);
}
}//foreach
}//if
Expand Down Expand Up @@ -7320,6 +7342,52 @@ private struct NetResource
[MarshalAs(UnmanagedType.LPWStr)]
public string Provider;
}

#region InodeTracker
/// <summary>
/// Tracks visited files/directories by caching their device IDs and inodes.
/// </summary>
private class InodeTracker
{
private HashSet<(UInt64, UInt64)> _visitations;

/// <summary>
/// Construct a new InodeTracker with an initial path
/// </summary>
internal InodeTracker(string path)
{
_visitations = new HashSet<(UInt64, UInt64)>();

if (InternalSymbolicLinkLinkCodeMethods.GetInodeData(path, out (UInt64, UInt64) inodeData))
{
_visitations.Add(inodeData);
}
}

/// <summary>
/// Attempt to mark a path as having been visited.
/// </summary>
/// <param name="path">
/// Path to the file system item to be visited.
/// </param>
/// <returns>
/// True if the path had not been previously visited and was
/// successfully marked as visited, false otherwise.
/// </returns>
internal bool TryVisitPath(string path)
{
bool returnValue = false;

if (InternalSymbolicLinkLinkCodeMethods.GetInodeData(path, out (UInt64, UInt64) inodeData))
{
returnValue = _visitations.Add(inodeData);
}

return returnValue;
}
}

#endregion
} // class FileSystemProvider

internal static class SafeInvokeCommand
Expand Down Expand Up @@ -7482,6 +7550,12 @@ internal sealed class GetChildDynamicParameters
[Parameter]
public FlagsExpression<FileAttributes> Attributes { get; set; }

/// <summary>
/// Gets or sets the flag to follow symbolic links when recursing.
/// </summary>
[Parameter]
public SwitchParameter FollowSymlink { get; set; }

/// <summary>
/// Gets or sets the filter directory flag
/// </summary>
Expand Down Expand Up @@ -8238,6 +8312,45 @@ internal static bool WinIsSameFileSystemItem(string pathOne, string pathTwo)
return false;
}

internal static bool GetInodeData(string path, out System.ValueTuple<UInt64, UInt64> inodeData)
{
Copy link
Member

Choose a reason for hiding this comment

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

I see that you are following a pattern like IsSameFileSystemItem or IsHardLink. Ideally, code here should be refactored to consolidate Unix/Windows implementations in one place and eliminate wrapper functions like IsSameFileSystemItem and IsHardLink. Instead they should be

internal static bool IsSameFileSystemItem(string pathOne, string pathTwo)
{
#if UNIX
   // Unix implementation goes here
#else
   // Windows implementation goes here
#endif
}

I'm not asking you to do this change for this PR, just want to call out that it should be this way moving forward. I opened #4086 to track the refactoring task.

#if UNIX
bool rv = Platform.NonWindowsGetInodeData(path, out inodeData);
#else
bool rv = WinGetInodeData(path, out inodeData);
#endif
return rv;
}

internal static bool WinGetInodeData(string path, out System.ValueTuple<UInt64, UInt64> inodeData)
{
var access = FileAccess.Read;
var share = FileShare.Read;
var creation = FileMode.Open;
var attributes = FileAttributes.BackupSemantics | FileAttributes.PosixSemantics;

using (var sf = AlternateDataStreamUtilities.NativeMethods.CreateFile(path, access, share, IntPtr.Zero, creation, (int)attributes, IntPtr.Zero))
{
if (!sf.IsInvalid)
{
BY_HANDLE_FILE_INFORMATION info;

if (GetFileInformationByHandle(sf.DangerousGetHandle(), out info))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it supported on Windows 7?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's supported as early as XP.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Closed.

{
UInt64 tmp = info.FileIndexHigh;
tmp = (tmp << 32) | info.FileIndexLow;

inodeData = (info.VolumeSerialNumber, tmp);

return true;
}
}
}

inodeData = (0, 0);
return false;
}

internal static bool IsHardLink(ref IntPtr handle)
{
#if UNIX
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,7 @@
<data name="SymlinkItemExists" xml:space="preserve">
<value>Cannot create symbolic link because the path {0} already exists.</value>
</data>
<data name="AlreadyListedDirectory" xml:space="preserve">
<value>Skip already-visited directory {0}.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/libpsl-native/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ add_library(psl-native SHARED
getlinkcount.cpp
getfullyqualifiedname.cpp
geterrorcategory.cpp
getinodedata.cpp
isfile.cpp
isdirectory.cpp
issamefilesystemitem.cpp
Expand Down
56 changes: 56 additions & 0 deletions src/libpsl-native/src/getinodedata.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! @file getinodedata.cpp
//! @author Jeff Bienstadt <v-jebien@microsoft.com>
//! @brief Retrieve the device ID and inode number of a file

#include "getinodedata.h"

#include <assert.h>
#include <errno.h>
#include <locale.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string>

//! @brief GetInodeData retrieves a file's device and inode information.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Duplicated "@brief" ?

Copy link
Contributor Author

@jeffbi jeffbi Jun 16, 2017

Choose a reason for hiding this comment

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

This one describes the function. The other describes the file.

I used the existing getlinkcount.cpp as the pattern for this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Closed.

//!
//! GetInodeData
//!
//! @param[in] fileName
//! @parblock
//! A pointer to the buffer that contains the file path
//!
//! char* is marshaled as an LPStr, which on Linux is UTF-8.
//! @endparblock
//!
//! @param[out] device
//! @parblock
//! Points to a uint64_t value that will contain the file's device ID.
//! @endparblock
//!
//! @param[out] inode
//! @parblock
//! Points to a uint64_t value that will contain the file's inode number.
//! @endparblock
//!
//! @retval 0 If the function succeeds, -1 otherwise.
//!

int32_t GetInodeData(const char* fileName, uint64_t* device, uint64_t* inode)
{
assert(fileName);
assert(device);
assert(inode);
errno = 0;

struct stat statBuf;
int ret = stat(fileName, &statBuf);

if (ret == 0)
{
*device = statBuf.st_dev;
*inode = statBuf.st_ino;
}

return ret;
}
9 changes: 9 additions & 0 deletions src/libpsl-native/src/getinodedata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include "pal.h"

PAL_BEGIN_EXTERNC

int32_t GetInodeData(const char* fileName, uint64_t* device, uint64_t* inode);

PAL_END_EXTERNC
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,18 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
$alphaLink = Join-Path $TestDrive "link-alpha"
$alphaFile1 = Join-Path $alphaDir "AlphaFile1.txt"
$alphaFile2 = Join-Path $alphaDir "AlphaFile2.txt"
$omegaDir = Join-Path $TestDrive "sub-omega"
$omegaFile1 = Join-Path $omegaDir "OmegaFile1"
$omegaFile2 = Join-Path $omegaDir "OmegaFile2"
$betaDir = Join-Path $alphaDir "sub-beta"
$betaLink = Join-Path $alphaDir "link-beta"
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
$betaFile2 = Join-Path $betaDir "BetaFile2.txt"
$betaFile3 = Join-Path $betaDir "BetaFile3.txt"
$gammaDir = Join-Path $betaDir "sub-gamma"
$uponeLink = Join-Path $gammaDir "upone-link"
$uptwoLink = Join-Path $gammaDir "uptwo-link"
$omegaLink = Join-Path $gammaDir "omegaLink"

New-Item -ItemType Directory -Path $alphaDir
New-Item -ItemType File -Path $alphaFile1
Expand All @@ -389,6 +396,9 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
New-Item -ItemType File -Path $betaFile1
New-Item -ItemType File -Path $betaFile2
New-Item -ItemType File -Path $betaFile3
New-Item -ItemType Directory $omegaDir
New-Item -ItemType File -Path $omegaFile1
New-Item -ItemType File -Path $omegaFile2
}
AfterAll {
Remove-Item -Path $alphaLink -Force -ErrorAction SilentlyContinue
Expand All @@ -408,6 +418,15 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
$ci = Get-ChildItem $alphaLink -Recurse
$ci.Count | Should BeExactly 7
}
It "Get-ChildItem will recurse into symlinks given -FollowSymlink, avoiding link loops" {
New-Item -ItemType Directory -Path $gammaDir
New-Item -ItemType SymbolicLink -Path $uponeLink -Value $betaDir
New-Item -ItemType SymbolicLink -Path $uptwoLink -Value $alphaDir
New-Item -ItemType SymbolicLink -Path $omegaLink -Value $omegaDir
$ci = Get-ChildItem -Path $alphaDir -FollowSymlink -Recurse -WarningVariable w -WarningAction SilentlyContinue
$ci.Count | Should BeExactly 13
$w.Count | Should BeExactly 3
}
}

Context "Remove-Item and hard/symbolic links" {
Expand Down