mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
New symbolic link implementation for legacy windows
This commit is contained in:
56
src/lib-csharp/Util/SymbolicLink.Unix.cs
Normal file
56
src/lib-csharp/Util/SymbolicLink.Unix.cs
Normal 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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/lib-csharp/Util/SymbolicLink.Windows.cs
Normal file
211
src/lib-csharp/Util/SymbolicLink.Windows.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user