Fix bug compressing directories with deeply nested/recursive symlinks (#197)

This commit is contained in:
Caelan
2024-08-06 23:06:37 +01:00
committed by GitHub
parent 3128d34b14
commit 53de2732d7
2 changed files with 65 additions and 10 deletions

View File

@@ -61,8 +61,10 @@ namespace Velopack.Compression
if (!Utility.IsFileInDirectory(absolute, destinationDirectoryName)) { if (!Utility.IsFileInDirectory(absolute, destinationDirectoryName)) {
throw new IOException("IO_SymlinkTargetNotInDirectory"); throw new IOException("IO_SymlinkTargetNotInDirectory");
} }
SymbolicLink.Create(fileDestinationPath, absolute, true, true); SymbolicLink.Create(fileDestinationPath, absolute, true, true);
} }
return; return;
} }
@@ -94,6 +96,7 @@ namespace Velopack.Compression
await DeterministicCreateFromDirectoryAsync(directoryToCompress, outputFile, compressionLevel, progress, cancelToken).ConfigureAwait(false); await DeterministicCreateFromDirectoryAsync(directoryToCompress, outputFile, compressionLevel, progress, cancelToken).ConfigureAwait(false);
} catch { } catch {
try { File.Delete(outputFile); } catch { } try { File.Delete(outputFile); } catch { }
throw; throw;
} }
} }
@@ -101,7 +104,8 @@ namespace Velopack.Compression
private static char s_pathSeperator = '/'; private static char s_pathSeperator = '/';
public static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc); public static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel compressionLevel, private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName,
CompressionLevel compressionLevel,
Action<int> progress, CancellationToken cancelToken) Action<int> progress, CancellationToken cancelToken)
{ {
Encoding entryNameEncoding = Encoding.UTF8; Encoding entryNameEncoding = Encoding.UTF8;
@@ -114,20 +118,19 @@ namespace Velopack.Compression
long totalBytes = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length); long totalBytes = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length);
long processedBytes = 0L; long processedBytes = 0L;
var directories = directoryInfo var dirsToProcess = new Stack<DirectoryInfo>();
.EnumerateDirectories("*", SearchOption.AllDirectories) dirsToProcess.Push(directoryInfo);
.Concat(new[] { directoryInfo })
.OrderBy(f => f.FullName)
.ToArray();
foreach (var dir in directories) { while (dirsToProcess.Count > 0) {
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();
var dir = dirsToProcess.Pop();
// if dir is a symlink, write it as a file containing path to target // if dir is a symlink, write it as a file containing path to target
if (SymbolicLink.Exists(dir.FullName)) { if (SymbolicLink.Exists(dir.FullName)) {
if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(dir.FullName, relative: false), sourceDirectoryName)) { if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(dir.FullName, relative: false), sourceDirectoryName)) {
throw new IOException("IO_SymlinkTargetNotInDirectory"); throw new IOException("IO_SymlinkTargetNotInDirectory");
} }
string entryName = EntryFromPath(dir.FullName, fullName.Length, dir.FullName.Length - fullName.Length); string entryName = EntryFromPath(dir.FullName, fullName.Length, dir.FullName.Length - fullName.Length);
string symlinkTarget = SymbolicLink.GetTarget(dir.FullName, relative: true) string symlinkTarget = SymbolicLink.GetTarget(dir.FullName, relative: true)
.Replace(Path.DirectorySeparatorChar, s_pathSeperator) + s_pathSeperator; .Replace(Path.DirectorySeparatorChar, s_pathSeperator) + s_pathSeperator;
@@ -135,6 +138,7 @@ namespace Velopack.Compression
using (var writer = new StreamWriter(entry.Open())) { using (var writer = new StreamWriter(entry.Open())) {
await writer.WriteAsync(symlinkTarget).ConfigureAwait(false); await writer.WriteAsync(symlinkTarget).ConfigureAwait(false);
} }
continue; continue;
} }
@@ -146,7 +150,12 @@ namespace Velopack.Compression
continue; continue;
} }
// if none of the above, enumerate files and add them to the archive // if none of the above, this is just a regular folder - so we'll enumerate dirs and add them to the search stack and
// enumerate files and add them to the archive
foreach (var subdir in dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) {
dirsToProcess.Push(subdir);
}
var files = dir var files = dir
.EnumerateFiles("*", SearchOption.TopDirectoryOnly) .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
.OrderBy(f => f.FullName) .OrderBy(f => f.FullName)
@@ -163,12 +172,14 @@ namespace Velopack.Compression
if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(fileInfo.FullName, relative: false), sourceDirectoryName)) { if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(fileInfo.FullName, relative: false), sourceDirectoryName)) {
throw new IOException("IO_SymlinkTargetNotInDirectory"); throw new IOException("IO_SymlinkTargetNotInDirectory");
} }
string symlinkTarget = SymbolicLink.GetTarget(fileInfo.FullName, relative: true) string symlinkTarget = SymbolicLink.GetTarget(fileInfo.FullName, relative: true)
.Replace(Path.DirectorySeparatorChar, s_pathSeperator); .Replace(Path.DirectorySeparatorChar, s_pathSeperator);
var entry = zipArchive.CreateEntry(entryName + SYMLINK_EXT); var entry = zipArchive.CreateEntry(entryName + SYMLINK_EXT);
using (var writer = new StreamWriter(entry.Open())) { using (var writer = new StreamWriter(entry.Open())) {
await writer.WriteAsync(symlinkTarget).ConfigureAwait(false); await writer.WriteAsync(symlinkTarget).ConfigureAwait(false);
} }
continue; continue;
} }
@@ -225,4 +236,4 @@ namespace Velopack.Compression
return true; return true;
} }
} }
} }

View File

@@ -1,4 +1,7 @@
namespace Velopack.Tests; using System.IO.Compression;
using Velopack.Compression;
namespace Velopack.Tests;
public class SymbolicLinkTests public class SymbolicLinkTests
{ {
@@ -220,4 +223,45 @@ public class SymbolicLinkTests
Assert.Throws<IOException>(() => SymbolicLink.Delete(Path.Combine(tempFolder, "AFile"))); Assert.Throws<IOException>(() => SymbolicLink.Delete(Path.Combine(tempFolder, "AFile")));
} }
[Fact]
public async Task ComplexSymlinkDirGetsZippedCorrectly()
{
using var _1 = Utility.GetTempDirectory(out var tempFolder);
var temp = new DirectoryInfo(tempFolder);
var versions = temp.CreateSubdirectory("Versions");
var a = versions.CreateSubdirectory("A");
var resources = a.CreateSubdirectory("Resources");
File.WriteAllText(Path.Combine(resources.FullName, "Info.plist"), "Hello, Resources!");
File.WriteAllText(Path.Combine(a.FullName, "App"), "Hello, App!");
SymbolicLink.Create(Path.Combine(versions.FullName, "Current"), a.FullName, false, true);
SymbolicLink.Create(Path.Combine(temp.FullName, "Resources"), Path.Combine(versions.FullName, "Current", "Resources"), false, true);
SymbolicLink.Create(Path.Combine(temp.FullName, "App"), Path.Combine(versions.FullName, "Current", "App"), false, true);
using var _2 = Utility.GetTempDirectory(out var tempOutput);
var output = Path.Combine(tempOutput, "output.zip");
await EasyZip.CreateZipFromDirectoryAsync(NullLogger.Instance, output, tempFolder);
ZipFile.ExtractToDirectory(output, tempOutput);
var appSym = Path.Combine(tempOutput, "App.__symlink");
Assert.True(File.Exists(appSym));
Assert.Equal("Versions/Current/App", File.ReadAllText(appSym));
var resSym = Path.Combine(tempOutput, "Resources.__symlink");
Assert.True(File.Exists(resSym));
Assert.Equal("Versions/Current/Resources/", File.ReadAllText(resSym));
Assert.True(Directory.Exists(Path.Combine(tempOutput, "Versions")));
Assert.False(Directory.Exists(Path.Combine(tempOutput, "App")));
Assert.False(Directory.Exists(Path.Combine(tempOutput, "Resources")));
Assert.True(Directory.Exists(Path.Combine(tempOutput, "Versions", "A")));
Assert.False(Directory.Exists(Path.Combine(tempOutput, "Versions", "Current")));
Assert.False(File.Exists(Path.Combine(tempOutput, "Versions", "Current")));
var currentSym = Path.Combine(tempOutput, "Versions", "Current.__symlink");
Assert.True(File.Exists(currentSym));
Assert.Equal("A/", File.ReadAllText(currentSym));
}
} }