From 99723d91dda1c5f404585315f98d1a9f58426593 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 21 Oct 2026 11:23:34 -0400 Subject: [PATCH] fix: don't split real paths on `!/` in `LocalPath` and `ContainerPath` `LocalPath` searched `FullNestedPath()` for the last `!/`, which is just `RealPath` for a file not in an archive. A host file like `/data/Hello!/world.txt` came back as `/world.txt`, so lookups and exclusions ran against the wrong path even with nested cataloging off. `ContainerPath` had the same problem when `FullPath` was set to `RealPath`. Both now cut `RealPath` off first and only look for a separator in the entry chain after it, which can't contain `!/` since entry names with it are refused. Signed-off-by: Alex Goodman --- internal/archive/traversal.go | 9 ++++++--- internal/archive/traversal_test.go | 3 +++ syft/file/coordinates.go | 16 ++++++++++------ syft/file/location_test.go | 4 ++++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/archive/traversal.go b/internal/archive/traversal.go index de8b0da62..ddb84a02d 100644 --- a/internal/archive/traversal.go +++ b/internal/archive/traversal.go @@ -90,11 +90,14 @@ func (t *Traversal) FullTraversalArchivePath() string { // ContainerPath returns the full path of the archive directly holding the file at c, or blank for a // file on the scanned filesystem, whether or not its FullPath is set. func ContainerPath(c file.Coordinates) string { - i := strings.LastIndex(c.FullPath, file.ArchivePathSeparator) - if i < 0 { + entries, ok := archiveEntries(c) + if !ok { return "" } - return c.FullPath[:i] + if i := strings.LastIndex(entries, file.ArchivePathSeparator); i >= 0 { + return c.RealPath + file.ArchivePathSeparator + entries[:i] + } + return c.RealPath } // archiveEntries is the "!/"-delimited chain of entry paths after RealPath, reporting false for a file diff --git a/internal/archive/traversal_test.go b/internal/archive/traversal_test.go index 15010c697..73d0306b0 100644 --- a/internal/archive/traversal_test.go +++ b/internal/archive/traversal_test.go @@ -32,6 +32,9 @@ func TestContainerPath(t *testing.T) { assert.Equal(t, "", ContainerPath(file.NewCoordinates("/app.war", ""))) assert.Equal(t, "", ContainerPath(file.Coordinates{RealPath: "/app.war", FullPath: "/app.war"}), "a top-level file whose FullPath is set") assert.Equal(t, "", ContainerPath(file.Coordinates{RealPath: "/odd!/name"}), "a separator in a top-level real path is not an archive boundary") + assert.Equal(t, "", ContainerPath(file.Coordinates{RealPath: "/odd!/name", FullPath: "/odd!/name"}), "nor when FullPath is set") + assert.Equal(t, "/odd!/name.zip", ContainerPath(locationIn("/odd!/name.zip", "a.txt").Coordinates)) + assert.Equal(t, "/odd!/name.zip!/b.jar", ContainerPath(locationIn("/odd!/name.zip", "b.jar!/a.txt").Coordinates)) assert.Equal(t, "/app.war", ContainerPath(locationIn("/app.war", "WEB-INF/lib/dep.jar").Coordinates)) assert.Equal(t, "/app.war!/WEB-INF/lib/dep.jar", ContainerPath(locationIn("/app.war", "WEB-INF/lib/dep.jar!/META-INF/MANIFEST.MF").Coordinates)) diff --git a/syft/file/coordinates.go b/syft/file/coordinates.go index c496aad67..3645c534d 100644 --- a/syft/file/coordinates.go +++ b/syft/file/coordinates.go @@ -30,14 +30,18 @@ func (c Coordinates) FullNestedPath() string { return c.FullPath } -// LocalPath is the rooted path within the resolver holding the file: the segment of FullNestedPath after -// the last archive separator, keeping the separator's slash, or RealPath for a file not within an archive. +// LocalPath is the rooted path within the resolver holding the file: the last entry path in FullPath, or +// RealPath for a file not within an archive. RealPath is cut off before looking for a separator, so a +// "!/" in a real path is never taken for an archive boundary. func (c Coordinates) LocalPath() string { - fullPath := c.FullNestedPath() - if i := strings.LastIndex(fullPath, ArchivePathSeparator); i >= 0 { - return fullPath[i+len(ArchivePathSeparator)-1:] + entries, ok := strings.CutPrefix(c.FullPath, c.RealPath+ArchivePathSeparator) + if !ok { + return c.RealPath } - return fullPath + if i := strings.LastIndex(entries, ArchivePathSeparator); i >= 0 { + entries = entries[i+len(ArchivePathSeparator):] + } + return "/" + entries } func NewCoordinates(realPath, fsID string) Coordinates { diff --git a/syft/file/location_test.go b/syft/file/location_test.go index 7ea14de63..34074c7fa 100644 --- a/syft/file/location_test.go +++ b/syft/file/location_test.go @@ -106,4 +106,8 @@ func TestCoordinates_LocalPath(t *testing.T) { assert.Equal(t, "/a.jar", NewCoordinates("/a.jar", "").LocalPath()) assert.Equal(t, "/a.jar", Coordinates{RealPath: "/a.jar"}.LocalPath()) assert.Equal(t, "/META-INF/MANIFEST.MF", Coordinates{RealPath: "/a.war", FullPath: "/a.war!/lib/b.jar!/META-INF/MANIFEST.MF"}.LocalPath()) + assert.Equal(t, "/data/Hello!/world.txt", NewCoordinates("/data/Hello!/world.txt", "").LocalPath(), "a separator in a real path is not an archive boundary") + assert.Equal(t, "/data/Hello!/world.txt", Coordinates{RealPath: "/data/Hello!/world.txt", FullPath: "/data/Hello!/world.txt"}.LocalPath()) + assert.Equal(t, "/x.txt", Coordinates{RealPath: "/data/Hello!/a.zip", FullPath: "/data/Hello!/a.zip!/x.txt"}.LocalPath()) + assert.Equal(t, "/x.txt", Coordinates{RealPath: "/data/Hello!/a.zip", FullPath: "/data/Hello!/a.zip!/b.jar!/x.txt"}.LocalPath()) }