8000 Replace string-compare-based test for copying to same file with more … by jeffbi · Pull Request #3441 · PowerShell/PowerShell · GitHub
[go: up one dir, main page]

Skip to content

Replace string-compare-based test for copying to same file with more … #3441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 21, 2017
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
12 changes: 11 additions & 1 deletion src/System.Management.Automation/CoreCLR/CorePsPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
//
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
51 changes: 46 additions & 5 deletions src/System.Management.Automation/namespaces/FileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";


/// <summary>
/// An instance of the PSTraceSource class used for trace output
/// using "FileSystemProvider" as the category.
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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"))
{
Expand Down Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please clarify - do we really need PosixSemantics here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's for if/when we find ourselves in running in a Windows environment in which case-sensitivity has been turned on. Since the purpose of the function is to see if two files/directories are the same, it made sense to me to give it every advantage in making a correct assessment.


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
Expand Down Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions src/libpsl-native/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ add_library(psl-native SHARED
geterrorcategory.cpp
isfile.cpp
isdirectory.cpp
issamefilesystemitem.cpp
issymlink.cpp
isexecutable.cpp
setdate.cpp
Expand Down
6D47 48 changes: 48 additions & 0 deletions src/libpsl-native/src/issamefilesystemitem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! @file issamefilesystemitem.cpp
//! @author Jeff Bienstadt <v-jebien@microsoft.com>
//! @brief Determines whether two paths ultimately point to the same filesystem object

#include "getstat.h"
#include "issamefilesystemitem.h"

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

//! @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;
}
11 changes: 11 additions & 0 deletions src/libpsl-native/src/issamefilesystemitem.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include "pal.h"

Copy link
Collaborator

Choose a reason for hiding this comment

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

Extra line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Discarded.

#include <stdbool.h>

PAL_BEGIN_EXTERNC

bool IsSameFileSystemItem(const char* path_one, const char* path_two);

PAL_END_EXTERNC
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<Name>" -TestCases $testCases {
Param (
[string]$Name,
[string]$Source,
[string]$Destination
)

TestSelfCopy $Source $Destination
}

It "<Name>" -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"
Expand Down Expand Up @@ -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
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

It distracts attention. We should make separate PR for formatting.

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"
}
Expand Down
0