New symbolic link implementation for legacy windows

This commit is contained in:
Caelan Sayler
2025-08-16 16:40:58 +01:00
committed by Caelan
parent efa9d296b0
commit 031bd9b63a
4 changed files with 656 additions and 83 deletions

View File

@@ -0,0 +1,56 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
namespace Velopack.Util
{
internal static partial class SymbolicLink
{
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern nint readlink(string path, byte[] buffer, ulong bufferSize);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int symlink(string target, string linkPath);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static string UnixReadLink(string symlinkPath)
{
const int bufferSize = 1024;
const int EINTR = 4;
byte[] buffer = new byte[bufferSize];
nint bytesWritten;
do {
bytesWritten = readlink(symlinkPath, buffer, bufferSize);
} while (bytesWritten == -1 && Marshal.GetLastWin32Error() == EINTR);
if (bytesWritten < 1) {
throw new InvalidOperationException($"Error resolving symlink: {Marshal.GetLastWin32Error()}");
}
return Encoding.UTF8.GetString(buffer, 0, (int) bytesWritten);
}
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static void UnixCreateSymlink(string target, string linkPath)
{
const int EINTR = 4;
int result;
do {
result = symlink(target, linkPath);
} while (result == -1 && Marshal.GetLastWin32Error() == EINTR);
if (result == -1) {
throw new InvalidOperationException($"Error creating symlink: {Marshal.GetLastWin32Error()}");
}
}
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace Velopack.Util
{
internal static partial class SymbolicLink
{
private const string Kernel32 = "kernel32.dll";
private const uint FSCTL_GET_REPARSE_POINT = 0x000900A8;
private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
private const int ERROR_NOT_A_REPARSE_POINT = 4390;
private const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
private const uint SYMLINK_FLAG_RELATIVE = 1;
private const uint GENERIC_READ = 0x80000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
[StructLayout(LayoutKind.Sequential)]
private struct ReparseHeader
{
public uint ReparseTag;
public ushort ReparseDataLength;
public ushort Reserved;
}
[StructLayout(LayoutKind.Sequential)]
private struct SymbolicData
{
public ushort SubstituteNameOffset;
public ushort SubstituteNameLength;
public ushort PrintNameOffset;
public ushort PrintNameLength;
public uint Flags;
}
[StructLayout(LayoutKind.Sequential)]
private struct JunctionData
{
public ushort SubstituteNameOffset;
public ushort SubstituteNameLength;
public ushort PrintNameOffset;
public ushort PrintNameLength;
}
[SupportedOSPlatform("windows")]
[DllImport(Kernel32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateSymbolicLinkW")]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool PInvokeWindowsCreateSymlink(
[In]
string lpSymlinkFileName,
[In]
string lpTargetFileName,
[In]
SymbolicLinkFlag dwFlags);
[DllImport(Kernel32, SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport(Kernel32, SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
SafeFileHandle hDevice,
uint dwIoControlCode,
IntPtr lpInBuffer,
int nInBufferSize,
IntPtr lpOutBuffer,
int nOutBufferSize,
out int lpBytesReturned,
IntPtr lpOverlapped);
[SupportedOSPlatform("windows")]
private static string WindowsReadLink(string symlinkPath)
{
using (var hReparsePoint = CreateFile(
symlinkPath,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
IntPtr.Zero)) {
if (hReparsePoint.IsInvalid) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var buffer = Marshal.AllocHGlobal(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
try {
int bytesReturned;
var success = DeviceIoControl(
hReparsePoint,
FSCTL_GET_REPARSE_POINT,
IntPtr.Zero,
0,
buffer,
MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
out bytesReturned,
IntPtr.Zero);
if (!success) {
int error = Marshal.GetLastWin32Error();
// The file or directory is not a reparse point.
if (error == ERROR_NOT_A_REPARSE_POINT) {
throw new InvalidOperationException($"Path is not a symbolic link: {symlinkPath}");
}
throw new Win32Exception(error);
}
var reparseHeader = Marshal.PtrToStructure<ReparseHeader>(buffer);
var reparseHeaderSize = Marshal.SizeOf<ReparseHeader>();
// We always use SubstituteName instead of PrintName,
// the latter is just the display name and can show something unrelated to the target.
if (reparseHeader.ReparseTag == IO_REPARSE_TAG_SYMLINK) {
var symbolicData = Marshal.PtrToStructure<SymbolicData>(buffer + reparseHeaderSize);
var offset = Marshal.SizeOf<SymbolicData>() + reparseHeaderSize;
var target = ReadStringFromBuffer(buffer, offset + symbolicData.SubstituteNameOffset, symbolicData.SubstituteNameLength);
bool isRelative = (symbolicData.Flags & SYMLINK_FLAG_RELATIVE) != 0;
if (!isRelative) {
// Absolute target is in NT format and we need to clean it up
return ParseNTPath(target);
}
// Return relative path as-is
return target;
} else if (reparseHeader.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {
var junctionData = Marshal.PtrToStructure<JunctionData>(buffer + reparseHeaderSize);
var offset = Marshal.SizeOf<JunctionData>() + reparseHeaderSize;
var target = ReadStringFromBuffer(buffer, offset + junctionData.SubstituteNameOffset, junctionData.SubstituteNameLength);
// Mount points are always absolute and in NT format
return ParseNTPath(target);
}
throw new InvalidOperationException($"Unsupported reparse point type: 0x{reparseHeader.ReparseTag:X}");
} finally {
Marshal.FreeHGlobal(buffer);
}
}
}
private static string ReadStringFromBuffer(IntPtr buffer, int offset, int byteCount)
{
var bytes = new byte[byteCount];
Marshal.Copy(buffer + offset, bytes, 0, byteCount);
return Encoding.Unicode.GetString(bytes);
}
private static string ParseNTPath(string path)
{
// NT paths come in different forms:
// \??\C:\foo - DOS device path
// \DosDevices\C:\foo - DOS device path
// \Global??\C:\foo - DOS device path
// \??\UNC\server\share - UNC path
const string NTPathPrefix = "\\??\\";
const string UNCNTPathPrefix = "\\??\\UNC\\";
const string UNCPathPrefix = "\\\\";
if (path.StartsWith(UNCNTPathPrefix, StringComparison.OrdinalIgnoreCase)) {
// Convert \??\UNC\server\share to \\server\share
return UNCPathPrefix + path.Substring(UNCNTPathPrefix.Length);
}
string[] dosDevicePrefixes = { NTPathPrefix, "\\DosDevices\\", "\\Global??\\" };
foreach (var prefix in dosDevicePrefixes) {
if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {
path = path.Substring(prefix.Length);
break;
}
}
// Remove trailing backslash except for root paths like C:\
if (path.Length > 3 && path.EndsWith("\\")) {
path = path.TrimEnd('\\');
}
return path;
}
[SupportedOSPlatform("windows")]
private static void WindowsCreateSymlink(string target, string linkPath, SymbolicLinkFlag mode)
{
if (!PInvokeWindowsCreateSymlink(linkPath, target, mode)) {
var errorCode = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"Error creating symlink: {errorCode}", new Win32Exception());
}
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using NCode.ReparsePoints;
namespace Velopack.Util
{
internal static class SymbolicLink
internal static partial class SymbolicLink
{
/// <summary>
/// Creates a symlink from the specified directory to the specified target directory.
@@ -38,9 +34,9 @@ namespace Velopack.Util
: targetPath;
if (Directory.Exists(targetPath)) {
CreateDirectoryLink(linkPath, finalTarget, targetPath);
CreateSymlink(linkPath, finalTarget, SymbolicLinkFlag.Directory);
} else if (File.Exists(targetPath)) {
CreateFileLink(linkPath, finalTarget);
CreateSymlink(linkPath, finalTarget, SymbolicLinkFlag.File);
} else {
throw new IOException("Target path does not exist.");
}
@@ -92,42 +88,32 @@ namespace Velopack.Util
}
}
private static void CreateFileLink(string linkPath, string targetPath)
[Serializable]
private enum SymbolicLinkFlag : uint
{
if (VelopackRuntimeInfo.IsWindows) {
var rp = new ReparsePointProvider();
rp.CreateSymbolicLink(linkPath, targetPath, false);
} else {
#if NETSTANDARD
UnixCreateSymlink(targetPath, linkPath);
#elif NET6_0_OR_GREATER
File.CreateSymbolicLink(linkPath, targetPath);
#else
throw new NotSupportedException();
#endif
}
File = 0,
Directory = 1,
}
private static void CreateDirectoryLink(string linkPath, string targetPath, string absoluteTargetPath)
private static void CreateSymlink(string linkPath, string targetPath, SymbolicLinkFlag mode)
{
if (VelopackRuntimeInfo.IsWindows) {
var rp = new ReparsePointProvider();
try {
rp.CreateSymbolicLink(linkPath, targetPath, true);
} catch (Win32Exception ex) when (ex.NativeErrorCode == 1314) {
// on windows 10 and below, symbolic links can only be created by an administrator
// junctions also do not support relative target path's
rp.CreateJunction(linkPath, absoluteTargetPath);
}
} else {
#if NETSTANDARD
UnixCreateSymlink(targetPath, linkPath);
#elif NET6_0_OR_GREATER
#if NET6_0_OR_GREATER
if (mode == SymbolicLinkFlag.File) {
File.CreateSymbolicLink(linkPath, targetPath);
} else if (mode == SymbolicLinkFlag.Directory) {
Directory.CreateSymbolicLink(linkPath, targetPath);
#else
throw new NotSupportedException();
#endif
} else {
throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid symbolic link mode.");
}
#else
if (VelopackRuntimeInfo.IsWindows) {
WindowsCreateSymlink(targetPath, linkPath, mode);
} else if (VelopackRuntimeInfo.IsLinux || VelopackRuntimeInfo.IsOSX) {
UnixCreateSymlink(targetPath, linkPath);
} else {
throw new NotSupportedException("Symbolic links are not supported on this platform.");
}
#endif
}
private static string GetUnresolvedTarget(string linkPath)
@@ -136,19 +122,19 @@ namespace Velopack.Util
throw new IOException("Path does not exist or is not a junction point / symlink.");
}
if (VelopackRuntimeInfo.IsWindows) {
var rp = new ReparsePointProvider();
var link = rp.GetLink(linkPath);
return link.Target;
} else {
#if NETSTANDARD
return UnixReadLink(linkPath);
#elif NET6_0_OR_GREATER
return fsi!.LinkTarget!;
#if NET6_0_OR_GREATER
return fsi!.LinkTarget!;
#else
throw new NotSupportedException();
#endif
if (VelopackRuntimeInfo.IsWindows) {
return WindowsReadLink(linkPath);
}
if (VelopackRuntimeInfo.IsLinux || VelopackRuntimeInfo.IsOSX) {
return UnixReadLink(linkPath);
}
throw new NotSupportedException();
#endif
}
private static bool TryGetLinkFsi(string path, out FileSystemInfo? fsi)
@@ -166,37 +152,5 @@ namespace Velopack.Util
return (fsi.Attributes & FileAttributes.ReparsePoint) != 0;
}
#if NETSTANDARD
[DllImport("libc", SetLastError = true)]
private static extern nint readlink(string path, byte[] buffer, ulong bufferSize);
[DllImport("libc", SetLastError = true)]
private static extern int symlink(string target, string linkPath);
private static string UnixReadLink(string symlinkPath)
{
const int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
nint bytesWritten = readlink(symlinkPath, buffer, bufferSize);
if (bytesWritten < 1) {
throw new InvalidOperationException($"Error resolving symlink: {Marshal.GetLastWin32Error()}");
}
return Encoding.UTF8.GetString(buffer, 0, (int) bytesWritten);
}
private static void UnixCreateSymlink(string target, string linkPath)
{
// Call the symlink function from libc
int result = symlink(target, linkPath);
// Check for errors (-1 return value indicates failure)
if (result == -1) {
throw new InvalidOperationException($"Error creating symlink: {Marshal.GetLastWin32Error()}");
}
}
#endif
}
}