Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/LayeredRootFileSystemTests.cs
2025-10-28 15:10:40 +02:00

177 lines
7.1 KiB
C#

using System;
using System.Formats.Tar;
using System.IO;
using System.Text;
using Xunit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class LayeredRootFileSystemTests : IDisposable
{
private readonly string _tempRoot;
public LayeredRootFileSystemTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-layerfs-{Guid.NewGuid():n}");
Directory.CreateDirectory(_tempRoot);
}
[Fact]
public void FromDirectories_HandlesWhiteoutsAndResolution()
{
var layer1 = CreateLayerDirectory("layer1");
var layer2 = CreateLayerDirectory("layer2");
var usrBin1 = Path.Combine(layer1, "usr", "bin");
Directory.CreateDirectory(usrBin1);
var entrypointPath = Path.Combine(usrBin1, "entrypoint.sh");
File.WriteAllText(entrypointPath, "#!/bin/sh\necho layer1\n");
var optDirectory1 = Path.Combine(layer1, "opt");
Directory.CreateDirectory(optDirectory1);
File.WriteAllText(Path.Combine(optDirectory1, "setup.sh"), "echo setup\n");
var optDirectory2 = Path.Combine(layer2, "opt");
Directory.CreateDirectory(optDirectory2);
File.WriteAllText(Path.Combine(optDirectory2, ".wh.setup.sh"), string.Empty);
var fs = LayeredRootFileSystem.FromDirectories(new[]
{
new LayeredRootFileSystem.LayerDirectory("sha256:layer1", layer1),
new LayeredRootFileSystem.LayerDirectory("sha256:layer2", layer2)
});
Assert.True(fs.TryResolveExecutable("entrypoint.sh", new[] { "/usr/bin" }, out var descriptor));
Assert.Equal("/usr/bin/entrypoint.sh", descriptor.Path);
Assert.Equal("sha256:layer1", descriptor.LayerDigest);
Assert.True(fs.TryReadAllText("/usr/bin/entrypoint.sh", out var textDescriptor, out var content));
Assert.Equal(descriptor.Path, textDescriptor.Path);
Assert.Contains("echo layer1", content);
Assert.False(fs.TryReadAllText("/opt/setup.sh", out _, out _));
var optEntries = fs.EnumerateDirectory("/opt");
Assert.DoesNotContain(optEntries, entry => entry.Path.EndsWith("setup.sh", StringComparison.Ordinal));
}
[Fact]
public void FromArchives_ResolvesSymlinkAndWhiteout()
{
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
var layer2Path = Path.Combine(_tempRoot, "layer2.tar");
CreateArchive(layer1Path, writer =>
{
var scriptEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/local/bin/start.sh");
scriptEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
scriptEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho start\n"));
writer.WriteEntry(scriptEntry);
var oldScript = new PaxTarEntry(TarEntryType.RegularFile, "opt/old.sh");
oldScript.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
oldScript.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("echo old\n"));
writer.WriteEntry(oldScript);
});
CreateArchive(layer2Path, writer =>
{
var symlinkEntry = new PaxTarEntry(TarEntryType.SymbolicLink, "usr/bin/start.sh");
symlinkEntry.LinkName = "/usr/local/bin/start.sh";
writer.WriteEntry(symlinkEntry);
var whiteout = new PaxTarEntry(TarEntryType.RegularFile, "opt/.wh.old.sh");
whiteout.DataStream = new MemoryStream(Array.Empty<byte>());
writer.WriteEntry(whiteout);
});
var fs = LayeredRootFileSystem.FromArchives(new[]
{
new LayeredRootFileSystem.LayerArchive("sha256:base", layer1Path),
new LayeredRootFileSystem.LayerArchive("sha256:update", layer2Path)
});
Assert.True(fs.TryResolveExecutable("start.sh", new[] { "/usr/bin" }, out var descriptor));
Assert.Equal("/usr/local/bin/start.sh", descriptor.Path);
Assert.Equal("sha256:base", descriptor.LayerDigest);
Assert.True(fs.TryReadAllText("/usr/bin/start.sh", out var resolvedDescriptor, out var content));
Assert.Equal(descriptor.Path, resolvedDescriptor.Path);
Assert.Contains("echo start", content);
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
}
[Fact]
public void FromArchives_ResolvesHardLinkContent()
{
var baseLayer = Path.Combine(_tempRoot, "base.tar");
var hardLinkLayer = Path.Combine(_tempRoot, "hardlink.tar");
CreateArchive(baseLayer, writer =>
{
var baseEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/bin/tool.sh");
baseEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
baseEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho tool\n"));
writer.WriteEntry(baseEntry);
});
CreateArchive(hardLinkLayer, writer =>
{
var hardLink = new PaxTarEntry(TarEntryType.HardLink, "bin/tool.sh")
{
LinkName = "/usr/bin/tool.sh",
Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute
};
writer.WriteEntry(hardLink);
});
var fs = LayeredRootFileSystem.FromArchives(new[]
{
new LayeredRootFileSystem.LayerArchive("sha256:base", baseLayer),
new LayeredRootFileSystem.LayerArchive("sha256:hardlink", hardLinkLayer)
});
Assert.True(fs.TryReadAllText("/bin/tool.sh", out var descriptor, out var content));
Assert.Equal("/usr/bin/tool.sh", descriptor.Path);
Assert.Contains("echo tool", content);
}
private string CreateLayerDirectory(string name)
{
var path = Path.Combine(_tempRoot, name);
Directory.CreateDirectory(path);
return path;
}
private static void CreateArchive(string path, Action<TarWriter> writerAction)
{
using var stream = File.Create(path);
using var writer = new TarWriter(stream, leaveOpen: false);
writerAction(writer);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempRoot))
{
Directory.Delete(_tempRoot, recursive: true);
}
}
catch
{
// ignore cleanup failures
}
}
}