From b9fdabc2f620f8a6bf2ab67b34ceac6974e7bc0e Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 21 Oct 2026 11:30:31 -0400 Subject: [PATCH] fix: make `Location.Path()` always the full path `Path()` fell back to `LocalPath()` when `AccessPath` was blank, so for a nested file it returned the full chain from a resolver but the archive-local path from a hand-built or decoded location, and callers couldn't tell which they had. - `Path()` now falls back to `FullNestedPath()`. Root files are unchanged - the maven parent pom lookup joins `relativePath` onto `LocalPath()`, since enough `../` on the full path cleaned past the `!/` boundary into an outer path that the archive resolver then read as archive-local Signed-off-by: Alex Goodman --- syft/file/location.go | 5 ++-- syft/file/location_test.go | 9 +++++++ .../cataloger/java/internal/maven/resolver.go | 3 ++- .../java/internal/maven/resolver_test.go | 24 +++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/syft/file/location.go b/syft/file/location.go index 18651ee71..14256ffa3 100644 --- a/syft/file/location.go +++ b/syft/file/location.go @@ -205,12 +205,13 @@ func NewVirtualLocationFromDirectory(responsePath, responseAccessPath string, re } } -// Path is the path this location was accessed by, or its local path (see Coordinates.LocalPath). +// Path is the path this location was accessed by, or its full path (see Coordinates.FullNestedPath). For a +// file within an archive both are full paths, so use LocalPath for path arithmetic against the resolver. func (l Location) Path() string { if l.AccessPath != "" { return l.AccessPath } - return l.LocalPath() + return l.FullNestedPath() } func (l Location) String() string { diff --git a/syft/file/location_test.go b/syft/file/location_test.go index 34074c7fa..5302c951a 100644 --- a/syft/file/location_test.go +++ b/syft/file/location_test.go @@ -111,3 +111,12 @@ func TestCoordinates_LocalPath(t *testing.T) { 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()) } + +func TestLocation_Path(t *testing.T) { + nested := Coordinates{RealPath: "/a.war", FullPath: "/a.war!/lib/b.jar!/META-INF/MANIFEST.MF"} + assert.Equal(t, "/a.jar", NewLocation("/a.jar").Path()) + assert.Equal(t, "/link", NewVirtualLocation("/a.jar", "/link").Path()) + assert.Equal(t, nested.FullPath, NewLocationFromCoordinates(nested).Path()) + assert.Equal(t, nested.FullPath, Location{LocationData: LocationData{Coordinates: nested}}.Path(), "a nested location without an access path is still full") + assert.Equal(t, nested.FullPath, NewVirtualLocationFromCoordinates(nested, nested.FullPath).Path()) +} diff --git a/syft/pkg/cataloger/java/internal/maven/resolver.go b/syft/pkg/cataloger/java/internal/maven/resolver.go index 67f3b25bb..36192fd8c 100644 --- a/syft/pkg/cataloger/java/internal/maven/resolver.go +++ b/syft/pkg/cataloger/java/internal/maven/resolver.go @@ -647,7 +647,8 @@ func (r *Resolver) findParentPomByRelativePath(ctx context.Context, pom *Project if relativePath == "" { return nil } - p := pomLocation.Path() + // relative to the pom within its own archive, so "../" can't clean past the archive boundary + p := pomLocation.LocalPath() p = path.Dir(p) p = path.Join(p, relativePath) p = path.Clean(p) diff --git a/syft/pkg/cataloger/java/internal/maven/resolver_test.go b/syft/pkg/cataloger/java/internal/maven/resolver_test.go index 259a55c45..44cda5ec3 100644 --- a/syft/pkg/cataloger/java/internal/maven/resolver_test.go +++ b/syft/pkg/cataloger/java/internal/maven/resolver_test.go @@ -6,9 +6,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/fileresolver" maventest "github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven/test" ) @@ -368,3 +370,25 @@ func Test_relativePathParent(t *testing.T) { func ptr[T any](value T) *T { return &value } + +// pathRecorder records the paths it is asked for and finds nothing. +type pathRecorder struct { + file.Resolver + paths []string +} + +func (p *pathRecorder) FilesByPath(paths ...string) ([]file.Location, error) { + p.paths = append(p.paths, paths...) + return nil, nil +} + +func Test_relativePathParent_staysInsideTheArchive(t *testing.T) { + recorder := &pathRecorder{} + r := NewResolver(recorder, DefaultConfig()) + pom := &Project{Parent: &Parent{RelativePath: ptr("../../other")}} + nested := file.Coordinates{RealPath: "/data/outer.zip", FullPath: "/data/outer.zip!/a/pom.xml"} + r.pomLocations[pom] = file.NewVirtualLocationFromCoordinates(nested, nested.FullPath) + + assert.Nil(t, r.findParentPomByRelativePath(context.Background(), pom, ID{}, nil)) + assert.Equal(t, []string{"/other/pom.xml"}, recorder.paths) +}