From 33b665309eeacdfba7c7a7dd62b1034644b384e3 Mon Sep 17 00:00:00 2001 From: Jean-Claude Grenier Date: Thu, 27 Apr 2023 12:38:35 -0400 Subject: [PATCH 1/2] Added FileSystem.Unix.cs file to bugfix (bugfix in next commit for reviewer's convenience) --- .../src/System/IO/FileSystem.Unix.cs | 563 ++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs diff --git a/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs new file mode 100644 index 000000000000..94b45576433a --- /dev/null +++ b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs @@ -0,0 +1,563 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.IO +{ + /// Provides an implementation of FileSystem for Unix systems. + internal static partial class FileSystem + { + internal const int DefaultBufferSize = 4096; + + private static bool CopyDanglingSymlink(string sourceFullPath, string destFullPath) + { + // Check if the source is a dangling symlink. In those cases, we just want to copy the link + Interop.Sys.FileStatus ignored; + if (! (Interop.Sys.Stat(sourceFullPath, out ignored) < 0 && + Interop.Sys.LStat(sourceFullPath, out ignored) == 0)) + { + return false; + } + + Interop.ErrorInfo errorInfo; + // get the target of the symlink + string linkTarget = Interop.Sys.ReadLink(sourceFullPath); + if (linkTarget == null) + { + errorInfo = Interop.Sys.GetLastErrorInfo(); + throw Interop.GetExceptionForIoErrno(errorInfo, sourceFullPath); + } + + if (Interop.Sys.Symlink(linkTarget, destFullPath) == 0) + return true; + + errorInfo = Interop.Sys.GetLastErrorInfo(); + throw Interop.GetExceptionForIoErrno(errorInfo, destFullPath); + } + + public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite) + { + // The destination path may just be a directory into which the file should be copied. + // If it is, append the filename from the source onto the destination directory + if (DirectoryExists(destFullPath)) + { + destFullPath = Path.Combine(destFullPath, Path.GetFileName(sourceFullPath)); + } + + if (CopyDanglingSymlink(sourceFullPath, destFullPath)) + return; + + // Copy the contents of the file from the source to the destination, creating the destination in the process + using (var src = new FileStream(sourceFullPath, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, FileOptions.None)) + using (var dst = new FileStream(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, DefaultBufferSize, FileOptions.None)) + { + Interop.CheckIo(Interop.Sys.CopyFile(src.SafeFileHandle, dst.SafeFileHandle)); + } + } + + private static void LinkOrCopyFile (string sourceFullPath, string destFullPath) + { + if (CopyDanglingSymlink(sourceFullPath, destFullPath)) + return; + + if (Interop.Sys.Link(sourceFullPath, destFullPath) >= 0) + return; + + // If link fails, we can fall back to doing a full copy, but we'll only do so for + // cases where we expect link could fail but such a copy could succeed. We don't + // want to do so for all errors, because the copy could incur a lot of cost + // even if we know it'll eventually fail, e.g. EROFS means that the source file + // system is read-only and couldn't support the link being added, but if it's + // read-only, then the move should fail any way due to an inability to delete + // the source file. + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EXDEV || // rename fails across devices / mount points + errorInfo.Error == Interop.Error.EACCES || + errorInfo.Error == Interop.Error.EPERM || // permissions might not allow creating hard links even if a copy would work + errorInfo.Error == Interop.Error.EOPNOTSUPP || // links aren't supported by the source file system + errorInfo.Error == Interop.Error.EMLINK || // too many hard links to the source file + errorInfo.Error == Interop.Error.ENOSYS) // the file system doesn't support link + { + CopyFile(sourceFullPath, destFullPath, overwrite: false); + } + else + { + // The operation failed. Within reason, try to determine which path caused the problem + // so we can throw a detailed exception. + string path = null; + bool isDirectory = false; + if (errorInfo.Error == Interop.Error.ENOENT) + { + if (!Directory.Exists(Path.GetDirectoryName(destFullPath))) + { + // The parent directory of destFile can't be found. + // Windows distinguishes between whether the directory or the file isn't found, + // and throws a different exception in these cases. We attempt to approximate that + // here; there is a race condition here, where something could change between + // when the error occurs and our checks, but it's the best we can do, and the + // worst case in such a race condition (which could occur if the file system is + // being manipulated concurrently with these checks) is that we throw a + // FileNotFoundException instead of DirectoryNotFoundexception. + path = destFullPath; + isDirectory = true; + } + else + { + path = sourceFullPath; + } + } + else if (errorInfo.Error == Interop.Error.EEXIST) + { + path = destFullPath; + } + + throw Interop.GetExceptionForIoErrno(errorInfo, path, isDirectory); + } + } + + + public static void ReplaceFile(string sourceFullPath, string destFullPath, string destBackupFullPath, bool ignoreMetadataErrors) + { + if (destBackupFullPath != null) + { + // We're backing up the destination file to the backup file, so we need to first delete the backup + // file, if it exists. If deletion fails for a reason other than the file not existing, fail. + if (Interop.Sys.Unlink(destBackupFullPath) != 0) + { + Interop.ErrorInfo errno = Interop.Sys.GetLastErrorInfo(); + if (errno.Error != Interop.Error.ENOENT) + { + throw Interop.GetExceptionForIoErrno(errno, destBackupFullPath); + } + } + + // Now that the backup is gone, link the backup to point to the same file as destination. + // This way, we don't lose any data in the destination file, no copy is necessary, etc. + LinkOrCopyFile(destFullPath, destBackupFullPath); + } + else + { + // There is no backup file. Just make sure the destination file exists, throwing if it doesn't. + Interop.Sys.FileStatus ignored; + if (Interop.Sys.Stat(destFullPath, out ignored) != 0) + { + Interop.ErrorInfo errno = Interop.Sys.GetLastErrorInfo(); + if (errno.Error == Interop.Error.ENOENT) + { + throw Interop.GetExceptionForIoErrno(errno, destBackupFullPath); + } + } + } + + // Finally, rename the source to the destination, overwriting the destination. + Interop.CheckIo(Interop.Sys.Rename(sourceFullPath, destFullPath)); + } + + public static void MoveFile(string sourceFullPath, string destFullPath) + { + // The desired behavior for Move(source, dest) is to not overwrite the destination file + // if it exists. Since rename(source, dest) will replace the file at 'dest' if it exists, + // link/unlink are used instead. Rename is more efficient than link/unlink on file systems + // where hard links are not supported (such as FAT). Therefore, given that source file exists, + // rename is used in 2 cases: when dest file does not exist or when source path and dest + // path refer to the same file (on the same device). This is important for case-insensitive + // file systems (e.g. renaming a file in a way that just changes casing), so that we support + // changing the casing in the naming of the file. If this fails in any way (e.g. source file + // doesn't exist, dest file doesn't exist, rename fails, etc.), we just fall back to trying the + // link/unlink approach and generating any exceptional messages from there as necessary. + Interop.Sys.FileStatus sourceStat, destStat; + if (Interop.Sys.LStat(sourceFullPath, out sourceStat) == 0 && // source file exists + (Interop.Sys.LStat(destFullPath, out destStat) != 0 || // dest file does not exist + (sourceStat.Dev == destStat.Dev && // source and dest are on the same device + sourceStat.Ino == destStat.Ino)) && // source and dest are the same file on that device + Interop.Sys.Rename(sourceFullPath, destFullPath) == 0) // try the rename + { + // Renamed successfully. + return; + } + + LinkOrCopyFile(sourceFullPath, destFullPath); + DeleteFile(sourceFullPath); + } + + public static void DeleteFile(string fullPath) + { + if (Interop.Sys.Unlink(fullPath) < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + switch (errorInfo.Error) + { + case Interop.Error.ENOENT: + // ENOENT means it already doesn't exist; nop + return; + case Interop.Error.EROFS: + // EROFS means the file system is read-only + // Need to manually check file existence + // github.com/dotnet/corefx/issues/21273 + Interop.ErrorInfo fileExistsError; + + // Input allows trailing separators in order to match Windows behavior + // Unix does not accept trailing separators, so must be trimmed + if (!FileExists(PathInternal.TrimEndingDirectorySeparator(fullPath), + Interop.Sys.FileTypes.S_IFREG, out fileExistsError) && + fileExistsError.Error == Interop.Error.ENOENT) + { + return; + } + goto default; + case Interop.Error.EISDIR: + errorInfo = Interop.Error.EACCES.Info(); + goto default; + default: + throw Interop.GetExceptionForIoErrno(errorInfo, fullPath); + } + } + } + + public static void CreateDirectory(string fullPath) + { + // NOTE: This logic is primarily just carried forward from Win32FileSystem.CreateDirectory. + + int length = fullPath.Length; + + // We need to trim the trailing slash or the code will try to create 2 directories of the same name. + if (length >= 2 && PathInternal.EndsInDirectorySeparator(fullPath)) + { + length--; + } + + // For paths that are only // or /// + if (length == 2 && PathInternal.IsDirectorySeparator(fullPath[1])) + { + throw new IOException(SR.Format(SR.IO_CannotCreateDirectory, fullPath)); + } + + // We can save a bunch of work if the directory we want to create already exists. + if (DirectoryExists(fullPath)) + { + return; + } + + // Attempt to figure out which directories don't exist, and only create the ones we need. + bool somepathexists = false; + Stack stackDir = new Stack(); + int lengthRoot = PathInternal.GetRootLength(fullPath); + if (length > lengthRoot) + { + int i = length - 1; + while (i >= lengthRoot && !somepathexists) + { + string dir = fullPath.Substring(0, i + 1); + if (!DirectoryExists(dir)) // Create only the ones missing + { + stackDir.Push(dir); + } + else + { + somepathexists = true; + } + + while (i > lengthRoot && !PathInternal.IsDirectorySeparator(fullPath[i])) + { + i--; + } + i--; + } + } + + int count = stackDir.Count; + if (count == 0 && !somepathexists) + { + string root = Directory.InternalGetDirectoryRoot(fullPath); + if (!DirectoryExists(root)) + { + throw Interop.GetExceptionForIoErrno(Interop.Error.ENOENT.Info(), fullPath, isDirectory: true); + } + return; + } + + // Create all the directories + int result = 0; + Interop.ErrorInfo firstError = default(Interop.ErrorInfo); + string errorString = fullPath; + while (stackDir.Count > 0) + { + string name = stackDir.Pop(); + + // The mkdir command uses 0777 by default (it'll be AND'd with the process umask internally). + // We do the same. + result = Interop.Sys.MkDir(name, (int)Interop.Sys.Permissions.Mask); + if (result < 0 && firstError.Error == 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + + // While we tried to avoid creating directories that don't + // exist above, there are a few cases that can fail, e.g. + // a race condition where another process or thread creates + // the directory first, or there's a file at the location. + if (errorInfo.Error != Interop.Error.EEXIST) + { + firstError = errorInfo; + } + else if (FileExists(name) || (!DirectoryExists(name, out errorInfo) && errorInfo.Error == Interop.Error.EACCES)) + { + // If there's a file in this directory's place, or if we have ERROR_ACCESS_DENIED when checking if the directory already exists throw. + firstError = errorInfo; + errorString = name; + } + } + } + + // Only throw an exception if creating the exact directory we wanted failed to work correctly. + if (result < 0 && firstError.Error != 0) + { + throw Interop.GetExceptionForIoErrno(firstError, errorString, isDirectory: true); + } + } + + public static void MoveDirectory(string sourceFullPath, string destFullPath) + { + // Windows doesn't care if you try and copy a file via "MoveDirectory"... + if (FileExists(sourceFullPath)) + { + // ... 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 a IOException, if we let it go beyond here it would + // give DirectoryNotFound. + + if (PathInternal.EndsInDirectorySeparator(sourceFullPath)) + throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); + + // ... but it doesn't care if the destination has a trailing separator. + destFullPath = PathInternal.TrimEndingDirectorySeparator(destFullPath); + + // ... and dest cannot be an existing file. + if(FileExists(destFullPath)) + throw new IOException(SR.IO_FileCreateAlreadyExists); + } + + if (Interop.Sys.Rename(sourceFullPath, destFullPath) < 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); + default: + throw Interop.GetExceptionForIoErrno(errorInfo, sourceFullPath, isDirectory: true); + } + } + } + + public static void RemoveDirectory(string fullPath, bool recursive) + { + var di = new DirectoryInfo(fullPath); + if (!di.Exists) + { + throw Interop.GetExceptionForIoErrno(Interop.Error.ENOENT.Info(), fullPath, isDirectory: true); + } + RemoveDirectoryInternal(di, recursive, throwOnTopLevelDirectoryNotFound: true); + } + + private static void RemoveDirectoryInternal(DirectoryInfo directory, bool recursive, bool throwOnTopLevelDirectoryNotFound) + { + Exception firstException = null; + + if ((directory.Attributes & FileAttributes.ReparsePoint) != 0) + { + DeleteFile(directory.FullName); + return; + } + + if (recursive) + { + try + { + foreach (string item in Directory.EnumerateFileSystemEntries(directory.FullName)) + { + if (!ShouldIgnoreDirectory(Path.GetFileName(item))) + { + try + { + var childDirectory = new DirectoryInfo(item); + if (childDirectory.Exists) + { + RemoveDirectoryInternal(childDirectory, recursive, throwOnTopLevelDirectoryNotFound: false); + } + else + { + DeleteFile(item); + } + } + catch (Exception exc) + { + if (firstException != null) + { + firstException = exc; + } + } + } + } + } + catch (Exception exc) + { + if (firstException != null) + { + firstException = exc; + } + } + + if (firstException != null) + { + throw firstException; + } + } + + if (Interop.Sys.RmDir(directory.FullName) < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + switch (errorInfo.Error) + { + case Interop.Error.EACCES: + case Interop.Error.EPERM: + case Interop.Error.EROFS: + case Interop.Error.EISDIR: + throw new IOException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, directory.FullName)); // match Win32 exception + case Interop.Error.ENOENT: + if (!throwOnTopLevelDirectoryNotFound) + { + return; + } + goto default; + default: + throw Interop.GetExceptionForIoErrno(errorInfo, directory.FullName, isDirectory: true); + } + } + } + + public static bool DirectoryExists(ReadOnlySpan fullPath) + { + Interop.ErrorInfo ignored; + return DirectoryExists(fullPath, out ignored); + } + + private static bool DirectoryExists(ReadOnlySpan fullPath, out Interop.ErrorInfo errorInfo) + { + return FileExists(fullPath, Interop.Sys.FileTypes.S_IFDIR, out errorInfo); + } + + public static bool FileExists(ReadOnlySpan fullPath) + { + Interop.ErrorInfo ignored; + // File.Exists() explicitly checks for a trailing separator and returns false if found. FileInfo.Exists and all other + // internal usages do not check for the trailing separator. Historically we've always removed the trailing separator + // when getting attributes as trailing separators are generally not accepted by Windows APIs. Unix will take + // trailing separators, but it infers that the path must be a directory (it effectively appends "."). To align with + // our historical behavior (outside of File.Exists()), we need to trim. + // + // See http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 for details. + return FileExists(PathInternal.TrimEndingDirectorySeparator(fullPath), Interop.Sys.FileTypes.S_IFREG, out ignored); + } + + private static bool FileExists(ReadOnlySpan fullPath, int fileType, out Interop.ErrorInfo errorInfo) + { + Debug.Assert(fileType == Interop.Sys.FileTypes.S_IFREG || fileType == Interop.Sys.FileTypes.S_IFDIR); + + Interop.Sys.FileStatus fileinfo; + errorInfo = default(Interop.ErrorInfo); + + // First use stat, as we want to follow symlinks. If that fails, it could be because the symlink + // is broken, we don't have permissions, etc., in which case fall back to using LStat to evaluate + // based on the symlink itself. + if (Interop.Sys.Stat(fullPath, out fileinfo) < 0 && + Interop.Sys.LStat(fullPath, out fileinfo) < 0) + { + errorInfo = Interop.Sys.GetLastErrorInfo(); + return false; + } + + // Something exists at this path. If the caller is asking for a directory, return true if it's + // a directory and false for everything else. If the caller is asking for a file, return false for + // a directory and true for everything else. + return + (fileType == Interop.Sys.FileTypes.S_IFDIR) == + ((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR); + } + + /// Determines whether the specified directory name should be ignored. + /// The name to evaluate. + /// true if the name is "." or ".."; otherwise, false. + private static bool ShouldIgnoreDirectory(string name) + { + return name == "." || name == ".."; + } + + public static FileAttributes GetAttributes(string fullPath) + { + FileAttributes attributes = new FileInfo(fullPath, null).Attributes; + + if (attributes == (FileAttributes)(-1)) + FileSystemInfo.ThrowNotFound(fullPath); + + return attributes; + } + + public static void SetAttributes(string fullPath, FileAttributes attributes) + { + new FileInfo(fullPath, null).Attributes = attributes; + } + + public static DateTimeOffset GetCreationTime(string fullPath) + { + return new FileInfo(fullPath, null).CreationTime; + } + + public static void SetCreationTime(string fullPath, DateTimeOffset time, bool asDirectory) + { + FileSystemInfo info = asDirectory ? + (FileSystemInfo)new DirectoryInfo(fullPath, null) : + (FileSystemInfo)new FileInfo(fullPath, null); + + info.CreationTimeCore = time; + } + + public static DateTimeOffset GetLastAccessTime(string fullPath) + { + return new FileInfo(fullPath, null).LastAccessTime; + } + + public static void SetLastAccessTime(string fullPath, DateTimeOffset time, bool asDirectory) + { + FileSystemInfo info = asDirectory ? + (FileSystemInfo)new DirectoryInfo(fullPath, null) : + (FileSystemInfo)new FileInfo(fullPath, null); + + info.LastAccessTimeCore = time; + } + + public static DateTimeOffset GetLastWriteTime(string fullPath) + { + return new FileInfo(fullPath, null).LastWriteTime; + } + + public static void SetLastWriteTime(string fullPath, DateTimeOffset time, bool asDirectory) + { + FileSystemInfo info = asDirectory ? + (FileSystemInfo)new DirectoryInfo(fullPath, null) : + (FileSystemInfo)new FileInfo(fullPath, null); + + info.LastWriteTimeCore = time; + } + + public static string[] GetLogicalDrives() + { + return DriveInfoInternal.GetLogicalDrives(); + } + } +} From c7d5f4ce8427d1624e98867e61613c95386e46f2 Mon Sep 17 00:00:00 2001 From: Jean-Claude Grenier Date: Thu, 27 Apr 2023 12:41:30 -0400 Subject: [PATCH 2/2] CopyTo will now throw an UnauthorizedAccessException as stipulated in the upstream docs, on both osx and win --- .../src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs | 4 +--- .../System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs | 2 +- mcs/class/corlib/unix_build_corlib.dll.sources | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs index 94b45576433a..31f1e0e89b86 100644 --- a/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs +++ b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs @@ -40,11 +40,9 @@ private static bool CopyDanglingSymlink(string sourceFullPath, string destFullPa public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite) { - // The destination path may just be a directory into which the file should be copied. - // If it is, append the filename from the source onto the destination directory if (DirectoryExists(destFullPath)) { - destFullPath = Path.Combine(destFullPath, Path.GetFileName(sourceFullPath)); + throw new System.UnauthorizedAccessException(SR.Format(SR.Arg_FileIsDirectory_Name, destFullPath)); } if (CopyDanglingSymlink(sourceFullPath, destFullPath)) diff --git a/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs index 97aac4363905..459c68fdf86c 100644 --- a/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs +++ b/external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs @@ -38,7 +38,7 @@ public static void CopyFile(string sourceFullPath, string destFullPath, bool ove if (errorCode == Interop.Errors.ERROR_ACCESS_DENIED) { if (DirectoryExists(destFullPath)) - throw new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, destFullPath), Interop.Errors.ERROR_ACCESS_DENIED); + throw new System.UnauthorizedAccessException(SR.Format(SR.Arg_FileIsDirectory_Name, destFullPath)); } } diff --git a/mcs/class/corlib/unix_build_corlib.dll.sources b/mcs/class/corlib/unix_build_corlib.dll.sources index a3ebc60f09cb..99280b6fcf01 100644 --- a/mcs/class/corlib/unix_build_corlib.dll.sources +++ b/mcs/class/corlib/unix_build_corlib.dll.sources @@ -1,5 +1,5 @@ #include corlib.dll.sources -../../../external/corefx/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs +../../../external/corefx-bugfix/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs ../../../external/corefx/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs ../../../external/corefx/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs ../../../external/corefx/src/Common/src/CoreLib/Internal/IO/File.Unix.cs