mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Merge branch 'develop' into kdb/msi-wip
This commit is contained in:
18
README.md
18
README.md
@@ -50,3 +50,21 @@ I've used a lot of installer frameworks and Velopack is by far the best. Everyth
|
||||
|
||||
I'm extremely impressed with Velopack's performance in creating releases, as well as checking for and applying updates. It is significantly faster than other tools. The vpk CLI is intuitive and easy to implement, even with my complex build pipeline. Thanks to Velopack, I've been able to streamline my workflow and save valuable time. It's a fantastic tool that I highly recommend!
|
||||
[- khdc (Discord)](https://discord.com/channels/767856501477343282/947444323765583913/1216460920696344576)
|
||||
|
||||
## Sponsors
|
||||
Huge thanks to everyone who provides the project with free tools, services, or donations!
|
||||
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://velopack.github.io/velopack.sponsorkit/sponsors-white.svg">
|
||||
<img alt="Sponsors Cloud" src="https://velopack.github.io/velopack.sponsorkit/sponsors-black.svg">
|
||||
</picture>
|
||||
|
||||
<span>Free tools provided by<br/></span>
|
||||
<a href="https://jb.gg/OpenSource">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://www.jetbrains.com/company/brand/img/logo_jb_dos_3.svg">
|
||||
<img alt="Jetbrains Logo" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" width="160">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
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,10 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
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 +35,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.");
|
||||
}
|
||||
@@ -63,7 +60,7 @@ namespace Velopack.Util
|
||||
{
|
||||
var isLink = TryGetLinkFsi(linkPath, out var fsi);
|
||||
if (fsi != null && !isLink) {
|
||||
throw new IOException("Path is not a junction point / symlink.");
|
||||
ThrowPathNotASymlinkException(linkPath);
|
||||
} else {
|
||||
fsi?.Delete();
|
||||
}
|
||||
@@ -77,6 +74,7 @@ namespace Velopack.Util
|
||||
public static string GetTarget(string linkPath, bool relative = false)
|
||||
{
|
||||
var target = GetUnresolvedTarget(linkPath);
|
||||
|
||||
if (relative) {
|
||||
if (Path.IsPathRooted(target)) {
|
||||
return PathUtil.MakePathRelativeTo(Path.GetDirectoryName(linkPath)!, target);
|
||||
@@ -92,63 +90,62 @@ 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
|
||||
linkPath = linkPath.TrimEnd('\\', '/');
|
||||
|
||||
#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)
|
||||
{
|
||||
linkPath = linkPath.TrimEnd('\\', '/');
|
||||
|
||||
if (!TryGetLinkFsi(linkPath, out var fsi)) {
|
||||
throw new IOException("Path does not exist or is not a junction point / symlink.");
|
||||
ThrowPathNotASymlinkException(linkPath);
|
||||
}
|
||||
|
||||
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!;
|
||||
string target;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
target = fsi!.LinkTarget!;
|
||||
#else
|
||||
throw new NotSupportedException();
|
||||
#endif
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
target = WindowsReadLink(linkPath);
|
||||
} else if (VelopackRuntimeInfo.IsLinux || VelopackRuntimeInfo.IsOSX) {
|
||||
target = UnixReadLink(linkPath);
|
||||
} else {
|
||||
throw new NotSupportedException("Symbolic links are not supported on this platform.");
|
||||
}
|
||||
#endif
|
||||
if (String.IsNullOrEmpty(target)) {
|
||||
ThrowPathNotASymlinkException(linkPath);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
private static bool TryGetLinkFsi(string path, out FileSystemInfo? fsi)
|
||||
@@ -167,36 +164,12 @@ 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()}");
|
||||
}
|
||||
}
|
||||
#if NET6_0_OR_GREATER
|
||||
[DoesNotReturn]
|
||||
#endif
|
||||
private static void ThrowPathNotASymlinkException(string path)
|
||||
{
|
||||
throw new IOException($"The path '{path}' is not a symbolic link or junction point.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleZ
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_bundle_from_memory(zip_range: &[u8]) -> Result<BundleZip<'_>, Error> {
|
||||
pub fn load_bundle_from_memory<'a>(zip_range: &'a [u8]) -> Result<BundleZip<'a>, Error> {
|
||||
info!("Loading bundle from embedded zip...");
|
||||
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
|
||||
let zip = ZipArchive::new(cursor)?;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" Version="4.0.1.3" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" />
|
||||
<PackageReference Include="Gitea.Net.API" Version="25.3.5" />
|
||||
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.10" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Velopack.Core;
|
||||
using Velopack.Core.Abstractions;
|
||||
@@ -56,6 +56,12 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
|
||||
}
|
||||
}
|
||||
|
||||
// Files in the MacOS directory need to be signed, but text files are signed via xattrs, which we don't yet preserve
|
||||
// in nupkg releases. Instead we can put it in the Resources dir and symlink to it. Symlinks don't need to be signed.
|
||||
var resourcesdir = structure.ResourcesDirectory;
|
||||
File.WriteAllText(Path.Combine(resourcesdir, "sq.version"), GenerateNuspecContent());
|
||||
SymbolicLink.Create(Path.Combine(macosdir, "sq.version"), Path.Combine(resourcesdir, "sq.version"), false, true);
|
||||
|
||||
progress(100);
|
||||
return Task.FromResult(dir.FullName);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO.Compression;
|
||||
using NCode.ReparsePoints;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using Velopack.Logging;
|
||||
using Velopack.Util;
|
||||
using NCode.ReparsePoints;
|
||||
|
||||
namespace Velopack.Tests;
|
||||
|
||||
@@ -270,4 +270,356 @@ public class SymbolicLinkTests
|
||||
Assert.True(File.Exists(currentSym));
|
||||
Assert.Equal("A/", File.ReadAllText(currentSym));
|
||||
}
|
||||
|
||||
// ===== New comprehensive tests =====
|
||||
|
||||
[Fact]
|
||||
public void Create_SymlinkToNonExistentTarget_ShouldWork()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "NonExistent");
|
||||
var link = Path.Combine(tempFolder, "Link");
|
||||
|
||||
// Should be able to create symlink to non-existent target
|
||||
File.WriteAllText(target, "test");
|
||||
SymbolicLink.Create(link, target);
|
||||
Assert.True(SymbolicLink.Exists(link));
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link));
|
||||
|
||||
// Clean up
|
||||
SymbolicLink.Delete(link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_MultipleLevelsOfSymlinks()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var file = Path.Combine(tempFolder, "Original.txt");
|
||||
var link1 = Path.Combine(tempFolder, "Link1.txt");
|
||||
var link2 = Path.Combine(tempFolder, "Link2.txt");
|
||||
var link3 = Path.Combine(tempFolder, "Link3.txt");
|
||||
|
||||
File.WriteAllText(file, "Hello");
|
||||
|
||||
// Create chain: link3 -> link2 -> link1 -> file
|
||||
SymbolicLink.Create(link1, file);
|
||||
SymbolicLink.Create(link2, link1);
|
||||
SymbolicLink.Create(link3, link2);
|
||||
|
||||
// All should resolve to the same content
|
||||
Assert.Equal("Hello", File.ReadAllText(link1));
|
||||
Assert.Equal("Hello", File.ReadAllText(link2));
|
||||
Assert.Equal("Hello", File.ReadAllText(link3));
|
||||
|
||||
// Each should point to their immediate target
|
||||
Assert.Equal(file, SymbolicLink.GetTarget(link1));
|
||||
Assert.Equal(link1, SymbolicLink.GetTarget(link2));
|
||||
Assert.Equal(link2, SymbolicLink.GetTarget(link3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTarget_WithTrailingSlash_ShouldWork()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "Target");
|
||||
var link = Path.Combine(tempFolder, "Link");
|
||||
|
||||
Directory.CreateDirectory(target);
|
||||
SymbolicLink.Create(link, target);
|
||||
|
||||
// Should work with and without trailing slash
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link));
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link + Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithSpecialCharactersInPath()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "Target With Spaces & Special-Chars");
|
||||
var link = Path.Combine(tempFolder, "Link With Spaces & Special-Chars");
|
||||
|
||||
Directory.CreateDirectory(target);
|
||||
File.WriteAllText(Path.Combine(target, "file.txt"), "content");
|
||||
|
||||
SymbolicLink.Create(link, target);
|
||||
Assert.True(SymbolicLink.Exists(link));
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link));
|
||||
Assert.Equal("content", File.ReadAllText(Path.Combine(link, "file.txt")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_AbsoluteVsRelativeComparison()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var subdir = Directory.CreateDirectory(Path.Combine(tempFolder, "subdir")).FullName;
|
||||
var target = Path.Combine(tempFolder, "target.txt");
|
||||
var linkAbs = Path.Combine(subdir, "link_abs.txt");
|
||||
var linkRel = Path.Combine(subdir, "link_rel.txt");
|
||||
|
||||
File.WriteAllText(target, "test");
|
||||
|
||||
// Create absolute and relative links
|
||||
SymbolicLink.Create(linkAbs, target, relative: false);
|
||||
SymbolicLink.Create(linkRel, target, relative: true);
|
||||
|
||||
// Both should work
|
||||
Assert.Equal("test", File.ReadAllText(linkAbs));
|
||||
Assert.Equal("test", File.ReadAllText(linkRel));
|
||||
|
||||
// Check targets
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(linkAbs));
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(linkRel));
|
||||
|
||||
// Relative target should be different when requested
|
||||
var relTarget = SymbolicLink.GetTarget(linkRel, relative: true);
|
||||
Assert.Contains("..", relTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileSymlink_AllImplementationsAgree()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "target.txt");
|
||||
var link = Path.Combine(tempFolder, "link.txt");
|
||||
|
||||
File.WriteAllText(target, "test content");
|
||||
|
||||
// Create with our implementation
|
||||
SymbolicLink.Create(link, target);
|
||||
|
||||
// Verify all implementations agree
|
||||
var ourTarget = SymbolicLink.GetTarget(link);
|
||||
Assert.Equal(target, ourTarget);
|
||||
|
||||
// Compare with NCode.ReparsePoints (Windows only)
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
var provider = new ReparsePointProvider();
|
||||
var linkInfo = provider.GetLink(link);
|
||||
Assert.Equal(LinkType.Symbolic, linkInfo.Type);
|
||||
Assert.Equal(target, linkInfo.Target);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var fileInfo = new FileInfo(link);
|
||||
Assert.NotNull(fileInfo.LinkTarget);
|
||||
Assert.Equal(target, Path.GetFullPath(fileInfo.LinkTarget));
|
||||
|
||||
// Test interoperability: create with framework, read with ours
|
||||
var link2 = Path.Combine(tempFolder, "link2.txt");
|
||||
File.CreateSymbolicLink(link2, target);
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link2));
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectorySymlink_AllImplementationsAgree()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "targetDir");
|
||||
var link = Path.Combine(tempFolder, "linkDir");
|
||||
|
||||
Directory.CreateDirectory(target);
|
||||
File.WriteAllText(Path.Combine(target, "file.txt"), "content");
|
||||
|
||||
// Create with our implementation
|
||||
SymbolicLink.Create(link, target);
|
||||
|
||||
// Verify all implementations agree
|
||||
var ourTarget = SymbolicLink.GetTarget(link);
|
||||
Assert.Equal(target, ourTarget);
|
||||
|
||||
// Compare with NCode.ReparsePoints (Windows only)
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
var provider = new ReparsePointProvider();
|
||||
var linkInfo = provider.GetLink(link);
|
||||
// Directory symlinks on Windows are actually junctions
|
||||
Assert.True(linkInfo.Type == LinkType.Junction || linkInfo.Type == LinkType.Symbolic);
|
||||
Assert.Equal(target, linkInfo.Target);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var dirInfo = new DirectoryInfo(link);
|
||||
Assert.NotNull(dirInfo.LinkTarget);
|
||||
Assert.Equal(target, Path.GetFullPath(dirInfo.LinkTarget));
|
||||
|
||||
// Test interoperability: create with framework, read with ours
|
||||
var link2 = Path.Combine(tempFolder, "linkDir2");
|
||||
Directory.CreateSymbolicLink(link2, target);
|
||||
Assert.Equal(target, SymbolicLink.GetTarget(link2));
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RelativeSymlink_AllImplementationsAgree()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var subdir = Directory.CreateDirectory(Path.Combine(tempFolder, "subdir")).FullName;
|
||||
var target = Path.Combine(tempFolder, "target.txt");
|
||||
var link = Path.Combine(subdir, "link.txt");
|
||||
|
||||
File.WriteAllText(target, "test");
|
||||
|
||||
// Create relative symlink with our implementation
|
||||
SymbolicLink.Create(link, target, relative: true);
|
||||
|
||||
// Verify our implementation handles relative vs absolute correctly
|
||||
var ourAbsoluteTarget = SymbolicLink.GetTarget(link);
|
||||
var ourRelativeTarget = SymbolicLink.GetTarget(link, relative: true);
|
||||
Assert.Equal(target, ourAbsoluteTarget);
|
||||
Assert.Contains("..", ourRelativeTarget);
|
||||
|
||||
// Compare with NCode.ReparsePoints (Windows only)
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
var provider = new ReparsePointProvider();
|
||||
var linkInfo = provider.GetLink(link);
|
||||
Assert.Equal(LinkType.Symbolic, linkInfo.Type);
|
||||
// NCode returns the raw target path as stored in the symlink
|
||||
// For relative symlinks, this will be the relative path, not absolute
|
||||
Assert.Equal(ourRelativeTarget, linkInfo.Target);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var fileInfo = new FileInfo(link);
|
||||
Assert.NotNull(fileInfo.LinkTarget);
|
||||
// Framework returns the relative path for relative symlinks
|
||||
Assert.Contains("..", fileInfo.LinkTarget);
|
||||
Assert.Equal(fileInfo.LinkTarget, ourRelativeTarget);
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleLevelsOfSymlinks_AllImplementationsAgree()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var file = Path.Combine(tempFolder, "original.txt");
|
||||
var link1 = Path.Combine(tempFolder, "link1.txt");
|
||||
var link2 = Path.Combine(tempFolder, "link2.txt");
|
||||
|
||||
File.WriteAllText(file, "content");
|
||||
|
||||
// Create chain: link2 -> link1 -> file
|
||||
SymbolicLink.Create(link1, file);
|
||||
SymbolicLink.Create(link2, link1);
|
||||
|
||||
// Verify our implementation
|
||||
Assert.Equal(file, SymbolicLink.GetTarget(link1));
|
||||
Assert.Equal(link1, SymbolicLink.GetTarget(link2));
|
||||
|
||||
// Verify content access works through the chain
|
||||
Assert.Equal("content", File.ReadAllText(link1));
|
||||
Assert.Equal("content", File.ReadAllText(link2));
|
||||
|
||||
// Compare with NCode.ReparsePoints (Windows only)
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
var provider = new ReparsePointProvider();
|
||||
var link1Info = provider.GetLink(link1);
|
||||
var link2Info = provider.GetLink(link2);
|
||||
Assert.Equal(LinkType.Symbolic, link1Info.Type);
|
||||
Assert.Equal(LinkType.Symbolic, link2Info.Type);
|
||||
Assert.Equal(file, link1Info.Target);
|
||||
Assert.Equal(link1, link2Info.Target);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var fileInfo1 = new FileInfo(link1);
|
||||
var fileInfo2 = new FileInfo(link2);
|
||||
Assert.NotNull(fileInfo1.LinkTarget);
|
||||
Assert.NotNull(fileInfo2.LinkTarget);
|
||||
Assert.Equal(file, Path.GetFullPath(fileInfo1.LinkTarget));
|
||||
Assert.Equal(link1, Path.GetFullPath(fileInfo2.LinkTarget));
|
||||
|
||||
// Test ResolveLinkTarget for final resolution
|
||||
var finalTarget = fileInfo2.ResolveLinkTarget(true);
|
||||
Assert.Equal(file, finalTarget?.FullName);
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_OnlyDeletesLinkNotTarget()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "target.txt");
|
||||
var link = Path.Combine(tempFolder, "link.txt");
|
||||
|
||||
File.WriteAllText(target, "important data");
|
||||
SymbolicLink.Create(link, target);
|
||||
|
||||
// Delete the link
|
||||
SymbolicLink.Delete(link);
|
||||
|
||||
// Link should be gone, but target should remain
|
||||
Assert.False(File.Exists(link));
|
||||
Assert.False(SymbolicLink.Exists(link));
|
||||
Assert.True(File.Exists(target));
|
||||
Assert.Equal("important data", File.ReadAllText(target));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void Exists_ReturnsFalseForHardLink()
|
||||
{
|
||||
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
|
||||
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target = Path.Combine(tempFolder, "target.txt");
|
||||
var hardLink = Path.Combine(tempFolder, "hardlink.txt");
|
||||
|
||||
File.WriteAllText(target, "test");
|
||||
|
||||
// Create hard link using P/Invoke
|
||||
if (!CreateHardLink(hardLink, target, IntPtr.Zero)) {
|
||||
// Skip test if hard link creation fails (may need elevation)
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard link should exist as a file but not as a symbolic link
|
||||
Assert.True(File.Exists(hardLink));
|
||||
Assert.False(SymbolicLink.Exists(hardLink));
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
|
||||
|
||||
[Fact]
|
||||
public void GetTarget_ErrorMessages_AreDescriptive()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var regularFile = Path.Combine(tempFolder, "regular.txt");
|
||||
var regularDir = Path.Combine(tempFolder, "regularDir");
|
||||
var nonExistent = Path.Combine(tempFolder, "nonExistent");
|
||||
|
||||
File.WriteAllText(regularFile, "test");
|
||||
Directory.CreateDirectory(regularDir);
|
||||
|
||||
// Test various error conditions
|
||||
var ex1 = Assert.Throws<IOException>(() => SymbolicLink.GetTarget(regularFile));
|
||||
Assert.Contains("junction", ex1.Message.ToLower());
|
||||
|
||||
var ex2 = Assert.Throws<IOException>(() => SymbolicLink.GetTarget(regularDir));
|
||||
Assert.Contains("junction", ex2.Message.ToLower());
|
||||
|
||||
var ex3 = Assert.Throws<IOException>(() => SymbolicLink.GetTarget(nonExistent));
|
||||
Assert.Contains("junction", ex2.Message.ToLower());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_OverwriteExistingSymlink()
|
||||
{
|
||||
using var _1 = TempUtil.GetTempDirectory(out var tempFolder);
|
||||
var target1 = Path.Combine(tempFolder, "target1.txt");
|
||||
var target2 = Path.Combine(tempFolder, "target2.txt");
|
||||
var link = Path.Combine(tempFolder, "link.txt");
|
||||
|
||||
File.WriteAllText(target1, "content1");
|
||||
File.WriteAllText(target2, "content2");
|
||||
|
||||
// Create initial symlink
|
||||
SymbolicLink.Create(link, target1);
|
||||
Assert.Equal("content1", File.ReadAllText(link));
|
||||
|
||||
// Overwrite with new target
|
||||
SymbolicLink.Create(link, target2, overwrite: true);
|
||||
Assert.Equal("content2", File.ReadAllText(link));
|
||||
Assert.Equal(target2, SymbolicLink.GetTarget(link));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user