diff --git a/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs b/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs new file mode 100644 index 00000000000000..d9807b427bf26d --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + // Unix max paths are typically 1K or 4K UTF-8 bytes, 256 should handle the majority of paths + // without putting too much pressure on the stack. + internal const int DefaultPathBufferSize = 256; +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs index 1faef8cc0be8dd..78da5a667310f1 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs @@ -14,18 +14,16 @@ internal static partial class Sys internal static unsafe string GetCwd() { - const int StackLimit = 256; - // First try to get the path into a buffer on the stack - byte* stackBuf = stackalloc byte[StackLimit]; - string? result = GetCwdHelper(stackBuf, StackLimit); + byte* stackBuf = stackalloc byte[DefaultPathBufferSize]; + string? result = GetCwdHelper(stackBuf, DefaultPathBufferSize); if (result != null) { return result; } // If that was too small, try increasing large buffer sizes - int bufferSize = StackLimit; + int bufferSize = DefaultPathBufferSize; while (true) { checked { bufferSize *= 2; } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs index 8f0f6a15fed95d..94f37d4ccc3f85 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Buffers; using System.Text; +using System; internal static partial class Interop { @@ -20,24 +21,31 @@ internal static partial class Sys /// Returns the number of bytes placed into the buffer on success; bufferSize if the buffer is too small; and -1 on error. /// [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadLink", SetLastError = true)] - private static extern int ReadLink(string path, byte[] buffer, int bufferSize); + private static extern int ReadLink(ref byte path, byte[] buffer, int bufferSize); /// /// Takes a path to a symbolic link and returns the link target path. /// - /// The path to the symlink - /// - /// Returns the link to the target path on success; and null otherwise. - /// - public static string? ReadLink(string path) + /// The path to the symlink. + /// Returns the link to the target path on success; and null otherwise. + internal static string? ReadLink(ReadOnlySpan path) { - int bufferSize = 256; + int outputBufferSize = 1024; + + // Use an initial buffer size that prevents disposing and renting + // a second time when calling ConvertAndTerminateString. + using var converter = new ValueUtf8Converter(stackalloc byte[1024]); + while (true) { - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + byte[] buffer = ArrayPool.Shared.Rent(outputBufferSize); try { - int resultLength = Interop.Sys.ReadLink(path, buffer, buffer.Length); + int resultLength = Interop.Sys.ReadLink( + ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), + buffer, + buffer.Length); + if (resultLength < 0) { // error @@ -54,8 +62,8 @@ internal static partial class Sys ArrayPool.Shared.Return(buffer); } - // buffer was too small, loop around again and try with a larger buffer. - bufferSize *= 2; + // Output buffer was too small, loop around again and try with a larger buffer. + outputBufferSize = buffer.Length * 2; } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs index 3c638cb60aa524..85028fd0fd088d 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs @@ -9,16 +9,12 @@ internal static partial class Interop { internal static partial class Sys { - // Unix max paths are typically 1K or 4K UTF-8 bytes, 256 should handle the majority of paths - // without putting too much pressure on the stack. - private const int StackBufferSize = 256; - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Stat", SetLastError = true)] internal static extern int Stat(ref byte path, out FileStatus output); internal static int Stat(ReadOnlySpan path, out FileStatus output) { - var converter = new ValueUtf8Converter(stackalloc byte[StackBufferSize]); + var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]); int result = Stat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output); converter.Dispose(); return result; @@ -29,7 +25,7 @@ internal static int Stat(ReadOnlySpan path, out FileStatus output) internal static int LStat(ReadOnlySpan path, out FileStatus output) { - var converter = new ValueUtf8Converter(stackalloc byte[StackBufferSize]); + var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]); int result = LStat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output); converter.Dispose(); return result; diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs new file mode 100644 index 00000000000000..922ecd5bc66255 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SymLink", SetLastError = true)] + internal static extern int SymLink(string target, string linkPath); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs index 338706ea8491bc..d5f6d1637507fa 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs @@ -91,5 +91,6 @@ internal static partial class Errors internal const int ERROR_EVENTLOG_FILE_CHANGED = 0x5DF; internal const int ERROR_TRUSTED_RELATIONSHIP_FAILURE = 0x6FD; internal const int ERROR_RESOURCE_LANG_NOT_FOUND = 0x717; + internal const int ERROR_NOT_A_REPARSE_POINT = 0x1126; } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs new file mode 100644 index 00000000000000..9ecd41c46bd6b3 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + /// + /// The link target is a directory. + /// + internal const int SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1; + + /// + /// Allows creation of symbolic links from a process that is not elevated. Requires Windows 10 Insiders build 14972 or later. + /// Developer Mode must first be enabled on the machine before this option will function. + /// + internal const int SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2; + + [DllImport(Libraries.Kernel32, EntryPoint = "CreateSymbolicLinkW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)] + private static extern bool CreateSymbolicLinkPrivate(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); + + /// + /// Creates a symbolic link. + /// + /// The symbolic link to be created. + /// The name of the target for the symbolic link to be created. + /// If it has a device name associated with it, the link is treated as an absolute link; otherwise, the link is treated as a relative link. + /// if the link target is a directory; otherwise. + internal static void CreateSymbolicLink(string symlinkFileName, string targetFileName, bool isDirectory) + { + string originalPath = symlinkFileName; + symlinkFileName = PathInternal.EnsureExtendedPrefixIfNeeded(symlinkFileName); + targetFileName = PathInternal.EnsureExtendedPrefixIfNeeded(targetFileName); + + int flags = 0; + + bool isAtLeastWin10Build14972 = + Environment.OSVersion.Version.Major == 10 && Environment.OSVersion.Version.Build >= 14972 || + Environment.OSVersion.Version.Major >= 11; + + if (isAtLeastWin10Build14972) + { + flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + } + + if (isDirectory) + { + flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + } + + bool success = CreateSymbolicLinkPrivate(symlinkFileName, targetFileName, flags); + + int error; + if (!success) + { + throw Win32Marshal.GetExceptionForLastWin32Error(originalPath); + } + // In older versions we need to check GetLastWin32Error regardless of the return value of CreateSymbolicLink, + // e.g: if the user doesn't have enough privileges to create a symlink the method returns success which we can consider as a silent failure. + else if (!isAtLeastWin10Build14972 && (error = Marshal.GetLastWin32Error()) != 0) + { + throw Win32Marshal.GetExceptionForWin32Error(error, originalPath); + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs new file mode 100644 index 00000000000000..be8def215178fb --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_get_reparse_point + internal const int FSCTL_GET_REPARSE_POINT = 0x000900a8; + + [DllImport(Libraries.Kernel32, EntryPoint = "DeviceIoControl", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)] + internal static extern bool DeviceIoControl( + SafeHandle hDevice, + uint dwIoControlCode, + IntPtr lpInBuffer, + uint nInBufferSize, + byte[] lpOutBuffer, + uint nOutBufferSize, + out uint lpBytesReturned, + IntPtr lpOverlapped); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs index f2a3872299d2c1..cc4896c1c52e48 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs @@ -9,6 +9,7 @@ internal static partial class IOReparseOptions { internal const uint IO_REPARSE_TAG_FILE_PLACEHOLDER = 0x80000015; internal const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + internal const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; } internal static partial class FileOperations @@ -18,6 +19,7 @@ internal static partial class FileOperations internal const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; internal const int FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; + internal const int FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000; internal const int FILE_FLAG_OVERLAPPED = 0x40000000; internal const int FILE_LIST_DIRECTORY = 0x0001; diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs new file mode 100644 index 00000000000000..756b1bbd72db12 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + internal const uint FILE_NAME_NORMALIZED = 0x0; + + // https://docs.microsoft.com/windows/desktop/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew (kernel32) + [DllImport(Libraries.Kernel32, EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)] + internal static unsafe extern uint GetFinalPathNameByHandle( + SafeFileHandle hFile, + char* lpszFilePath, + uint cchFilePath, + uint dwFlags); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs new file mode 100644 index 00000000000000..3bcb9162d57bfc --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://docs.microsoft.com/windows-hardware/drivers/ifs/fsctl-get-reparse-point + internal const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024; + + internal const uint SYMLINK_FLAG_RELATIVE = 1; + + // https://msdn.microsoft.com/library/windows/hardware/ff552012.aspx + // We don't need all the struct fields; omitting the rest. + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct REPARSE_DATA_BUFFER + { + internal uint ReparseTag; + internal ushort ReparseDataLength; + internal ushort Reserved; + internal SymbolicLinkReparseBuffer ReparseBufferSymbolicLink; + + [StructLayout(LayoutKind.Sequential)] + internal struct SymbolicLinkReparseBuffer + { + internal ushort SubstituteNameOffset; + internal ushort SubstituteNameLength; + internal ushort PrintNameOffset; + internal ushort PrintNameLength; + internal uint Flags; + } + } + } +} diff --git a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs index 60e7a0aa466f1d..ad087304b4e5ac 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs @@ -64,19 +64,7 @@ internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_F { errorCode = Marshal.GetLastWin32Error(); - if (errorCode != Interop.Errors.ERROR_FILE_NOT_FOUND - && errorCode != Interop.Errors.ERROR_PATH_NOT_FOUND - && errorCode != Interop.Errors.ERROR_NOT_READY - && errorCode != Interop.Errors.ERROR_INVALID_NAME - && errorCode != Interop.Errors.ERROR_BAD_PATHNAME - && errorCode != Interop.Errors.ERROR_BAD_NETPATH - && errorCode != Interop.Errors.ERROR_BAD_NET_NAME - && errorCode != Interop.Errors.ERROR_INVALID_PARAMETER - && errorCode != Interop.Errors.ERROR_NETWORK_UNREACHABLE - && errorCode != Interop.Errors.ERROR_NETWORK_ACCESS_DENIED - && errorCode != Interop.Errors.ERROR_INVALID_HANDLE // eg from \\.\CON - && errorCode != Interop.Errors.ERROR_FILENAME_EXCED_RANGE // Path is too long - ) + if (!IsPathUnreachableError(errorCode)) { // Assert so we can track down other cases (if any) to add to our test suite Debug.Assert(errorCode == Interop.Errors.ERROR_ACCESS_DENIED || errorCode == Interop.Errors.ERROR_SHARING_VIOLATION || errorCode == Interop.Errors.ERROR_SEM_TIMEOUT, @@ -127,5 +115,27 @@ internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_F return errorCode; } + + internal static bool IsPathUnreachableError(int errorCode) + { + switch (errorCode) + { + case Interop.Errors.ERROR_FILE_NOT_FOUND: + case Interop.Errors.ERROR_PATH_NOT_FOUND: + case Interop.Errors.ERROR_NOT_READY: + case Interop.Errors.ERROR_INVALID_NAME: + case Interop.Errors.ERROR_BAD_PATHNAME: + case Interop.Errors.ERROR_BAD_NETPATH: + case Interop.Errors.ERROR_BAD_NET_NAME: + case Interop.Errors.ERROR_INVALID_PARAMETER: + case Interop.Errors.ERROR_NETWORK_UNREACHABLE: + case Interop.Errors.ERROR_NETWORK_ACCESS_DENIED: + case Interop.Errors.ERROR_INVALID_HANDLE: // eg from \\.\CON + case Interop.Errors.ERROR_FILENAME_EXCED_RANGE: // Path is too long + return true; + default: + return false; + } + } } } diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs index 02ffa607c94aa7..a45aab12165a51 100644 --- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -127,5 +127,63 @@ private string GenerateTestFileName(int? index, string memberName, int lineNumbe lineNumber, index.GetValueOrDefault(), Guid.NewGuid().ToString("N").Substring(0, 8)); // randomness to avoid collisions between derived test classes using same base method concurrently + + /// + /// In some cases (such as when running without elevated privileges), + /// the symbolic link may fail to create. Only run this test if it creates + /// links successfully. + /// + protected static bool CanCreateSymbolicLinks => s_canCreateSymbolicLinks.Value; + + private static readonly Lazy s_canCreateSymbolicLinks = new Lazy(() => + { + bool success = true; + + // Verify file symlink creation + string path = Path.GetTempFileName(); + string linkPath = path + ".link"; + success = CreateSymLink(path, linkPath, isDirectory: false); + try { File.Delete(path); } catch { } + try { File.Delete(linkPath); } catch { } + + // Verify directory symlink creation + path = Path.GetTempFileName(); + linkPath = path + ".link"; + success = success && CreateSymLink(path, linkPath, isDirectory: true); + try { Directory.Delete(path); } catch { } + try { Directory.Delete(linkPath); } catch { } + + return success; + }); + + protected static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory) + { +#if NETFRAMEWORK + bool isWindows = true; +#else + if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsBrowser()) // OSes that don't support Process.Start() + { + return false; + } + bool isWindows = OperatingSystem.IsWindows(); +#endif + Process symLinkProcess = new Process(); + if (isWindows) + { + symLinkProcess.StartInfo.FileName = "cmd"; + symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath)); + } + else + { + symLinkProcess.StartInfo.FileName = "/bin/ln"; + symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath)); + } + symLinkProcess.StartInfo.RedirectStandardOutput = true; + symLinkProcess.Start(); + + symLinkProcess.WaitForExit(); + return (0 == symLinkProcess.ExitCode); + } + } } diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index db9472c1432f36..f22780b1a5e6e7 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -1,4 +1,4 @@ - + net472 $(DefineConstants);MS_IO_REDIST @@ -34,6 +34,8 @@ Link="Microsoft\IO\File.cs" /> + + + + + The file is too long. This operation is currently limited to supporting files less than 2 gigabytes in size. + + The link's file system entry type is inconsistent with that of its target: {0} + Could not find a part of the path. diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index b1b5a92e5f35b7..f39360810b57e8 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -81,6 +81,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_Access) DllImportEntry(SystemNative_LSeek) DllImportEntry(SystemNative_Link) + DllImportEntry(SystemNative_SymLink) DllImportEntry(SystemNative_MksTemps) DllImportEntry(SystemNative_MMap) DllImportEntry(SystemNative_MUnmap) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index d7eb6c4ab23aca..9e02aa7c2e65d4 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -715,6 +715,13 @@ int32_t SystemNative_Link(const char* source, const char* linkTarget) return result; } +int32_t SystemNative_SymLink(const char* target, const char* linkPath) +{ + int32_t result; + while ((result = symlink(target, linkPath)) < 0 && errno == EINTR); + return result; +} + intptr_t SystemNative_MksTemps(char* pathTemplate, int32_t suffixLength) { intptr_t result; diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index 1dc70387ad5eac..6461881a91eb1c 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -527,12 +527,19 @@ PALEXPORT int32_t SystemNative_Access(const char* path, int32_t mode); PALEXPORT int64_t SystemNative_LSeek(intptr_t fd, int64_t offset, int32_t whence); /** - * Creates a hard-link at link pointing to source. + * Creates a hard-link at linkTarget pointing to source. * * Returns 0 on success; otherwise, returns -1 and errno is set. */ PALEXPORT int32_t SystemNative_Link(const char* source, const char* linkTarget); +/** + * Creates a symbolic link at linkPath pointing to target. + * + * Returns 0 on success; otherwise, returns -1 and errno is set. + */ +PALEXPORT int32_t SystemNative_SymLink(const char* target, const char* linkPath); + /** * Creates a file name that adheres to the specified template, creates the file on disk with * 0600 permissions, and returns an open r/w File Descriptor on the file. diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 424a7a87b31c66..882b1beb85ffca 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -233,6 +233,8 @@ Link="Common\Interop\Unix\Interop.Errors.cs" /> + + + - /// In some cases (such as when running without elevated privileges), - /// the symbolic link may fail to create. Only run this test if it creates - /// links successfully. - /// - protected static bool CanCreateSymbolicLinks - { - get - { - bool success = true; - - // Verify file symlink creation - string path = Path.GetTempFileName(); - string linkPath = path + ".link"; - success = CreateSymLink(path, linkPath, isDirectory: false); - try { File.Delete(path); } catch { } - try { File.Delete(linkPath); } catch { } - - // Verify directory symlink creation - path = Path.GetTempFileName(); - linkPath = path + ".link"; - success = success && CreateSymLink(path, linkPath, isDirectory: true); - try { Directory.Delete(path); } catch { } - try { Directory.Delete(linkPath); } catch { } - - return success; - } - } - - public static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory) - { - if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start() - { - return false; - } - - Process symLinkProcess = new Process(); - if (OperatingSystem.IsWindows()) - { - symLinkProcess.StartInfo.FileName = "cmd"; - symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath)); - } - else - { - symLinkProcess.StartInfo.FileName = "/bin/ln"; - symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath)); - } - symLinkProcess.StartInfo.RedirectStandardOutput = true; - symLinkProcess.Start(); - - if (symLinkProcess != null) - { - symLinkProcess.WaitForExit(); - return (0 == symLinkProcess.ExitCode); - } - else - { - return false; - } - } - - public static IEnumerable FilterTypes() { foreach (NotifyFilters filter in Enum.GetValues(typeof(NotifyFilters))) diff --git a/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs deleted file mode 100644 index caa5ae9c3e11d5..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace System.IO.Tests -{ - public abstract class BaseSymbolicLinks : FileSystemTest - { - protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink() - { - DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); - string pathToLink = Path.Join(testDirectory.FullName, GetTestFileName()); - Assert.True(MountHelper.CreateSymbolicLink(pathToLink, pathToLink, isDirectory: true)); // Create a symlink cycle - return testDirectory; - } - } -} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs new file mode 100644 index 00000000000000..6f9a1ace40045f --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs @@ -0,0 +1,531 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace System.IO.Tests +{ + // Contains test methods that can be used for FileInfo, DirectoryInfo, File or Directory. + public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks + { + protected abstract bool IsDirectoryTest { get; } + + /// Creates a new file or directory depending on the implementing class. + /// If createOpposite is true, creates a directory if the implementing class is for File or FileInfo, or + /// creates a file if the implementing class is for Directory or DirectoryInfo. + protected abstract void CreateFileOrDirectory(string path, bool createOpposite = false); + + protected abstract void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo); + + protected abstract void AssertLinkExists(FileSystemInfo linkInfo); + + /// Calls the actual public API for creating a symbolic link. + protected abstract FileSystemInfo CreateSymbolicLink(string path, string pathToTarget); + + private void CreateSymbolicLink_Opposite(string path, string pathToTarget) + { + if (IsDirectoryTest) + { + File.CreateSymbolicLink(path, pathToTarget); + } + else + { + Directory.CreateSymbolicLink(path, pathToTarget); + } + } + + /// Calls the actual public API for resolving the symbolic link target. + protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget); + + [Fact] + public void CreateSymbolicLink_NullPathToTarget() + { + Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget: null)); + } + + [Theory] + [InlineData("")] + [InlineData("\0")] + public void CreateSymbolicLink_InvalidPathToTarget(string pathToTarget) + { + Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget)); + } + + [Fact] + public void CreateSymbolicLink_RelativeTargetPath_TargetExists() + { + // /path/to/link -> /path/to/existingtarget + + string linkPath = GetRandomLinkPath(); + string existentTarget = GetRandomFileName(); + string targetPath = Path.Join(Path.GetDirectoryName(linkPath), existentTarget); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: existentTarget, + targetPath: targetPath); + } + + [Fact] + public void CreateSymbolicLink_RelativeTargetPath_TargetExists_WithRedundantSegments() + { + // /path/to/link -> /path/to/../to/existingtarget + + string linkPath = GetRandomLinkPath(); + string fileName = GetRandomFileName(); + string dirPath = Path.GetDirectoryName(linkPath); + string dirName = Path.GetFileName(dirPath); + string targetPath = Path.Join(dirPath, fileName); + string existentTarget = Path.Join("..", dirName, fileName); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: existentTarget, + targetPath: targetPath); + } + + [Fact] + public void CreateSymbolicLink_AbsoluteTargetPath_TargetExists() + { + // /path/to/link -> /path/to/existingtarget + string linkPath = GetRandomLinkPath(); + string targetPath = GetRandomFilePath(); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: targetPath, + targetPath: targetPath); + } + + [Fact] + public void CreateSymbolicLink_AbsoluteTargetPath_TargetExists_WithRedundantSegments() + { + // /path/to/link -> /path/to/../to/existingtarget + + string linkPath = GetRandomLinkPath(); + string fileName = GetRandomFileName(); + string dirPath = Path.GetDirectoryName(linkPath); + string dirName = Path.GetFileName(dirPath); + string targetPath = Path.Join(dirPath, fileName); + string existentTarget = Path.Join(dirPath, "..", dirName, fileName); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: existentTarget, + targetPath: targetPath); + } + + [Fact] + public void CreateSymbolicLink_RelativeTargetPath_NonExistentTarget() + { + // /path/to/link -> /path/to/nonexistenttarget + + string linkPath = GetRandomLinkPath(); + string nonExistentTarget = GetRandomFileName(); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: nonExistentTarget, + targetPath: null); // do not create target + } + + [Fact] + public void CreateSymbolicLink_AbsoluteTargetPath_NonExistentTarget() + { + // /path/to/link -> /path/to/nonexistenttarget + + string linkPath = GetRandomLinkPath(); + string nonExistentTarget = GetRandomFilePath(); + VerifySymbolicLinkAndResolvedTarget( + linkPath: linkPath, + expectedLinkTarget: nonExistentTarget, + targetPath: null); // do not create target + } + + protected void ResolveLinkTarget_Throws_NotExists_Internal() where T : Exception + { + string path = GetRandomFilePath(); + Assert.Throws(() => ResolveLinkTarget(path, returnFinalTarget: false)); + Assert.Throws(() => ResolveLinkTarget(path, returnFinalTarget: true)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResolveLinkTarget_ReturnsNull_NotALink(bool returnFinalTarget) + { + string path = GetRandomFilePath(); + CreateFileOrDirectory(path); + Assert.Null(ResolveLinkTarget(path, returnFinalTarget)); + } + + [Theory] + [MemberData(nameof(ResolveLinkTarget_PathToTarget_Data))] + public void ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarget) + { + string linkPath = GetRandomLinkPath(); + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, pathToTarget); + AssertLinkExists(linkInfo); + AssertIsCorrectTypeAndDirectoryAttribute(linkInfo); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + + FileSystemInfo targetInfo = ResolveLinkTarget(linkPath, returnFinalTarget); + Assert.NotNull(targetInfo); + Assert.False(targetInfo.Exists); + + string expectedTargetFullName = Path.IsPathFullyQualified(pathToTarget) ? + pathToTarget : Path.GetFullPath(Path.Join(Path.GetDirectoryName(linkPath), pathToTarget)); + + Assert.Equal(expectedTargetFullName, targetInfo.FullName); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResolveLinkTarget_FileSystemEntryExistsButIsNotALink(bool returnFinalTarget) + { + string path = GetRandomFilePath(); + CreateFileOrDirectory(path); // entry exists as a normal file, not as a link + + FileSystemInfo target = ResolveLinkTarget(path, returnFinalTarget); + Assert.Null(target); + } + + [Fact] + public void ResolveLinkTarget_ReturnFinalTarget_Absolute() + { + string link1Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); + string filePath = GetRandomFilePath(); + + ResolveLinkTarget_ReturnFinalTarget( + link1Path: link1Path, + link1Target: link2Path, + link2Path: link2Path, + link2Target: filePath, + filePath: filePath); + } + + [Fact] + public void ResolveLinkTarget_ReturnFinalTarget_Absolute_WithRedundantSegments() + { + string link1Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); + string filePath = GetRandomFilePath(); + + string dirPath = Path.GetDirectoryName(filePath); + string dirName = Path.GetFileName(dirPath); + + string link2FileName = Path.GetFileName(link2Path); + string fileName = Path.GetFileName(filePath); + + ResolveLinkTarget_ReturnFinalTarget( + link1Path: link1Path, + link1Target: Path.Join(dirPath, "..", dirName, link2FileName), + link2Path: link2Path, + link2Target: Path.Join(dirPath, "..", dirName, fileName), + filePath: filePath); + } + + [Fact] + public void ResolveLinkTarget_ReturnFinalTarget_Relative() + { + string link1Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); + string filePath = GetRandomFilePath(); + + string link2FileName = Path.GetFileName(link2Path); + string fileName = Path.GetFileName(filePath); + + ResolveLinkTarget_ReturnFinalTarget( + link1Path: link1Path, + link1Target: link2FileName, + link2Path: link2Path, + link2Target: fileName, + filePath: filePath); + } + + [Fact] + public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() + { + string link1Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); + string filePath = GetRandomFilePath(); + + string dirPath = Path.GetDirectoryName(filePath); + string dirName = Path.GetFileName(dirPath); + + string link2FileName = Path.GetFileName(link2Path); + string fileName = Path.GetFileName(filePath); + + ResolveLinkTarget_ReturnFinalTarget( + link1Path: link1Path, + link1Target: Path.Join("..", dirName, link2FileName), + link2Path: link2Path, + link2Target: Path.Join("..", dirName, fileName), + filePath: filePath); + } + + [Theory] + [InlineData(1, false)] + [InlineData(10, false)] + [InlineData(20, false)] + [InlineData(1, true)] + [InlineData(10, true)] + [InlineData(20, true)] + public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) + { + string target = GetRandomFilePath(); + CreateFileOrDirectory(target); + + string tail = CreateChainOfLinks(target, length, relative); + FileSystemInfo targetInfo = ResolveLinkTarget(tail, returnFinalTarget: true); + Assert.Equal(target, targetInfo.FullName); + } + + [Theory] + // 100 is way beyond the limit (63 in Windows and 40 in Unix), we just want to make sure that a nice exception is thrown when its exceeded. + // We also don't want to test for a very precise limit given that it is very inconsistent across Windows versions. + [InlineData(100, false)] + [InlineData(100, true)] + public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_ExceedsLimit_Throws(int length, bool relative) + { + string target = GetRandomFilePath(); + CreateFileOrDirectory(target); + + string tail = CreateChainOfLinks(target, length, relative); + Assert.Throws(() => ResolveLinkTarget(tail, returnFinalTarget: true)); + } + + private string CreateChainOfLinks(string target, int length, bool relative) + { + string previousPath = target; + + for (int i = 0; i < length; i++) + { + string currentLinkPath = GetRandomLinkPath(); + CreateSymbolicLink(currentLinkPath, relative ? Path.GetFileName(previousPath) : previousPath); + previousPath = currentLinkPath; + } + + return previousPath; + } + + [Fact] + public void DetectSymbolicLinkCycle() + { + // link1 -> link2 -> link1 (cycle) + + string link2Path = GetRandomFilePath(); + string link1Path = GetRandomFilePath(); + + FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link2Path); + FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link1Path); + + // Can get targets without following symlinks + FileSystemInfo link1Target = ResolveLinkTarget(link1Path, returnFinalTarget: false); + FileSystemInfo link2Target = ResolveLinkTarget(link2Path, returnFinalTarget: false); + + // Cannot get target when following symlinks + Assert.Throws(() => ResolveLinkTarget(link1Path, returnFinalTarget: true)); + Assert.Throws(() => ResolveLinkTarget(link2Path, returnFinalTarget: true)); + } + + [Fact] + public void DetectLinkReferenceToSelf() + { + // link -> link (reference to itself) + + string linkPath = GetRandomFilePath(); + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, linkPath); + + // Can get target without following symlinks + FileSystemInfo linkTarget = ResolveLinkTarget(linkPath, returnFinalTarget: false); + + // Cannot get target when following symlinks + Assert.Throws(() => ResolveLinkTarget(linkPath, returnFinalTarget: true)); + } + + [Fact] + public void CreateSymbolicLink_WrongTargetType_Throws() + { + // dirLink -> file + // fileLink -> dir + + string targetPath = GetRandomFilePath(); + CreateFileOrDirectory(targetPath, createOpposite: true); // The underlying file system entry needs to be different + Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), targetPath)); + } + + [Fact] + public void CreateSymbolicLink_WrongTargetType_Indirect_Throws() + { + // link-2 (dir) -> link-1 (file) -> file + // link-2 (file) -> link-1 (dir) -> dir + string targetPath = GetRandomFilePath(); + string firstLinkPath = GetRandomFilePath(); + string secondLinkPath = GetRandomFilePath(); + + CreateFileOrDirectory(targetPath, createOpposite: true); + CreateSymbolicLink_Opposite(firstLinkPath, targetPath); + + Assert.Throws(() => CreateSymbolicLink(secondLinkPath, firstLinkPath)); + } + + [Fact] + public void CreateSymbolicLink_CorrectTargetType_Indirect_Succeeds() + { + // link-2 (file) -> link-1 (file) -> file + // link-2 (dir) -> link-1 (dir) -> dir + string targetPath = GetRandomFilePath(); + string firstLinkPath = GetRandomFilePath(); + string secondLinkPath = GetRandomFilePath(); + + CreateFileOrDirectory(targetPath, createOpposite: false); + CreateSymbolicLink(firstLinkPath, targetPath); + + FileSystemInfo secondLinkInfo = CreateSymbolicLink(secondLinkPath, firstLinkPath); + Assert.Equal(firstLinkPath, secondLinkInfo.LinkTarget); + Assert.Equal(targetPath, secondLinkInfo.ResolveLinkTarget(true).FullName); + } + + private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expectedLinkTarget, string targetPath = null) + { + // linkPath -> expectedLinkTarget (created in targetPath if not null) + + if (targetPath != null) + { + CreateFileOrDirectory(targetPath); + } + + FileSystemInfo link = CreateSymbolicLink(linkPath, expectedLinkTarget); + if (targetPath == null) + { + // Behavior different between files and directories when target does not exist + AssertLinkExists(link); + } + else + { + Assert.True(link.Exists); // The target file or directory was created above, so we report Exists of the target for both + } + + FileSystemInfo target = ResolveLinkTarget(linkPath, returnFinalTarget: false); + AssertIsCorrectTypeAndDirectoryAttribute(target); + Assert.True(Path.IsPathFullyQualified(target.FullName)); + } + + /// + /// Creates and Resolves a chain of links. + /// link1 -> link2 -> file + /// + private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1Target, string link2Path, string link2Target, string filePath) + { + Assert.True(Path.IsPathFullyQualified(link1Path)); + Assert.True(Path.IsPathFullyQualified(link2Path)); + Assert.True(Path.IsPathFullyQualified(filePath)); + + CreateFileOrDirectory(filePath); + + // link2 to file + FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link2Target); + Assert.True(link2Info.Exists); + Assert.True(link2Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertIsCorrectTypeAndDirectoryAttribute(link2Info); + Assert.Equal(link2Target, link2Info.LinkTarget); + + // link1 to link2 + FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link1Target); + Assert.True(link1Info.Exists); + Assert.True(link1Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertIsCorrectTypeAndDirectoryAttribute(link1Info); + Assert.Equal(link1Target, link1Info.LinkTarget); + + // link1: do not follow symlinks + FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path, returnFinalTarget: false); + Assert.True(link1TargetInfo.Exists); + AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo); + Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); + Assert.Equal(link2Path, link1TargetInfo.FullName); + Assert.Equal(link2Target, link1TargetInfo.LinkTarget); + + // link2: do not follow symlinks + FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path, returnFinalTarget: false); + Assert.True(link2TargetInfo.Exists); + AssertIsCorrectTypeAndDirectoryAttribute(link2TargetInfo); + Assert.False(link2TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); + Assert.Equal(filePath, link2TargetInfo.FullName); + Assert.Null(link2TargetInfo.LinkTarget); + + // link1: follow symlinks + FileSystemInfo finalTarget = ResolveLinkTarget(link1Path, returnFinalTarget: true); + Assert.True(finalTarget.Exists); + AssertIsCorrectTypeAndDirectoryAttribute(finalTarget); + Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint)); + Assert.Equal(filePath, finalTarget.FullName); + } + + protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool createOpposite) + { + string tempCwd = GetRandomDirPath(); + Directory.CreateDirectory(tempCwd); + Directory.SetCurrentDirectory(tempCwd); + + // Create a dummy file or directory in cwd. + string fileOrDirectoryInCwd = GetRandomFileName(); + CreateFileOrDirectory(fileOrDirectoryInCwd, createOpposite); + + string oneLevelUpPath = Path.Combine(tempCwd, "one-level-up"); + Directory.CreateDirectory(oneLevelUpPath); + string linkPath = Path.Combine(oneLevelUpPath, GetRandomLinkName()); + + // Create a link with a similar Target Path to the one of our dummy file or directory. + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, fileOrDirectoryInCwd); + FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(returnFinalTarget: false); + + // Verify that Target is resolved and is relative to Link's directory and not to the cwd. + Assert.False(targetInfo.Exists); + Assert.Equal(Path.GetDirectoryName(linkInfo.FullName), Path.GetDirectoryName(targetInfo.FullName)); + } + + public static IEnumerable ResolveLinkTarget_PathToTarget_Data + { + get + { + foreach (string path in PathToTargetData) + { + yield return new object[] { path, false }; + yield return new object[] { path, true }; + } + } + } + + internal static IEnumerable PathToTargetData + { + get + { + if (OperatingSystem.IsWindows()) + { + //Non-rooted relative + yield return "foo"; + yield return @".\foo"; + yield return @"..\foo"; + // Rooted relative + yield return @"\foo"; + // Rooted absolute + yield return Path.Combine(Path.GetTempPath(), "foo"); + // Extended DOS + yield return Path.Combine(@"\\?\", Path.GetTempPath(), "foo"); + // UNC + yield return @"\\SERVER\share\path"; + } + else + { + //Non-rooted relative + yield return "foo"; + yield return "./foo"; + yield return "../foo"; + // Rooted relative + yield return "/foo"; + // Rooted absolute + Path.Combine(Path.GetTempPath(), "foo"); + } + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs new file mode 100644 index 00000000000000..e82864248eb434 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using Xunit; + +namespace System.IO.Tests +{ + // Contains test methods that can be used for FileInfo and DirectoryInfo. + public abstract class BaseSymbolicLinks_FileSystemInfo : BaseSymbolicLinks_FileSystem + { + // Creates and returns FileSystemInfo instance by calling either the DirectoryInfo or FileInfo constructor and passing the path. + protected abstract FileSystemInfo GetFileSystemInfo(string path); + + protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) + { + FileSystemInfo link = GetFileSystemInfo(path); + link.CreateAsSymbolicLink(pathToTarget); + return link; + } + + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) + => GetFileSystemInfo(linkPath).ResolveLinkTarget(returnFinalTarget); + + private void Delete(string path) + { + if (IsDirectoryTest) + { + Directory.Delete(path); + } + else + { + File.Delete(path); + } + } + + [Fact] + public void LinkTarget_ReturnsNull_NotExists() + { + FileSystemInfo info = GetFileSystemInfo(GetRandomLinkPath()); + Assert.Null(info.LinkTarget); + } + + [Fact] + public void LinkTarget_ReturnsNull_NotALink() + { + string path = GetRandomFilePath(); + CreateFileOrDirectory(path); + FileSystemInfo info = GetFileSystemInfo(path); + + Assert.True(info.Exists); + Assert.Null(info.LinkTarget); + } + + [Theory] + [MemberData(nameof(LinkTarget_PathToTarget_Data))] + public void LinkTarget_Succeeds(string pathToTarget) + { + FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), pathToTarget); + + AssertLinkExists(linkInfo); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + } + + [Fact] + public void LinkTarget_RefreshesCorrectly() + { + string path = GetRandomLinkPath(); + string pathToTarget = GetRandomFilePath(); + CreateFileOrDirectory(pathToTarget); + FileSystemInfo linkInfo = CreateSymbolicLink(path, pathToTarget); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + + Delete(path); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + + linkInfo.Refresh(); + Assert.Null(linkInfo.LinkTarget); + + string newPathToTarget = GetRandomFilePath(); + CreateFileOrDirectory(newPathToTarget); + FileSystemInfo newLinkInfo = CreateSymbolicLink(path, newPathToTarget); + + linkInfo.Refresh(); + Assert.Equal(newPathToTarget, linkInfo.LinkTarget); + Assert.Equal(newLinkInfo.LinkTarget, linkInfo.LinkTarget); + } + + public static IEnumerable LinkTarget_PathToTarget_Data + { + get + { + foreach (string path in PathToTargetData) + { + yield return new object[] { path }; + } + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs new file mode 100644 index 00000000000000..8acc040a8cb695 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + private string GetTestDirectoryActualCasing() => TestDirectory; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs new file mode 100644 index 00000000000000..4572d48544fcf2 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + private const int OPEN_EXISTING = 3; + private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + + // Some Windows versions like Windows Nano Server have the %TEMP% environment variable set to "C:\TEMP" but the + // actual folder name is "C:\Temp", which prevents asserting path values using Assert.Equal due to case sensitiveness. + // So instead of using TestDirectory directly, we retrieve the real path with proper casing of the initial folder path. + private unsafe string GetTestDirectoryActualCasing() + { + try + { + using SafeFileHandle handle = Interop.Kernel32.CreateFile( + TestDirectory, + dwDesiredAccess: 0, + dwShareMode: FileShare.ReadWrite | FileShare.Delete, + dwCreationDisposition: FileMode.Open, + dwFlagsAndAttributes: + OPEN_EXISTING | + FILE_FLAG_BACKUP_SEMANTICS // Necessary to obtain a handle to a directory + ); + + if (!handle.IsInvalid) + { + const int InitialBufferSize = 4096; + char[]? buffer = ArrayPool.Shared.Rent(InitialBufferSize); + uint result = GetFinalPathNameByHandle(handle, buffer); + + // Remove extended prefix + int skip = PathInternal.IsExtended(buffer) ? 4 : 0; + + return new string( + buffer, + skip, + (int)result - skip); + } + } + catch { } + + return TestDirectory; + } + + private unsafe uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) + { + fixed (char* bufPtr = buffer) + { + return Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs new file mode 100644 index 00000000000000..d8869ff1581217 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ConditionalClass(typeof(BaseSymbolicLinks), nameof(CanCreateSymbolicLinks))] + // Contains helper methods that are shared by all symbolic link test classes. + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetRandomDirPath()); + string pathToLink = Path.Join(testDirectory.FullName, GetRandomDirName()); + Assert.True(MountHelper.CreateSymbolicLink(pathToLink, pathToLink, isDirectory: true)); // Create a symlink cycle + return testDirectory; + } + + protected string GetRandomFileName() => GetTestFileName() + ".txt"; + protected string GetRandomLinkName() => GetTestFileName() + ".link"; + protected string GetRandomDirName() => GetTestFileName() + "_dir"; + + protected string GetRandomFilePath() => Path.Join(ActualTestDirectory.Value, GetRandomFileName()); + protected string GetRandomLinkPath() => Path.Join(ActualTestDirectory.Value, GetRandomLinkName()); + protected string GetRandomDirPath() => Path.Join(ActualTestDirectory.Value, GetRandomDirName()); + + private Lazy ActualTestDirectory => new Lazy(() => GetTestDirectoryActualCasing()); + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs index 970c784c7f8198..d3d04ca2e54888 100644 --- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs @@ -1,15 +1,61 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Linq; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests { - public class Directory_SymbolicLinks : BaseSymbolicLinks + public class Directory_SymbolicLinks : BaseSymbolicLinks_FileSystem { - [ConditionalFact(nameof(CanCreateSymbolicLinks))] + protected override bool IsDirectoryTest => true; + + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) + { + if (!createOpposite) + { + Directory.CreateDirectory(path); + } + else + { + File.Create(path).Dispose(); + } + } + + protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => + Directory.CreateSymbolicLink(path, pathToTarget); + + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) => + Directory.ResolveLinkTarget(linkPath, returnFinalTarget); + + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) + { + if (linkInfo.Exists) + { + Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); + } + Assert.True(linkInfo is DirectoryInfo); + } + + protected override void AssertLinkExists(FileSystemInfo link) + { + if (PlatformDetection.IsWindows) + { + Assert.True(link.Exists); + } + else + { + // Unix implementation detail: + // When the directory target does not exist FileStatus.GetExists returns false because: + // - We check _exists (which whould be true because the link itself exists). + // - We check InitiallyDirectory, which is the initial expected object type (which would be true). + // - We check _directory (false because the target directory does not exist) + Assert.False(link.Exists); + } + } + + [Fact] public void EnumerateDirectories_LinksWithCycles_ShouldNotThrow() { DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink(); @@ -19,7 +65,7 @@ public void EnumerateDirectories_LinksWithCycles_ShouldNotThrow() Assert.Equal(expected, Directory.EnumerateDirectories(testDirectory.FullName).Count()); } - [ConditionalFact(nameof(CanCreateSymbolicLinks))] + [Fact] public void EnumerateFiles_LinksWithCycles_ShouldNotThrow() { DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink(); @@ -29,11 +75,22 @@ public void EnumerateFiles_LinksWithCycles_ShouldNotThrow() Assert.Equal(expected, Directory.EnumerateFiles(testDirectory.FullName).Count()); } - [ConditionalFact(nameof(CanCreateSymbolicLinks))] + [Fact] public void EnumerateFileSystemEntries_LinksWithCycles_ShouldNotThrow() { DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink(); Assert.Single(Directory.EnumerateFileSystemEntries(testDirectory.FullName)); } + + [Fact] + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs index 5dedc0b7b45285..e7ee5d0c6727c5 100644 --- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs @@ -1,15 +1,58 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Linq; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests { - public class DirectoryInfo_SymbolicLinks : BaseSymbolicLinks + public class DirectoryInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo { - [ConditionalTheory(nameof(CanCreateSymbolicLinks))] + protected override bool IsDirectoryTest => true; + + protected override FileSystemInfo GetFileSystemInfo(string path) => + new DirectoryInfo(path); + + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) + { + if (!createOpposite) + { + Directory.CreateDirectory(path); + } + else + { + File.Create(path).Dispose(); + } + } + + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) + { + if (linkInfo.Exists) + { + Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); + } + Assert.True(linkInfo is DirectoryInfo); + } + + protected override void AssertLinkExists(FileSystemInfo link) + { + if (PlatformDetection.IsWindows) + { + Assert.True(link.Exists); + } + else + { + // Unix implementation detail: + // When the directory target does not exist FileStatus.GetExists returns false because: + // - We check _exists (which whould be true because the link itself exists). + // - We check InitiallyDirectory, which is the initial expected object type (which would be true). + // - We check _directory (false because the target directory does not exist) + Assert.False(link.Exists); + } + } + + [Theory] [InlineData(false)] [InlineData(true)] public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse) @@ -31,7 +74,7 @@ public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLi } } - [ConditionalTheory(nameof(CanCreateSymbolicLinks))] + [Theory] [InlineData(false)] [InlineData(true)] public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse) @@ -53,7 +96,7 @@ public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bo } } - [ConditionalTheory(nameof(CanCreateSymbolicLinks))] + [Theory] [InlineData(false)] [InlineData(true)] public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse) @@ -74,5 +117,16 @@ public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbol Assert.Throws(() => testDirectory.GetFileSystemInfos("*", options).Count()); } } + + [Fact] + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs new file mode 100644 index 00000000000000..4c41fa7bb16576 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.IO.Tests +{ + public class File_SymbolicLinks : BaseSymbolicLinks_FileSystem + { + protected override bool IsDirectoryTest => false; + + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) + { + if (!createOpposite) + { + File.Create(path).Dispose(); + } + else + { + Directory.CreateDirectory(path); + } + } + + protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => + File.CreateSymbolicLink(path, pathToTarget); + + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) => + File.ResolveLinkTarget(linkPath, returnFinalTarget); + + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) + { + if (linkInfo.Exists) + { + Assert.False(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); + } + Assert.True(linkInfo is FileInfo); + } + + protected override void AssertLinkExists(FileSystemInfo link) => + Assert.True(link.Exists); + + [Fact] + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs new file mode 100644 index 00000000000000..fa334dd1ca376d --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.IO.Tests +{ + public class FileInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo + { + protected override bool IsDirectoryTest => false; + + protected override FileSystemInfo GetFileSystemInfo(string path) => + new FileInfo(path); + + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) + { + if (!createOpposite) + { + File.Create(path).Dispose(); + } + else + { + Directory.CreateDirectory(path); + } + } + + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) + { + if (linkInfo.Exists) + { + Assert.False(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); + } + Assert.True(linkInfo is FileInfo); + } + + protected override void AssertLinkExists(FileSystemInfo link) => + Assert.True(link.Exists); + + [Fact] + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs index d82346f6f4f10a..9300660f1418a2 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs @@ -55,32 +55,6 @@ public static TheoryData TrailingSeparators } } - /// - /// In some cases (such as when running without elevated privileges), - /// the symbolic link may fail to create. Only run this test if it creates - /// links successfully. - /// - protected static bool CanCreateSymbolicLinks => s_canCreateSymbolicLinks.Value; - - private static readonly Lazy s_canCreateSymbolicLinks = new Lazy(() => - { - // Verify file symlink creation - string path = Path.GetTempFileName(); - string linkPath = path + ".link"; - bool success = MountHelper.CreateSymbolicLink(linkPath, path, isDirectory: false); - try { File.Delete(path); } catch { } - try { File.Delete(linkPath); } catch { } - - // Verify directory symlink creation - path = Path.GetTempFileName(); - linkPath = path + ".link"; - success = success && MountHelper.CreateSymbolicLink(linkPath, path, isDirectory: true); - try { Directory.Delete(path); } catch { } - try { Directory.Delete(linkPath); } catch { } - - return success; - }); - public static string GetNamedPipeServerStreamName() { if (PlatformDetection.IsInAppContainer) diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index e9ab0a7365e303..dc418e5dfdb7cc 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -23,10 +23,16 @@ + + + + + + diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 91135c4439c971..0decd976d8e537 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -14,13 +14,16 @@ - + + + + @@ -64,12 +67,14 @@ + + @@ -78,13 +83,20 @@ + + + + + + + @@ -137,6 +149,7 @@ + diff --git a/src/libraries/System.Net.Ping/src/System.Net.Ping.csproj b/src/libraries/System.Net.Ping/src/System.Net.Ping.csproj index d3fd26f7079675..d7becfa7963ded 100644 --- a/src/libraries/System.Net.Ping/src/System.Net.Ping.csproj +++ b/src/libraries/System.Net.Ping/src/System.Net.Ping.csproj @@ -46,6 +46,8 @@ + + diff --git a/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj b/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj index 86a7dacccaefdf..90acc7605d8ab8 100644 --- a/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj +++ b/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj @@ -19,6 +19,8 @@ Link="SocketCommon\Configuration.cs" /> + + diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 05cc719ed051d9..ce9ef00df0373f 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2659,6 +2659,9 @@ BindHandle for ThreadPool failed on this handle. + + The link's file system entry type is inconsistent with that of its target: {0} + The file '{0}' already exists. @@ -2710,6 +2713,9 @@ The path '{0}' is too long, or a component of the specified path is too long. + + Too many levels of symbolic links in '{0}'. + [Unknown] diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index f344b005f933aa..f8126d9516352a 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -422,6 +422,7 @@ + @@ -1426,9 +1427,15 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile_IntPtr.cs + + Common\Interop\Windows\Kernel32\Interop.CreateSymbolicLink.cs + Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs + + Common\Interop\Windows\Kernel32\Interop.DeviceIoControl.cs + Common\Interop\Windows\Kernel32\Interop.ExpandEnvironmentStrings.cs @@ -1507,6 +1514,9 @@ Common\Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs + + Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs + Common\Interop\Windows\Kernel32\Interop.GetFullPathNameW.cs @@ -1618,6 +1628,9 @@ Common\Interop\Windows\Kernel32\Interop.RemoveDirectory.cs + + Common\Interop\Windows\Kernel32\Interop.REPARSE_DATA_BUFFER.cs + Common\Interop\Windows\Kernel32\Interop.ReplaceFile.cs @@ -1892,6 +1905,9 @@ Common\Interop\Unix\Interop.Libraries.cs + + Common\Interop\Unix\Interop.DefaultPathBufferSize.cs + Common\Interop\Unix\System.Native\Interop.Access.cs @@ -2033,6 +2049,9 @@ Common\Interop\Unix\System.Native\Interop.Stat.Span.cs + + Common\Interop\Unix\System.Native\Interop.SymLink.cs + Common\Interop\Unix\System.Native\Interop.SysConf.cs 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 3fbf585db5de12..f6bc4f42b064d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -315,5 +315,47 @@ public static string[] GetLogicalDrives() { return FileSystem.GetLogicalDrives(); } + + /// + /// Creates a directory symbolic link identified by that points to . + /// + /// The absolute path where the symbolic link should be created. + /// The target directory of the symbolic link. + /// A instance that wraps the newly created directory symbolic link. + /// or is . + /// or is empty. + /// -or- + /// is not an absolute path. + /// -or- + /// or contains invalid path characters. + /// A file or directory already exists in the location of . + /// -or- + /// An I/O error occurred. + public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) + { + string fullPath = Path.GetFullPath(path); + FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + + FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: true); + return new DirectoryInfo(originalPath: path, fullPath: fullPath, isNormalized: true); + } + + /// + /// Gets the target of the specified directory link. + /// + /// The path of the directory link. + /// to follow links to the final target; to return the immediate next link. + /// A instance if exists, independently if the target exists or not. if is not a link. + /// The directory on does not exist. + /// -or- + /// The link's file system entry type is inconsistent with that of its target. + /// -or- + /// Too many levels of symbolic links. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. + public static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + FileSystem.VerifyValidPath(linkPath, nameof(linkPath)); + return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: true); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 626dc761e807b7..8480e46a094d3e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1007,5 +1007,45 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont ? Task.FromCanceled(cancellationToken) : InternalWriteAllLinesAsync(AsyncStreamWriter(path, encoding, append: true), contents, cancellationToken); } + + /// + /// Creates a file symbolic link identified by that points to . + /// + /// The path where the symbolic link should be created. + /// The path of the target to which the symbolic link points. + /// A instance that wraps the newly created file symbolic link. + /// or is . + /// or is empty. + /// -or- + /// or contains a null character. + /// A file or directory already exists in the location of . + /// -or- + /// An I/O error occurred. + public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) + { + string fullPath = Path.GetFullPath(path); + FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + + FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: false); + return new FileInfo(originalPath: path, fullPath: fullPath, isNormalized: true); + } + + /// + /// Gets the target of the specified file link. + /// + /// The path of the file link. + /// to follow links to the final target; to return the immediate next link. + /// A instance if exists, independently if the target exists or not. if is not a link. + /// The file on does not exist. + /// -or- + /// The link's file system entry type is inconsistent with that of its target. + /// -or- + /// Too many levels of symbolic links. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. + public static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + FileSystem.VerifyValidPath(linkPath, nameof(linkPath)); + return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false); + } } } 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 9a759e7b2b06c3..0a4ef7cdef5fb8 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Text; namespace System.IO { @@ -11,6 +12,10 @@ internal static partial class FileSystem { internal const int DefaultBufferSize = 4096; + // On Linux, the maximum number of symbolic links that are followed while resolving a pathname is 40. + // See: https://man7.org/linux/man-pages/man7/path_resolution.7.html + private const int MaxFollowedLinks = 40; + public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite) { // If the destination path points to a directory, we throw to match Windows behaviour @@ -529,5 +534,91 @@ public static string[] GetLogicalDrives() { return DriveInfoInternal.GetLogicalDrives(); } + + internal static string? GetLinkTarget(ReadOnlySpan linkPath, bool isDirectory) => Interop.Sys.ReadLink(linkPath); + + internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory) + { + string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget); + + // Fail if the target exists but is not consistent with the expected filesystem entry type + if (Interop.Sys.Stat(pathToTargetFullPath, out Interop.Sys.FileStatus targetInfo) == 0) + { + if (isDirectory != ((targetInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)) + { + throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); + } + } + + Interop.CheckIo(Interop.Sys.SymLink(pathToTarget, path), path, isDirectory); + } + + internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) + { + ValueStringBuilder sb = new(Interop.DefaultPathBufferSize); + sb.Append(linkPath); + + string? linkTarget = GetLinkTarget(linkPath, isDirectory: false /* Irrelevant in Unix */); + if (linkTarget == null) + { + sb.Dispose(); + Interop.Error error = Interop.Sys.GetLastError(); + // Not a link, return null + if (error == Interop.Error.EINVAL) + { + return null; + } + + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(error), linkPath, isDirectory); + } + + if (!returnFinalTarget) + { + GetLinkTargetFullPath(ref sb, linkTarget); + } + else + { + string? current = linkTarget; + int visitCount = 1; + + while (current != null) + { + if (visitCount > MaxFollowedLinks) + { + sb.Dispose(); + // We went over the limit and couldn't reach the final target + throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath)); + } + + GetLinkTargetFullPath(ref sb, current); + current = GetLinkTarget(sb.AsSpan(), isDirectory: false); + visitCount++; + } + } + + Debug.Assert(sb.Length > 0); + linkTarget = sb.ToString(); // ToString disposes + + return isDirectory ? + new DirectoryInfo(linkTarget) : + new FileInfo(linkTarget); + + // In case of link target being relative: + // Preserve the full path of the directory of the previous path + // so the final target is returned with a valid full path + static void GetLinkTargetFullPath(ref ValueStringBuilder sb, ReadOnlySpan linkTarget) + { + if (PathInternal.IsPartiallyQualified(linkTarget)) + { + sb.Length = Path.GetDirectoryNameOffset(sb.AsSpan()); + sb.Append(PathInternal.DirectorySeparatorChar); + } + else + { + sb.Length = 0; + } + sb.Append(linkTarget); + } + } } } 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 5b5f9a86bc7446..5f88f53a7c539f 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 @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.IO; using System.Text; +using System.Buffers; #if MS_IO_REDIST namespace Microsoft.IO @@ -185,7 +186,7 @@ public static void RemoveDirectory(string fullPath, bool recursive) } Interop.Kernel32.WIN32_FIND_DATA findData = default; - GetFindData(fullPath, ref findData); + GetFindData(fullPath, isDirectory: true, ref findData); if (IsNameSurrogateReparsePoint(ref findData)) { // Don't recurse @@ -199,18 +200,16 @@ public static void RemoveDirectory(string fullPath, bool recursive) RemoveDirectoryRecursive(fullPath, ref findData, topLevel: true); } - private static void GetFindData(string fullPath, ref Interop.Kernel32.WIN32_FIND_DATA findData) + private static void GetFindData(string fullPath, bool isDirectory, ref Interop.Kernel32.WIN32_FIND_DATA findData) { - using (SafeFindHandle handle = Interop.Kernel32.FindFirstFile(Path.TrimEndingDirectorySeparator(fullPath), ref findData)) + using SafeFindHandle handle = Interop.Kernel32.FindFirstFile(Path.TrimEndingDirectorySeparator(fullPath), ref findData); + if (handle.IsInvalid) { - if (handle.IsInvalid) - { - int errorCode = Marshal.GetLastWin32Error(); - // File not found doesn't make much sense coming from a directory delete. - if (errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND) - errorCode = Interop.Errors.ERROR_PATH_NOT_FOUND; - throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath); - } + int errorCode = Marshal.GetLastWin32Error(); + // File not found doesn't make much sense coming from a directory. + if (isDirectory && errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND) + errorCode = Interop.Errors.ERROR_PATH_NOT_FOUND; + throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath); } } @@ -407,5 +406,225 @@ public static void SetLastWriteTime(string fullPath, DateTimeOffset time, bool a public static string[] GetLogicalDrives() => DriveInfoInternal.GetLogicalDrives(); + + internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory) + { + string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget); + + Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; + int errorCode = FillAttributeInfo(pathToTargetFullPath, ref data, returnErrorOnNotFound: true); + if (errorCode == Interop.Errors.ERROR_SUCCESS && + data.dwFileAttributes != -1 && + isDirectory != ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)) + { + throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); + } + + Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory); + } + + internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) + { + string? targetPath = returnFinalTarget ? + GetFinalLinkTarget(linkPath, isDirectory) : + GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: true, returnFullPath: true); + + return targetPath == null ? null : + isDirectory ? new DirectoryInfo(targetPath) : new FileInfo(targetPath); + } + + internal static string? GetLinkTarget(string linkPath, bool isDirectory) + => GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, returnFullPath: false); + + /// + /// Gets reparse point information associated to . + /// + /// The immediate link target, absolute or relative or null if the file is not a supported link. + internal static unsafe string? GetImmediateLinkTarget(string linkPath, bool isDirectory, bool throwOnUnreachable, bool returnFullPath) + { + using SafeFileHandle handle = OpenSafeFileHandle(linkPath, + Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS | + Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT); + + if (handle.IsInvalid) + { + int error = Marshal.GetLastWin32Error(); + + if (!throwOnUnreachable && IsPathUnreachableError(error)) + { + return null; + } + + // File not found doesn't make much sense coming from a directory. + if (isDirectory && error == Interop.Errors.ERROR_FILE_NOT_FOUND) + { + error = Interop.Errors.ERROR_PATH_NOT_FOUND; + } + + throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); + } + + byte[] buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + try + { + bool success = Interop.Kernel32.DeviceIoControl( + handle, + dwIoControlCode: Interop.Kernel32.FSCTL_GET_REPARSE_POINT, + lpInBuffer: IntPtr.Zero, + nInBufferSize: 0, + lpOutBuffer: buffer, + nOutBufferSize: Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE, + out _, + IntPtr.Zero); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + // The file or directory is not a reparse point. + if (error == Interop.Errors.ERROR_NOT_A_REPARSE_POINT) + { + return null; + } + + throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); + } + + Span bufferSpan = new(buffer); + success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.REPARSE_DATA_BUFFER rdb); + Debug.Assert(success); + + // Only symbolic links are supported at the moment. + if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) + { + return null; + } + + // We use PrintName instead of SubstitutneName given that we don't want to return a NT path when the link wasn't created with such NT path. + // Unlike SubstituteName and GetFinalPathNameByHandle(), PrintName doesn't start with a prefix. + // Another nuance is that SubstituteName does not contain redundant path segments while PrintName does. + // PrintName can ONLY return a NT path if the link was created explicitly targeting a file/folder in such way. e.g: mklink /D linkName \??\C:\path\to\target. + int printNameNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.PrintNameOffset; + int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength; + + Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); + Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath)); + + if (returnFullPath && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) + { + // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory. + return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath); + } + + return targetPath.ToString(); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static unsafe string? GetFinalLinkTarget(string linkPath, bool isDirectory) + { + Interop.Kernel32.WIN32_FIND_DATA data = default; + GetFindData(linkPath, isDirectory, ref data); + + // The file or directory is not a reparse point. + if ((data.dwFileAttributes & (uint)FileAttributes.ReparsePoint) == 0 || + // Only symbolic links are supported at the moment. + (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) + { + return null; + } + + // We try to open the final file since they asked for the final target. + using SafeFileHandle handle = OpenSafeFileHandle(linkPath, + Interop.Kernel32.FileOperations.OPEN_EXISTING | + Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); + + if (handle.IsInvalid) + { + // If the handle fails because it is unreachable, is because the link was broken. + // We need to fallback to manually traverse the links and return the target of the last resolved link. + int error = Marshal.GetLastWin32Error(); + if (IsPathUnreachableError(error)) + { + return GetFinalLinkTargetSlow(linkPath); + } + + throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); + } + + const int InitialBufferSize = 4096; + char[] buffer = ArrayPool.Shared.Rent(InitialBufferSize); + try + { + uint result = GetFinalPathNameByHandle(handle, buffer); + + // If the function fails because lpszFilePath is too small to hold the string plus the terminating null character, + // the return value is the required buffer size, in TCHARs. This value includes the size of the terminating null character. + if (result > buffer.Length) + { + ArrayPool.Shared.Return(buffer); + buffer = ArrayPool.Shared.Rent((int)result); + + result = GetFinalPathNameByHandle(handle, buffer); + } + + // If the function fails for any other reason, the return value is zero. + if (result == 0) + { + throw Win32Marshal.GetExceptionForLastWin32Error(linkPath); + } + + Debug.Assert(PathInternal.IsExtended(new string(buffer, 0, (int)result).AsSpan())); + // GetFinalPathNameByHandle always returns with extended DOS prefix even if the link target was created without one. + // While this does not interfere with correct behavior, it might be unexpected. + // Hence we trim it if the passed-in path to the link wasn't extended. + int start = PathInternal.IsExtended(linkPath.AsSpan()) ? 0 : 4; + return new string(buffer, start, (int)result - start); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) + { + fixed (char* bufPtr = buffer) + { + return Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + } + + string? GetFinalLinkTargetSlow(string linkPath) + { + // Since all these paths will be passed to CreateFile, which takes a string anyway, it is pointless to use span. + // I am not sure if it's possible to change CreateFile's param to ROS and avoid all these allocations. + string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, returnFullPath: true); + string? prev = null; + + while (current != null) + { + prev = current; + current = GetImmediateLinkTarget(current, isDirectory, throwOnUnreachable: false, returnFullPath: true); + } + + return prev; + } + } + + private static unsafe SafeFileHandle OpenSafeFileHandle(string path, int flags) + { + SafeFileHandle handle = Interop.Kernel32.CreateFile( + path, + dwDesiredAccess: 0, + FileShare.ReadWrite | FileShare.Delete, + lpSecurityAttributes: (Interop.Kernel32.SECURITY_ATTRIBUTES*)IntPtr.Zero, + FileMode.Open, + dwFlagsAndAttributes: flags, + hTemplateFile: IntPtr.Zero); + + return handle; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs new file mode 100644 index 00000000000000..a1d506e9218c86 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if MS_IO_REDIST +using System; + +namespace Microsoft.IO +#else +namespace System.IO +#endif +{ + internal static partial class FileSystem + { + internal static void VerifyValidPath(string path, string argName) + { + if (path == null) + { + throw new ArgumentNullException(argName); + } + else if (path.Length == 0) + { + throw new ArgumentException(SR.Arg_PathEmpty, argName); + } + else if (path.Contains('\0')) + { + throw new ArgumentException(SR.Argument_InvalidPathChars, argName); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs index 9bcae393120a8b..d59c90f23104ff 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs @@ -27,7 +27,7 @@ internal static FileSystemInfo Create(string fullPath, string fileName, ref File return info; } - internal void Invalidate() => _fileStatus.InvalidateCaches(); + internal void InvalidateCore() => _fileStatus.InvalidateCaches(); internal unsafe void Init(ref FileStatus fileStatus) { @@ -63,7 +63,11 @@ internal DateTimeOffset LastWriteTimeCore internal long LengthCore => _fileStatus.GetLength(FullPath); - public void Refresh() => _fileStatus.RefreshCaches(FullPath); + public void Refresh() + { + _linkTargetIsValid = false; + _fileStatus.RefreshCaches(FullPath); + } internal static void ThrowNotFound(string path) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs index 47d064e8fe1735..bb1440851733cf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs @@ -42,10 +42,7 @@ internal static unsafe FileSystemInfo Create(string fullPath, ref FileSystemEntr return info; } - internal void Invalidate() - { - _dataInitialized = -1; - } + internal void InvalidateCore() => _dataInitialized = -1; internal unsafe void Init(Interop.NtDll.FILE_FULL_DIR_INFORMATION* info) { @@ -77,7 +74,7 @@ internal bool ExistsCore get { if (_dataInitialized == -1) - Refresh(); + RefreshCore(); if (_dataInitialized != 0) { // Refresh was unable to initialize the data. @@ -145,7 +142,7 @@ private void EnsureDataInitialized() if (_dataInitialized == -1) { _data = default; - Refresh(); + RefreshCore(); } if (_dataInitialized != 0) // Refresh was unable to initialize the data @@ -153,6 +150,12 @@ private void EnsureDataInitialized() } public void Refresh() + { + _linkTargetIsValid = false; + RefreshCore(); + } + + private void RefreshCore() { // This should not throw, instead we store the result so that we can throw it // when someone actually accesses a property diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs index 98bf36b6fd0d6c..3beed9e25635e2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs @@ -19,11 +19,20 @@ public abstract partial class FileSystemInfo : MarshalByRefObject, ISerializable internal string _name = null!; // Fields initiated in derived classes + private string? _linkTarget; + private bool _linkTargetIsValid; + protected FileSystemInfo(SerializationInfo info, StreamingContext context) { throw new PlatformNotSupportedException(); } + internal void Invalidate() + { + _linkTargetIsValid = false; + InvalidateCore(); + } + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { throw new PlatformNotSupportedException(); @@ -107,6 +116,59 @@ public DateTime LastWriteTimeUtc set => LastWriteTimeCore = File.GetUtcDateTimeOffset(value); } + /// + /// If this instance represents a link, returns the link target's path. + /// If a link does not exist in , or this instance does not represent a link, returns . + /// + public string? LinkTarget + { + get + { + if (_linkTargetIsValid) + { + return _linkTarget; + } + + _linkTarget = FileSystem.GetLinkTarget(FullPath, this is DirectoryInfo); + _linkTargetIsValid = true; + return _linkTarget; + } + } + + /// + /// Creates a symbolic link located in that points to the specified . + /// + /// The path of the symbolic link target. + /// is . + /// is empty. + /// -or- + /// This instance was not created passing an absolute path. + /// -or- + /// contains invalid path characters. + /// A file or directory already exists in the location of . + /// -or- + /// An I/O error occurred. + public void CreateAsSymbolicLink(string pathToTarget) + { + FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + FileSystem.CreateSymbolicLink(OriginalPath, pathToTarget, this is DirectoryInfo); + Invalidate(); + } + + /// + /// Gets the target of the specified link. + /// + /// to follow links to the final target; to return the immediate next link. + /// A instance if the link exists, independently if the target exists or not; if this file or directory is not a link. + /// The file or directory does not exist. + /// -or- + /// The link's file system entry type is inconsistent with that of its target. + /// -or- + /// Too many levels of symbolic links. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. + public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget) => + FileSystem.ResolveLinkTarget(FullPath, returnFinalTarget, this is DirectoryInfo); + /// /// Returns the original path. Use FullName or Name properties for the full path or file/directory name. /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs index 104704de7b7154..cdc96b5f45228c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -125,7 +125,7 @@ public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path) return end >= 0 ? path.Slice(0, end) : ReadOnlySpan.Empty; } - private static int GetDirectoryNameOffset(ReadOnlySpan path) + internal static int GetDirectoryNameOffset(ReadOnlySpan path) { int rootLength = PathInternal.GetRootLength(path); int end = path.Length; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs index fcbeece65a73fa..ff9d79caee2532 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs @@ -245,5 +245,13 @@ internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + + internal static string GetLinkTargetFullPath(string path, string pathToTarget) + => IsPartiallyQualified(pathToTarget.AsSpan()) ? +#if MS_IO_REDIST + Path.Combine(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; +#else + Path.Join(Path.GetDirectoryName(path.AsSpan()), pathToTarget.AsSpan()) : pathToTarget; +#endif } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 67adfd0c6d35c2..1b9b1d241b8549 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10175,6 +10175,7 @@ public override void WriteByte(byte value) { } public static partial class Directory { public static System.IO.DirectoryInfo CreateDirectory(string path) { throw null; } + public static System.IO.FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { throw null; } public static void Delete(string path) { } public static void Delete(string path, bool recursive) { } public static System.Collections.Generic.IEnumerable EnumerateDirectories(string path) { throw null; } @@ -10213,6 +10214,7 @@ public static void Delete(string path, bool recursive) { } public static string[] GetLogicalDrives() { throw null; } public static System.IO.DirectoryInfo? GetParent(string path) { throw null; } public static void Move(string sourceDirName, string destDirName) { } + public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) { throw null; } public static void SetCreationTime(string path, System.DateTime creationTime) { } public static void SetCreationTimeUtc(string path, System.DateTime creationTimeUtc) { } public static void SetCurrentDirectory(string path) { } @@ -10237,10 +10239,13 @@ protected FileSystemInfo(System.Runtime.Serialization.SerializationInfo info, Sy public System.DateTime LastAccessTimeUtc { get { throw null; } set { } } public System.DateTime LastWriteTime { get { throw null; } set { } } public System.DateTime LastWriteTimeUtc { get { throw null; } set { } } + public string? LinkTarget { get { throw null; } } public abstract string Name { get; } + public void CreateAsSymbolicLink(string pathToTarget) { } public abstract void Delete(); public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public void Refresh() { } + public System.IO.FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget) { throw null; } public override string ToString() { throw null; } } public sealed partial class DirectoryInfo : System.IO.FileSystemInfo @@ -10325,6 +10330,7 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri public static System.IO.FileStream Create(string path) { throw null; } public static System.IO.FileStream Create(string path, int bufferSize) { throw null; } public static System.IO.FileStream Create(string path, int bufferSize, System.IO.FileOptions options) { throw null; } + public static System.IO.FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { throw null; } public static System.IO.StreamWriter CreateText(string path) { throw null; } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public static void Decrypt(string path) { } @@ -10363,6 +10369,7 @@ public static void Move(string sourceFileName, string destFileName, bool overwri public static System.Collections.Generic.IEnumerable ReadLines(string path, System.Text.Encoding encoding) { throw null; } public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName) { } public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) { } + public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) { throw null; } public static void SetAttributes(string path, System.IO.FileAttributes fileAttributes) { } public static void SetCreationTime(string path, System.DateTime creationTime) { } public static void SetCreationTimeUtc(string path, System.DateTime creationTimeUtc) { } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj index 2c0921f9fcd93c..505d487f1d486b 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj @@ -324,10 +324,14 @@ Link="Common\Interop\Unix\Interop.Libraries.cs" /> + +