diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs index d3f8cead66f..7a5f93cc94d 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs @@ -515,6 +515,11 @@ internal static bool NonWindowsIsDirectory(string path) return Unix.NativeMethods.IsDirectory(path); } + internal static bool NonWindowsIsSameFileSystemItem(string pathOne, string pathTwo) + { + return Unix.NativeMethods.IsSameFileSystemItem(pathOne, pathTwo); + } + internal static bool NonWindowsIsExecutable(string path) { return Unix.NativeMethods.IsExecutable(path); @@ -530,7 +535,7 @@ internal static int NonWindowsGetProcessParentPid(int pid) return IsOSX ? Unix.NativeMethods.GetPPid(pid) : Unix.GetProcFSParentPid(pid); } - + // Unix specific implementations of required functionality // @@ -722,6 +727,11 @@ internal static extern int CreateHardLink([MarshalAs(UnmanagedType.LPStr)]string [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] [return: MarshalAs(UnmanagedType.I1)] internal static extern bool IsDirectory([MarshalAs(UnmanagedType.LPStr)]string filePath); + + [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.I1)] + internal static extern bool IsSameFileSystemItem([MarshalAs(UnmanagedType.LPStr)]string filePathOne, + [MarshalAs(UnmanagedType.LPStr)]string filePathTwo); } } } diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index 213a7e66ac7..6008edeb9b5 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -62,6 +62,11 @@ public sealed partial class FileSystemProvider : NavigationCmdletProvider, // copy script will accomodate the new value. private const int FILETRANSFERSIZE = 4 * 1024 * 1024; + // The name of the key in an exception's Data dictionary when attempting + // to copy an item onto itself. + private const string SelfCopyDataKey = "SelfCopy"; + + /// /// An instance of the PSTraceSource class used for trace output /// using "FileSystemProvider" as the category. @@ -3460,14 +3465,14 @@ protected override void CopyItem( _excludeMatcher = SessionStateUtilities.CreateWildcardsFromStrings(Exclude, WildcardOptions.IgnoreCase); // if the source and destination path are same (for a local copy) then flag it as error. - if ((toSession == null) && (fromSession == null) && path.Equals(destinationPath, StringComparison.OrdinalIgnoreCase)) + if ((toSession == null) && (fromSession == null) && InternalSymbolicLinkLinkCodeMethods.IsSameFileSystemItem(path, destinationPath)) { String error = StringUtil.Format(FileSystemProviderStrings.CopyError, path); Exception e = new IOException(error); + e.Data[SelfCopyDataKey] = destinationPath; WriteError(new ErrorRecord(e, "CopyError", ErrorCategory.WriteError, path)); return; } - // Copy-Item from session if (fromSession != null) { @@ -3777,14 +3782,14 @@ private void CopyFileInfoItem(FileInfo file, string destinationPath, bool force, } //if the source and destination path are same then flag it as error. - if (destinationPath.Equals(file.FullName, StringComparison.OrdinalIgnoreCase)) + if (InternalSymbolicLinkLinkCodeMethods.IsSameFileSystemItem(destinationPath, file.FullName)) { String error = StringUtil.Format(FileSystemProviderStrings.CopyError, destinationPath); Exception e = new IOException(error); + e.Data[SelfCopyDataKey] = file.FullName; WriteError(new ErrorRecord(e, "CopyError", ErrorCategory.WriteError, destinationPath)); return; } - // Verify that the target doesn't represent a device name if (PathIsReservedDeviceName(destinationPath, "CopyError")) { @@ -8211,6 +8216,42 @@ internal static bool WinIsHardLink(FileSystemInfo fileInfo) return isHardLink; } + internal static bool IsSameFileSystemItem(string pathOne, string pathTwo) + { +#if UNIX + return Platform.NonWindowsIsSameFileSystemItem(pathOne, pathTwo); +#else + return WinIsSameFileSystemItem(pathOne, pathTwo); +#endif + } + + internal static bool WinIsSameFileSystemItem(string pathOne, string pathTwo) + { + var access = FileAccess.Read; + var share = FileShare.Read; + var creation = FileMode.Open; + var attributes = FileAttributes.BackupSemantics | FileAttributes.PosixSemantics; + + using (var sfOne = AlternateDataStreamUtilities.NativeMethods.CreateFile(pathOne, access, share, IntPtr.Zero, creation, (int)attributes, IntPtr.Zero)) + using (var sfTwo = AlternateDataStreamUtilities.NativeMethods.CreateFile(pathTwo, access, share, IntPtr.Zero, creation, (int)attributes, IntPtr.Zero)) + { + if (!sfOne.IsInvalid && !sfTwo.IsInvalid) + { + BY_HANDLE_FILE_INFORMATION infoOne; + BY_HANDLE_FILE_INFORMATION infoTwo; + if ( GetFileInformationByHandle(sfOne.DangerousGetHandle(), out infoOne) + && GetFileInformationByHandle(sfTwo.DangerousGetHandle(), out infoTwo)) + { + return infoOne.VolumeSerialNumber == infoTwo.VolumeSerialNumber + && infoOne.FileIndexHigh == infoTwo.FileIndexHigh + && infoOne.FileIndexLow == infoTwo.FileIndexLow; + } + } + } + + return false; + } + internal static bool IsHardLink(ref IntPtr handle) { #if UNIX @@ -8664,7 +8705,7 @@ internal static void SetZoneOfOrigin(string path, SecurityZone securityZone) // the code above seems cleaner and more robust than the IAttachmentExecute approach } - private static class NativeMethods + internal static class NativeMethods { internal const int ERROR_HANDLE_EOF = 38; internal enum StreamInfoLevels { FindStreamInfoStandard = 0 } diff --git a/src/libpsl-native/src/CMakeLists.txt b/src/libpsl-native/src/CMakeLists.txt index 6cb86d0169d..720bc65f0fe 100644 --- a/src/libpsl-native/src/CMakeLists.txt +++ b/src/libpsl-native/src/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(psl-native SHARED geterrorcategory.cpp isfile.cpp isdirectory.cpp + issamefilesystemitem.cpp issymlink.cpp isexecutable.cpp setdate.cpp diff --git a/src/libpsl-native/src/issamefilesystemitem.cpp b/src/libpsl-native/src/issamefilesystemitem.cpp new file mode 100644 index 00000000000..9e63e6e0749 --- /dev/null +++ b/src/libpsl-native/src/issamefilesystemitem.cpp @@ -0,0 +1,48 @@ +//! @file issamefilesystemitem.cpp +//! @author Jeff Bienstadt +//! @brief Determines whether two paths ultimately point to the same filesystem object + +#include "getstat.h" +#include "issamefilesystemitem.h" + +#include +#include +#include +#include + +//! @brief Returns a boolean value indicating whether two paths ultimately refer to the same file or directory. +//! +//! IsSameFileSystemItem +//! +//! @param[in] path_one +//! @parblock +//! A pointer to the buffer that contains the first path. +//! +//! char* is marshaled as an LPStr, which on Linux is UTF-8. +//! @endparblock +//! +//! @param[in] path_two +//! @parblock +//! A pointer to the buffer that contains the second path. +//! +//! char* is marshaled as an LPStr, which on Linux is UTF-8. +//! @endparblock +//! +//! @retval true if both paths point to the same filesystem object, +//! false otherwise +//! +bool IsSameFileSystemItem(const char* path_one, const char* path_two) +{ + assert(path_one); + assert(path_two); + + struct stat buf_1; + struct stat buf_2; + + if (GetStat(path_one, &buf_1) == 0 && GetStat(path_two, &buf_2) == 0) + { + return buf_1.st_dev == buf_2.st_dev && buf_1.st_ino == buf_2.st_ino; + } + + return false; +} diff --git a/src/libpsl-native/src/issamefilesystemitem.h b/src/libpsl-native/src/issamefilesystemitem.h new file mode 100644 index 00000000000..7c3e3ade860 --- /dev/null +++ b/src/libpsl-native/src/issamefilesystemitem.h @@ -0,0 +1,11 @@ +#pragma once + +#include "pal.h" + +#include + +PAL_BEGIN_EXTERNC + +bool IsSameFileSystemItem(const char* path_one, const char* path_two); + +PAL_END_EXTERNC diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1 index 748fb18313b..4a881d36ff5 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1 @@ -246,6 +246,157 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" { } } + +Describe "Copy-Item can avoid copying an item onto itself" -Tags "CI", "RequireAdminOnWindows" { + BeforeAll { + # For now, we'll assume the tests are running the platform's + # native filesystem, in its default mode + $isCaseSensitive = $IsLinux + + # The name of the key in an exception's Data dictionary when an + # attempt is made to copy an item onto itself. + $selfCopyKey = "SelfCopy" + + $TestDrive = "TestDrive:" + $subDir = "$TestDrive/sub" + $otherSubDir = "$TestDrive/other-sub" + $fileName = "file.txt" + $filePath = "$TestDrive/$fileName" + $otherFileName = "other-file" + $otherFile = "$otherSubDir/$otherFileName" + $symToOther = "$subDir/sym-to-other" + $secondSymToOther = "$subDir/another-sym-to-other" + $symToSym = "$subDir/sym-to-sym-to-other" + $symToOtherFile = "$subDir/sym-to-other-file" + $hardToOtherFile = "$subDir/hard-to-other-file" + $symdToOther = "$subDir/symd-to-other" + $junctionToOther = "$subDir/junction-to-other" + + New-Item -ItemType File $filePath -Value "stuff" >$null + New-Item -ItemType Directory $subDir >$null + New-Item -ItemType Directory $otherSubDir >$null + New-Item -ItemType File $otherFile -Value "some text" >$null + New-Item -ItemType SymbolicLink $symToOther -Value $otherSubDir >$null + New-Item -ItemType SymbolicLink $secondSymToOther -Value $otherSubDir >$null + New-Item -ItemType SymbolicLink $symToSym -Value $symToOther >$null + New-Item -ItemType SymbolicLink $symToOtherFile -Value $otherFile >$null + New-Item -ItemType HardLink $hardToOtherFile -Value $otherFile >$null + + if ($IsWindows) + { + New-Item -ItemType Junction $junctionToOther -Value $otherSubDir >$null + New-Item -ItemType SymbolicLink $symdToOther -Value $otherSubDir >$null + } + } + + Context "Copy-Item using different case (on case-sensitive file systems)" { + BeforeEach { + $sourcePath = $filePath + $destinationPath = "$TestDrive/" + $fileName.Toupper() + } + AfterEach { + Remove-Item -Path $destinationPath -ErrorAction SilentlyContinue + } + + It "Copy-Item can copy to file name differing only by case" { + if ($isCaseSensitive) + { + Copy-Item -Path $sourcePath -Destination $destinationPath -ErrorAction SilentlyContinue | Should Be $null + Test-Path -Path $destinationPath | Should Be $true + } + else + { + { Copy-Item -Path $sourcePath -Destination $destinationPath -ErrorAction Stop } | ShouldBeErrorId "CopyError,Microsoft.PowerShell.Commands.CopyItemCommand" + $Error[0].Exception | Should BeOfType System.IO.IOException + $Error[0].Exception.Data[$selfCopyKey] | Should Not Be $null + } + } + } + + Context "Copy-Item avoids copying an item onto itself" { + BeforeAll { + $testCases = @( + @{ + Name = "Copy to same path" + Source = $otherFile + Destination = $otherFile + } + @{ + Name = "Copy hard link" + Source = $hardToOtherFile + Destination = $otherFile + } + @{ + Name = "Copy hard link, reversed" + Source = $otherFile + Destination = $hardToOtherFile + } + @{ + Name = "Copy symbolic link to target" + Source = $symToOtherFile + Destination = $otherFile + } + @{ + Name = "Copy symbolic link to symbolic link with same target" + Source = $secondSymToOther + Destination = $symToOther + } + @{ + Name = "Copy through chain of symbolic links" + Source = $symToSym + Destination = $otherSubDir + } + ) + + # Junctions and directory symbolic links are Windows and NTFS only + $windowsTestCases = @( + @{ + Name = "Copy junction to target" + Source = $junctionToOther + Destination = $otherSubDir + } + @{ + Name = "Copy directory symbolic link to target" + Source = $symdToOther + Destination = $otherSubDir + } + ) + + function TestSelfCopy + { + Param ( + [string]$Source, + [string]$Destination + ) + + { Copy-Item -Path $Source -Destination $Destination -ErrorAction Stop } | ShouldBeErrorId "CopyError,Microsoft.PowerShell.Commands.CopyItemCommand" + $Error[0].Exception | Should BeOfType System.IO.IOException + $Error[0].Exception.Data[$selfCopyKey] | Should Not Be $null + } + } + + It "" -TestCases $testCases { + Param ( + [string]$Name, + [string]$Source, + [string]$Destination + ) + + TestSelfCopy $Source $Destination + } + + It "" -TestCases $windowsTestCases -Skip:(-not $IsWindows) { + Param ( + [string]$Name, + [string]$Source, + [string]$Destination + ) + + TestSelfCopy $Source $Destination + } + } +} + Describe "Extended FileSystem Item/Content Cmdlet Provider Tests" -Tags "Feature" { BeforeAll { $testDir = "testDir" @@ -629,14 +780,13 @@ Describe "Extended FileSystem Path/Location Cmdlet Provider Tests" -Tags "Featur $result = Split-Path -Path $level1_0Full -Leaf $result | Should Be $level1_0 } - + It 'Validate LeafBase' { $result = Split-Path -Path "$level2_1Full$fileExt" -LeafBase $result | Should Be $level2_1 } - It 'Validate LeafBase is not over-zealous' { - + $result = Split-Path -Path "$level2_1Full$fileExt$fileExt" -LeafBase $result | Should Be "$level2_1$fileExt" }