diff --git a/src/Velopack/Internal/SymbolicLink.cs b/src/Velopack/Internal/SymbolicLink.cs
index d3278765..d71bf44a 100644
--- a/src/Velopack/Internal/SymbolicLink.cs
+++ b/src/Velopack/Internal/SymbolicLink.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -33,7 +34,9 @@ namespace Velopack
}
}
- var finalTarget = relative ? GetRelativePath(linkPath, targetPath) : targetPath;
+ var finalTarget = relative
+ ? GetRelativePath(Path.GetDirectoryName(linkPath)!, targetPath)
+ : targetPath;
if (Directory.Exists(targetPath)) {
#if NETFRAMEWORK
@@ -54,11 +57,19 @@ namespace Velopack
}
}
+ ///
+ /// Returns true if the specified path exists and is a junction point or symlink.
+ /// If the path exists but is not a junction point or symlink, returns false.
+ ///
public static bool Exists(string linkPath)
{
return TryGetLinkFsi(linkPath, out var _);
}
+ ///
+ /// Does nothing if the path does not exist. If the path exists but is not
+ /// a junction / symlink, throws an IOException.
+ ///
public static void Delete(string linkPath)
{
var isLink = TryGetLinkFsi(linkPath, out var fsi);
@@ -69,15 +80,25 @@ namespace Velopack
}
}
- public static string GetTarget(string linkPath)
+ ///
+ /// Get the target of a junction point or symlink.
+ ///
+ /// The location of the symlink or junction point
+ /// If true, will return the full path to the target.
+ /// If false, will return the link target unadulterated - so it may be a
+ /// relative or an absolute path.
+ public static string GetTarget(string linkPath, bool resolve = true)
{
if (TryGetLinkFsi(linkPath, out var fsi)) {
string target;
#if NETFRAMEWORK
+
target = GetTargetWin32(linkPath);
#else
target = fsi.LinkTarget!;
#endif
+ if (!resolve) return target;
+
if (Path.IsPathRooted(target)) {
// if the path is absolute, we can return it as is.
return Path.GetFullPath(target);
@@ -89,12 +110,6 @@ namespace Velopack
throw new IOException("Path does not exist or is not a junction point / symlink.");
}
- public static string GetTargetRelativeToLink(string linkPath)
- {
- var targetPath = GetTarget(linkPath);
- return GetRelativePath(linkPath, targetPath);
- }
-
private static bool TryGetLinkFsi(string path, out FileSystemInfo fsi)
{
fsi = null!;
@@ -119,33 +134,6 @@ namespace Velopack
}
#if NETFRAMEWORK
- [Flags]
- private enum EFileAccess : uint
- {
- GenericRead = 0x80000000,
- GenericWrite = 0x40000000,
- GenericExecute = 0x20000000,
- GenericAll = 0x10000000,
- }
-
- [Flags]
- private enum EFileShare : uint
- {
- None = 0x00000000,
- Read = 0x00000001,
- Write = 0x00000002,
- Delete = 0x00000004,
- }
-
- private enum ECreationDisposition : uint
- {
- New = 1,
- CreateAlways = 2,
- OpenExisting = 3,
- OpenAlways = 4,
- TruncateExisting = 5,
- }
-
[Flags]
private enum EFileAttributes : uint
{
@@ -177,12 +165,12 @@ namespace Velopack
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
- private static extern IntPtr CreateFile(
+ private static extern SafeFileHandle CreateFile(
string lpFileName,
- EFileAccess dwDesiredAccess,
- EFileShare dwShareMode,
+ FileAccess dwDesiredAccess,
+ FileShare dwShareMode,
IntPtr lpSecurityAttributes,
- ECreationDisposition dwCreationDisposition,
+ FileMode dwCreationDisposition,
EFileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
@@ -192,31 +180,95 @@ namespace Velopack
private const int SYMBOLIC_LINK_FLAG_FILE = 0x0;
private const int SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1;
private const int SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2;
+ private const int INITIAL_REPARSE_DATA_BUFFER_SIZE = 1024;
+ private const int FSCTL_GET_REPARSE_POINT = 0x000900a8;
+ private const int ERROR_INSUFFICIENT_BUFFER = 0x7A;
+ private const int ERROR_MORE_DATA = 0xEA;
+ private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
+ private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern uint GetFinalPathNameByHandle(IntPtr hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DeviceIoControl(
+ SafeFileHandle deviceHandle,
+ uint ioControlCode,
+ IntPtr inputBuffer,
+ int inputBufferSize,
+ byte[] outputBuffer,
+ int outputBufferSize,
+ out int bytesReturned,
+ IntPtr overlapped);
+
private static string GetTargetWin32(string linkPath)
{
- using var handle = new SafeFileHandle(CreateFile(linkPath, EFileAccess.GenericRead,
- EFileShare.Read | EFileShare.Write | EFileShare.Delete,
- IntPtr.Zero, ECreationDisposition.OpenExisting,
- EFileAttributes.BackupSemantics, IntPtr.Zero), true);
+ // https://github.com/microsoft/BuildXL/blob/main/Public/Src/Utilities/Native/IO/Windows/FileSystem.Win.cs#L2711
+ // http://blog.kalmbach-software.de/2008/02/28/howto-correctly-read-reparse-data-in-vista/
+ // https://github.com/dotnet/runtime/blob/e5f0c361f5baea5e2b56e1776143d841b0cc6e6c/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L544
+ SafeFileHandle handle = CreateFile(
+ linkPath,
+ dwDesiredAccess: 0,
+ FileShare.ReadWrite | FileShare.Delete,
+ lpSecurityAttributes: IntPtr.Zero,
+ FileMode.Open,
+ dwFlagsAndAttributes: EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint,
+ hTemplateFile: IntPtr.Zero);
if (Marshal.GetLastWin32Error() != 0)
ThrowLastWin32Error("Unable to open reparse point.");
- var sb = new StringBuilder(1024);
- var res = GetFinalPathNameByHandle(handle.DangerousGetHandle(), sb, 1024, 0);
- if (res == 0)
- ThrowLastWin32Error("Unable to resolve reparse point target.");
+ int bufferSize = INITIAL_REPARSE_DATA_BUFFER_SIZE;
+ int errorCode = ERROR_INSUFFICIENT_BUFFER;
- var result = sb.ToString();
- if (result.StartsWith(@"\\?\"))
- result = result.Substring(4);
- if (result.StartsWith(@"\\?\UNC\"))
- result = @"\\" + result.Substring(8);
- return result;
+ byte[] buffer = null!;
+ while (errorCode == ERROR_MORE_DATA || errorCode == ERROR_INSUFFICIENT_BUFFER) {
+ buffer = new byte[bufferSize];
+ bool success = false;
+
+ int bufferReturnedSize;
+ success = DeviceIoControl(
+ handle,
+ FSCTL_GET_REPARSE_POINT,
+ IntPtr.Zero,
+ 0,
+ buffer,
+ bufferSize,
+ out bufferReturnedSize,
+ IntPtr.Zero);
+
+ bufferSize *= 2;
+ errorCode = success ? 0 : Marshal.GetLastWin32Error();
+ }
+
+ if (errorCode != 0) {
+ throw new Win32Exception(errorCode);
+ }
+
+ const uint PrintNameOffsetIndex = 12;
+ const uint PrintNameLengthIndex = 14;
+ const uint SubsNameOffsetIndex = 8;
+ const uint SubsNameLengthIndex = 10;
+
+ uint reparsePointTag = BitConverter.ToUInt32(buffer, 0);
+ if (reparsePointTag != IO_REPARSE_TAG_SYMLINK && reparsePointTag != IO_REPARSE_TAG_MOUNT_POINT) {
+ throw new NotSupportedException($"Reparse point tag {reparsePointTag:X} not supported");
+ }
+
+ uint pathBufferOffsetIndex = (uint) ((reparsePointTag == IO_REPARSE_TAG_SYMLINK) ? 20 : 16);
+
+ int nameOffset = BitConverter.ToInt16(buffer, (int) PrintNameOffsetIndex);
+ int nameLength = BitConverter.ToInt16(buffer, (int) PrintNameLengthIndex);
+ string targetPath = Encoding.Unicode.GetString(buffer, (int) pathBufferOffsetIndex + nameOffset, nameLength);
+
+ if (string.IsNullOrWhiteSpace(targetPath)) {
+ nameOffset = BitConverter.ToInt16(buffer, (int) SubsNameOffsetIndex);
+ nameLength = BitConverter.ToInt16(buffer, (int) SubsNameLengthIndex);
+ targetPath = Encoding.Unicode.GetString(buffer, (int) pathBufferOffsetIndex + nameOffset, nameLength);
+ }
+
+ return targetPath;
}
private static void ThrowLastWin32Error(string message)
diff --git a/test/Velopack.Tests/SymbolicLinkTests.cs b/test/Velopack.Tests/SymbolicLinkTests.cs
index dea3bbd6..ba437929 100644
--- a/test/Velopack.Tests/SymbolicLinkTests.cs
+++ b/test/Velopack.Tests/SymbolicLinkTests.cs
@@ -86,6 +86,53 @@ public class SymbolicLinkTests
Assert.False(SymbolicLink.Exists(symFile));
}
+ [Fact]
+ public void CreateFile_RelativePath()
+ {
+ using var _1 = Utility.GetTempDirectory(out var tempFolder);
+ var subDir = Directory.CreateDirectory(Path.Combine(tempFolder, "SubDir")).FullName;
+
+ var tmpFile = Path.Combine(tempFolder, "AFile");
+ var symFile1 = Path.Combine(tempFolder, "SymFile");
+ var symFile2 = Path.Combine(subDir, "SymFile2");
+ File.WriteAllText(tmpFile, "Hello!");
+
+ SymbolicLink.Create(symFile1, tmpFile, relative: true);
+ SymbolicLink.Create(symFile2, tmpFile, relative: true);
+
+ Assert.Equal("Hello!", File.ReadAllText(symFile1));
+ Assert.Equal("Hello!", File.ReadAllText(symFile2));
+
+ Assert.Equal("AFile", SymbolicLink.GetTarget(symFile1, resolve: false));
+ Assert.Equal("..\\AFile", SymbolicLink.GetTarget(symFile2, resolve: false));
+
+ Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile1));
+ Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile2));
+ }
+
+ [Fact]
+ public void CreateDirectory_RelativePath()
+ {
+ using var _1 = Utility.GetTempDirectory(out var tempFolder);
+ var subDir = Directory.CreateDirectory(Path.Combine(tempFolder, "SubDir")).FullName;
+ var subSubDir = Directory.CreateDirectory(Path.Combine(subDir, "SubSub")).FullName;
+ var subDir2 = Directory.CreateDirectory(Path.Combine(tempFolder, "SubDir2")).FullName;
+
+ File.WriteAllText(Path.Combine(subSubDir, "AFile"), "Hello!");
+ var sym1 = Path.Combine(subSubDir, "Sym1");
+ var sym2 = Path.Combine(tempFolder, "Sym2");
+
+ SymbolicLink.Create(sym1, subDir2, relative: true);
+ SymbolicLink.Create(sym2, subSubDir, relative: true);
+
+ Assert.Equal("Hello!", File.ReadAllText(Path.Combine(sym2, "AFile")));
+
+ Assert.Equal(subSubDir, SymbolicLink.GetTarget(sym2));
+ Assert.Equal(subDir2, SymbolicLink.GetTarget(sym1));
+ Assert.Equal("..\\..\\SubDir2", SymbolicLink.GetTarget(sym1, resolve: false));
+ Assert.Equal("SubDir\\SubSub", SymbolicLink.GetTarget(sym2, resolve: false));
+ }
+
[Fact]
public void Create_ThrowsIfOverwriteNotSpecifiedAndDirectoryExists()
{