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
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" />
+
+