mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
fix: make ArchivePath part of file ordering and Location identity
syft-json output wasn't reproducible for nested archives. `AllCoordinates()` comes from map iteration, and the coordinate and location sorters ignored `ArchivePath`, so the same path in two archives (e.g. `META-INF/MANIFEST.MF` in two jars in a war) compared equal and came out in random order. Package locations had the same problem. - `ArchivePath` is now the final tiebreak in `LocationSorter`, `CoordinatesSorter` and the `CoordinateSet.ToSlice` fallback - `Location.Equals` and `Location.String` include `ArchivePath`, which matches `Coordinates`, since the same path in different archives is a different file Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -78,6 +78,9 @@ func (s CoordinateSet) ToSlice(sorters ...func(a, b Coordinates) int) []Coordina
|
||||
if !sorted {
|
||||
sort.SliceStable(coordinates, func(i, j int) bool {
|
||||
if coordinates[i].FileSystemID == coordinates[j].FileSystemID {
|
||||
if coordinates[i].RealPath == coordinates[j].RealPath {
|
||||
return coordinates[i].ArchivePath < coordinates[j].ArchivePath
|
||||
}
|
||||
return coordinates[i].RealPath < coordinates[j].RealPath
|
||||
}
|
||||
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
|
||||
|
||||
@@ -224,6 +224,10 @@ func (l Location) String() string {
|
||||
str += fmt.Sprintf(" AccessPath=%q", l.AccessPath)
|
||||
}
|
||||
|
||||
if l.ArchivePath != "" {
|
||||
str += fmt.Sprintf(" ArchivePath=%q", l.ArchivePath)
|
||||
}
|
||||
|
||||
if l.FileSystemID != "" {
|
||||
str += fmt.Sprintf(" Layer=%q", l.FileSystemID)
|
||||
}
|
||||
@@ -233,5 +237,6 @@ func (l Location) String() string {
|
||||
func (l Location) Equals(other Location) bool {
|
||||
return l.RealPath == other.RealPath &&
|
||||
l.AccessPath == other.AccessPath &&
|
||||
l.ArchivePath == other.ArchivePath &&
|
||||
l.FileSystemID == other.FileSystemID
|
||||
}
|
||||
|
||||
@@ -48,3 +48,12 @@ func TestLocation_ID(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLocation_archivePathIsIdentity(t *testing.T) {
|
||||
a := NewLocationFromCoordinates(Coordinates{RealPath: "META-INF/MANIFEST.MF", ArchivePath: "/a.jar"})
|
||||
b := NewLocationFromCoordinates(Coordinates{RealPath: "META-INF/MANIFEST.MF", ArchivePath: "/b.jar"})
|
||||
|
||||
assert.False(t, a.Equals(b))
|
||||
assert.NotEqual(t, a.String(), b.String())
|
||||
assert.Contains(t, a.String(), `ArchivePath="/a.jar"`)
|
||||
}
|
||||
|
||||
+12
-2
@@ -83,7 +83,12 @@ func LocationSorter(layers []string) func(a, b Location) int { //nolint:gocognit
|
||||
return strings.Compare(a.AccessPath, b.AccessPath)
|
||||
}
|
||||
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
if a.RealPath != b.RealPath {
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
}
|
||||
|
||||
// the same path in different archives is a different file
|
||||
return strings.Compare(a.ArchivePath, b.ArchivePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +123,11 @@ func CoordinatesSorter(layers []string) func(a, b Coordinates) int {
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
if a.RealPath != b.RealPath {
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
}
|
||||
|
||||
// the same path in different archives is a different file
|
||||
return strings.Compare(a.ArchivePath, b.ArchivePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/evidence"
|
||||
)
|
||||
@@ -555,3 +556,24 @@ func TestLocationAndCoordinatesSorters(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSorters_archivePathBreaksTies(t *testing.T) {
|
||||
// the same path in two archives must sort the same way whatever order the inputs arrive in, or
|
||||
// output built from map iteration is not reproducible
|
||||
a := Coordinates{RealPath: "META-INF/MANIFEST.MF", ArchivePath: "/app.war:WEB-INF/lib/a.jar"}
|
||||
b := Coordinates{RealPath: "META-INF/MANIFEST.MF", ArchivePath: "/app.war:WEB-INF/lib/b.jar"}
|
||||
want := []string{a.ArchivePath, b.ArchivePath}
|
||||
|
||||
for _, in := range [][]Coordinates{{a, b}, {b, a}} {
|
||||
coords := slices.Clone(in)
|
||||
slices.SortFunc(coords, CoordinatesSorter(nil))
|
||||
assert.Equal(t, want, []string{coords[0].ArchivePath, coords[1].ArchivePath}, "CoordinatesSorter")
|
||||
|
||||
locs := []Location{NewLocationFromCoordinates(in[0]), NewLocationFromCoordinates(in[1])}
|
||||
slices.SortFunc(locs, LocationSorter(nil))
|
||||
assert.Equal(t, want, []string{locs[0].ArchivePath, locs[1].ArchivePath}, "LocationSorter")
|
||||
|
||||
got := NewCoordinateSet(in...).ToSlice()
|
||||
assert.Equal(t, want, []string{got[0].ArchivePath, got[1].ArchivePath}, "CoordinateSet.ToSlice")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -89,6 +90,30 @@ func TestEscapeHTML(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncode_sameFileInManyArchivesIsReproducible(t *testing.T) {
|
||||
// files and package locations that differ only by archive path come out of map iteration, so the
|
||||
// output is reproducible only if the sort breaks that tie
|
||||
s := testutil.DirectoryInput(t, t.TempDir())
|
||||
s.Artifacts.FileMetadata = map[file.Coordinates]file.Metadata{}
|
||||
p := pkg.Package{Name: "shared", Version: "1.0"}
|
||||
for i := range 20 {
|
||||
c := file.Coordinates{RealPath: "META-INF/MANIFEST.MF", ArchivePath: fmt.Sprintf("/app.war:WEB-INF/lib/%02d.jar", i)}
|
||||
s.Artifacts.FileMetadata[c] = file.Metadata{}
|
||||
p.Locations.Add(file.NewLocationFromCoordinates(c))
|
||||
}
|
||||
s.Artifacts.Packages.Add(p)
|
||||
|
||||
encode := func() string {
|
||||
var buffer bytes.Buffer
|
||||
require.NoError(t, NewFormatEncoder().Encode(&buffer, s))
|
||||
return buffer.String()
|
||||
}
|
||||
first := encode()
|
||||
for range 10 {
|
||||
require.Equal(t, first, encode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryEncoder(t *testing.T) {
|
||||
cfg := DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
|
||||
Reference in New Issue
Block a user