Skip to content

Commit

Permalink
Add symbolic link APIs (#54253)
Browse files Browse the repository at this point in the history
Co-authored-by: David Cantu <dacantu@microsoft.com>
Co-authored-by: carlossanlop <carlossanlop@users.noreply.github.com>
Co-authored-by: carlossanlop <Carlos Sanchez carlossanlop@users.noreply.github.com>
  • Loading branch information
4 people authored Jul 9, 2021
1 parent f1bf5ee commit dcce0f5
Show file tree
Hide file tree
Showing 50 changed files with 1,883 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Runtime.InteropServices;
using System.Buffers;
using System.Text;
using System;

internal static partial class Interop
{
Expand All @@ -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.
/// </returns>
[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);

/// <summary>
/// Takes a path to a symbolic link and returns the link target path.
/// </summary>
/// <param name="path">The path to the symlink</param>
/// <returns>
/// Returns the link to the target path on success; and null otherwise.
/// </returns>
public static string? ReadLink(string path)
/// <param name="path">The path to the symlink.</param>
/// <returns>Returns the link to the target path on success; and null otherwise.</returns>
internal static string? ReadLink(ReadOnlySpan<char> 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<byte>.Shared.Rent(bufferSize);
byte[] buffer = ArrayPool<byte>.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
Expand All @@ -54,8 +62,8 @@ internal static partial class Sys
ArrayPool<byte>.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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> 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;
Expand All @@ -29,7 +25,7 @@ internal static int Stat(ReadOnlySpan<char> path, out FileStatus output)

internal static int LStat(ReadOnlySpan<char> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The link target is a directory.
/// </summary>
internal const int SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1;

/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// Creates a symbolic link.
/// </summary>
/// <param name="symlinkFileName">The symbolic link to be created.</param>
/// <param name="targetFileName">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.</param>
/// <param name="isDirectory"><see langword="true" /> if the link target is a directory; <see langword="false" /> otherwise.</param>
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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
36 changes: 23 additions & 13 deletions src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
}
}
Loading

0 comments on commit dcce0f5

Please sign in to comment.