diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Rename.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Rename.cs index 8670f15a7ba40..741f4000627c0 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Rename.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Rename.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.InteropServices; +using System.Text; internal static partial class Interop { @@ -18,5 +19,20 @@ internal static partial class Sys /// [GeneratedDllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Rename", CharSet = CharSet.Ansi, SetLastError = true)] internal static partial int Rename(string oldPath, string newPath); + + [GeneratedDllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Rename", SetLastError = true)] + internal static partial int Rename(ref byte oldPath, ref byte newPath); + + internal static int Rename(ReadOnlySpan oldPath, ReadOnlySpan newPath) + { + ValueUtf8Converter converterNewPath = new(stackalloc byte[DefaultPathBufferSize]); + ValueUtf8Converter converterOldPath = new(stackalloc byte[DefaultPathBufferSize]); + int result = Rename( + ref MemoryMarshal.GetReference(converterOldPath.ConvertAndTerminateString(oldPath)), + ref MemoryMarshal.GetReference(converterNewPath.ConvertAndTerminateString(newPath))); + converterNewPath.Dispose(); + converterOldPath.Dispose(); + return result; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs index 2e2a4b8d72dd3..b434c2f8ba512 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -248,10 +248,7 @@ public static void Move(string sourceDirName, string destDirName) if (destDirName.Length == 0) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destDirName)); - string destinationFullPath = Path.GetFullPath(destDirName); - string destinationWithSeparator = PathInternal.EnsureTrailingSeparator(destinationFullPath); - - FileSystem.MoveDirectory(Path.GetFullPath(sourceDirName), destinationFullPath, destinationWithSeparator); + FileSystem.MoveDirectory(Path.GetFullPath(sourceDirName), Path.GetFullPath(destDirName)); } public static void Delete(string path) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs index 3ca339fc40eab..0f0d72cefe840 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs @@ -194,13 +194,11 @@ public void MoveTo(string destDirName) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destDirName)); string destination = Path.GetFullPath(destDirName); - string destinationWithSeparator = PathInternal.EnsureTrailingSeparator(destination); - FileSystem.MoveDirectory(FullPath, destination, destinationWithSeparator, - OperatingSystem.IsWindows() ? null : Exists); // on Windows we don't need to perform the extra check + FileSystem.MoveDirectory(FullPath, destination); Init(originalPath: destDirName, - fullPath: destinationWithSeparator, + fullPath: PathInternal.EnsureTrailingSeparator(destination), fileName: null, isNormalized: true); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs index 32eedaa50d06d..8f4775d09e418 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs @@ -387,58 +387,49 @@ private static void CreateParentsAndDirectory(string fullPath) } } - private static void MoveDirectory(string sourceFullPath, string destFullPath, bool sameDirectoryDifferentCase, bool? sourceDirectoryExists) + private static void MoveDirectory(string sourceFullPath, string destFullPath, bool sameDirectoryDifferentCase) { - sourceDirectoryExists ??= DirectoryExists(sourceFullPath); + ReadOnlySpan srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan()); + ReadOnlySpan destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan()); - // Windows will throw if the source file/directory doesn't exist, we preemptively check - // to make sure our cross platform behavior matches .NET Framework behavior. - if (!sourceDirectoryExists.Value) + if (!sameDirectoryDifferentCase) // This check is to allow renaming of directories { - if (!FileExists(sourceFullPath)) - { - throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); - } - - // Windows doesn't care if you try and copy a file via "MoveDirectory"... - // ... but it doesn't like the source to have a trailing slash ... - - // On Windows we end up with ERROR_INVALID_NAME, which is - // "The filename, directory name, or volume label syntax is incorrect." - // - // This surfaces as an IOException, if we let it go beyond here it would - // give DirectoryNotFound. - - if (Path.EndsInDirectorySeparator(sourceFullPath)) + if (Interop.Sys.Stat(destNoDirectorySeparator, out _) >= 0) { - throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); - } + // destination exists, but before we throw we need to check whether source exists or not - // ... but it doesn't care if the destination has a trailing separator. - destFullPath = Path.TrimEndingDirectorySeparator(destFullPath); - } + // Windows will throw if the source file/directory doesn't exist, we preemptively check + // to make sure our cross platform behavior matches .NET Framework behavior. + if (Interop.Sys.Stat(srcNoDirectorySeparator, out Interop.Sys.FileStatus sourceFileStatus) < 0) + { + throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); + } + else if ((sourceFileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFDIR + && Path.EndsInDirectorySeparator(sourceFullPath)) + { + throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); + } - if (!sameDirectoryDifferentCase) // This check is to allow renaming of directories - { - if (DirectoryExists(destFullPath)) - { - throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath)); - } - else if (FileExists(destFullPath)) - { // Some Unix distros will overwrite the destination file if it already exists. // Throwing IOException to match Windows behavior. throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath)); } } - if (Interop.Sys.Rename(sourceFullPath, destFullPath) < 0) + if (Interop.Sys.Rename(sourceFullPath, destNoDirectorySeparator) < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); switch (errorInfo.Error) { case Interop.Error.EACCES: // match Win32 exception throw new IOException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, sourceFullPath), errorInfo.RawErrno); + case Interop.Error.ENOENT: + throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, + Interop.Sys.Stat(srcNoDirectorySeparator, out _) >= 0 + ? destFullPath // the source directory exists, so destination does not. Example: Move("/tmp/existing/", "/tmp/nonExisting1/nonExisting2/") + : sourceFullPath)); + case Interop.Error.ENOTDIR: // sourceFullPath exists and it's not a directory + throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); default: throw Interop.GetExceptionForIoErrno(errorInfo, isDirectory: true); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs index eb7ad9b0d0b0b..a64f82b5ed498 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs @@ -154,7 +154,7 @@ public static DateTimeOffset GetLastWriteTime(string fullPath) return data.ftLastWriteTime.ToDateTimeOffset(); } - private static void MoveDirectory(string sourceFullPath, string destFullPath, bool sameDirectoryDifferentCase, bool? sourceDirectoryExists) + private static void MoveDirectory(string sourceFullPath, string destFullPath, bool sameDirectoryDifferentCase) { if (!Interop.Kernel32.MoveFile(sourceFullPath, destFullPath, overwrite: false)) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs index 1eb5971a3a056..99821e0f9e120 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs @@ -21,9 +21,10 @@ internal static void VerifyValidPath(string path, string argName) } } - internal static void MoveDirectory(string sourceFullPath, string destFullPath, string destinationWithSeparator, bool? sourceDirectoryExists = default) + internal static void MoveDirectory(string sourceFullPath, string destFullPath) { - string sourcePath = PathInternal.EnsureTrailingSeparator(sourceFullPath); + ReadOnlySpan srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan()); + ReadOnlySpan destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan()); ReadOnlySpan sourceDirNameFromFullPath = Path.GetFileName(sourceFullPath.AsSpan()); ReadOnlySpan destDirNameFromFullPath = Path.GetFileName(destFullPath.AsSpan()); @@ -37,17 +38,17 @@ internal static void MoveDirectory(string sourceFullPath, string destFullPath, s destDirNameFromFullPath.Equals(sourceDirNameFromFullPath, fileSystemSensitivity); // If the destination directories are the exact same name - if (!sameDirectoryDifferentCase && string.Equals(sourcePath, destinationWithSeparator, fileSystemSensitivity)) + if (!sameDirectoryDifferentCase && srcNoDirectorySeparator.Equals(destNoDirectorySeparator, fileSystemSensitivity)) throw new IOException(SR.IO_SourceDestMustBeDifferent); - ReadOnlySpan sourceRoot = Path.GetPathRoot(sourcePath.AsSpan()); - ReadOnlySpan destinationRoot = Path.GetPathRoot(destinationWithSeparator.AsSpan()); + ReadOnlySpan sourceRoot = Path.GetPathRoot(srcNoDirectorySeparator); + ReadOnlySpan destinationRoot = Path.GetPathRoot(destNoDirectorySeparator); // Compare paths for the same, skip this step if we already know the paths are identical. if (!sourceRoot.Equals(destinationRoot, StringComparison.OrdinalIgnoreCase)) throw new IOException(SR.IO_SourceDestMustHaveSameRoot); - MoveDirectory(sourceFullPath, destFullPath, sameDirectoryDifferentCase, sourceDirectoryExists); + MoveDirectory(sourceFullPath, destFullPath, sameDirectoryDifferentCase); } } }