diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs index 5d1250493..1beacfd09 100644 --- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -559,12 +559,16 @@ public async Task InstallNodeIfNecessary(IProgress? progress = n ) ); - // unzip - await ArchiveHelper.Extract7ZAuto(nodeDownloadPath, AssetsDir); - var nodeDir = Compat.IsMacOS ? AssetsDir.JoinDir("node-v20.19.3-darwin-arm64") : AssetsDir.JoinDir("node-v20.19.3-linux-x64"); + + // Node's tarball contains relative symlinks from bin/ into lib/ (e.g. bin/npm -> ../lib/...). + // Bundled 7-Zip 26.01's default symlink checks reject these ../lib links, regardless of filesystem. + // -snld6 was the lowest level that accepted them in testing, so relax the check for this archive only. + // A filesystem that cannot create symlinks (e.g. exFAT) may still fail independently. + await ArchiveHelper.Extract7ZTar(nodeDownloadPath, AssetsDir, symlinkSecurityLevel: 6); + Directory.Move(nodeDir, NodeDir); progress?.Report( diff --git a/StabilityMatrix.Core/Helper/ArchiveHelper.cs b/StabilityMatrix.Core/Helper/ArchiveHelper.cs index bd05b7033..b5dd87234 100644 --- a/StabilityMatrix.Core/Helper/ArchiveHelper.cs +++ b/StabilityMatrix.Core/Helper/ArchiveHelper.cs @@ -86,9 +86,20 @@ public static async Task AddToArchive7Z(string archivePath, string sourceDirecto result.EnsureSuccessExitCode(); } - public static async Task Extract7Z(string archivePath, string extractDirectory) + public static async Task Extract7Z( + string archivePath, + string extractDirectory, + int? symlinkSecurityLevel = null + ) { - var args = $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y"; + if (symlinkSecurityLevel is < 0 or > 20) + { + throw new ArgumentOutOfRangeException(nameof(symlinkSecurityLevel), "Must be between 0 and 20."); + } + + var symlinkSecuritySwitch = symlinkSecurityLevel is { } level ? $" -snld{level}" : string.Empty; + var args = + $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y{symlinkSecuritySwitch}"; var result = await ProcessRunner .GetProcessResultAsync(SevenZipPath, args) @@ -176,7 +187,11 @@ public static async Task Extract7Z( /// /// /// - public static async Task Extract7ZTar(string archivePath, string extractDirectory) + public static async Task Extract7ZTar( + string archivePath, + string extractDirectory, + int? symlinkSecurityLevel = null + ) { if (!archivePath.EndsWith(".tar.gz")) { @@ -194,7 +209,7 @@ public static async Task Extract7ZTar(string archivePath, string ex try { - return await Extract7Z(tarPath, extractDirectory).ConfigureAwait(false); + return await Extract7Z(tarPath, extractDirectory, symlinkSecurityLevel).ConfigureAwait(false); } finally { @@ -251,7 +266,7 @@ public static async Task Extract( var progressMonitor = progress switch { null => null, - _ => new Timer(TimeSpan.FromMilliseconds(36)) + _ => new Timer(TimeSpan.FromMilliseconds(36)), }; if (progressMonitor != null) @@ -264,28 +279,29 @@ public static async Task Extract( }; } - await Task.Factory.StartNew( - () => - { - var extractOptions = new ExtractionOptions { Overwrite = true, ExtractFullPath = true, }; - using var stream = File.OpenRead(archivePath); - using var archive = ReaderFactory.Open(stream); + await Task + .Factory.StartNew( + () => + { + var extractOptions = new ExtractionOptions { Overwrite = true, ExtractFullPath = true }; + using var stream = File.OpenRead(archivePath); + using var archive = ReaderFactory.Open(stream); - // Start the progress reporting timer - progressMonitor?.Start(); + // Start the progress reporting timer + progressMonitor?.Start(); - while (archive.MoveToNextEntry()) - { - var entry = archive.Entry; - if (!entry.IsDirectory) + while (archive.MoveToNextEntry()) { - count += (ulong)entry.CompressedSize; + var entry = archive.Entry; + if (!entry.IsDirectory) + { + count += (ulong)entry.CompressedSize; + } + archive.WriteEntryToDirectory(outputDirectory, extractOptions); } - archive.WriteEntryToDirectory(outputDirectory, extractOptions); - } - }, - TaskCreationOptions.LongRunning - ) + }, + TaskCreationOptions.LongRunning + ) .ConfigureAwait(false); progress?.Report(new ProgressReport(progress: 1, message: "Done extracting"));