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()); 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 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 } } }