Files
syft/internal/archive/index_test.go
T
2026-09-22 00:23:36 -04:00

272 lines
8.2 KiB
Go

package archive
import (
"archive/tar"
"bytes"
"context"
"io"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
)
func TestIndex_globByExtensionUsesTheNameIndex(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByGlob("**/*.jar")
require.NoError(t, err)
assert.Equal(t, []string{"/BOOT-INF/lib/inner.jar"}, relPaths(t, r, locations))
}
func TestIndex_globByExactNameAtAnyDepth(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByGlob("**/pom.properties")
require.NoError(t, err)
assert.Equal(t, []string{"/META-INF/maven/com.example/lib/pom.properties"}, relPaths(t, r, locations))
}
func TestIndex_globFindsRootLevelFiles(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByGlob("/*")
require.NoError(t, err)
assert.Equal(t, []string{"/LICENSE"}, relPaths(t, r, locations))
}
func TestIndex_globOneDirectoryDeep(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByGlob("/META-INF/*")
require.NoError(t, err)
assert.Equal(t, []string{"/META-INF/MANIFEST.MF"}, relPaths(t, r, locations),
"one level only: the maven files are deeper")
}
func TestIndex_globAcrossSyntheticDirectories(t *testing.T) {
// the archive never lists META-INF/ or META-INF/maven/
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByGlob("**/maven/*/*/pom.xml")
require.NoError(t, err)
assert.Equal(t, []string{"/META-INF/maven/com.example/lib/pom.xml"}, relPaths(t, r, locations))
}
func TestIndex_globHonorsNonWildcardMetacharacters(t *testing.T) {
// a segment with any metacharacter other than one `*` must reach doublestar rather than be looked
// up as a literal name
r := indexOver(t, 1<<20, map[string]string{
"usr/local/go/bin/go": "binary",
"usr/lib/libstd-0123456789abcdef.so": "binary",
"opt/a.jar": "PK\x03\x04",
"opt/b.war": "PK\x03\x04",
"opt/notes.txt": "prose",
})
tests := []struct {
pattern string
want []string
}{
{"**/{go,go.exe}", []string{"/usr/local/go/bin/go"}},
{"**/libstd-????????????????.so", []string{"/usr/lib/libstd-0123456789abcdef.so"}},
{"**/*.[jw]ar", []string{"/opt/a.jar", "/opt/b.war"}},
{"**/{a,b}.{jar,war}", []string{"/opt/a.jar", "/opt/b.war"}},
{"**/*.jar", []string{"/opt/a.jar"}},
{"**/go", []string{"/usr/local/go/bin/go"}},
{"**/notes*", []string{"/opt/notes.txt"}},
}
for _, tt := range tests {
t.Run(tt.pattern, func(t *testing.T) {
locations, err := r.FilesByGlob(tt.pattern)
require.NoError(t, err)
assert.Equal(t, tt.want, relPaths(t, r, locations))
})
}
}
func TestIndex_globMetacharactersInAMiddleSegment(t *testing.T) {
r := indexOver(t, 1<<20, map[string]string{
"META-INF/maven/com.example/lib/pom.xml": "<project/>",
"WEB-INF/lib/dep.jar": "PK\x03\x04",
})
locations, err := r.FilesByGlob("/{META-INF,WEB-INF}/**/*.xml")
require.NoError(t, err)
assert.Equal(t, []string{"/META-INF/maven/com.example/lib/pom.xml"}, relPaths(t, r, locations))
}
func TestIndex_pathsAndContents(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
locations, err := r.FilesByPath("/META-INF/MANIFEST.MF")
require.NoError(t, err)
require.Len(t, locations, 1)
assert.True(t, r.HasPath("/META-INF/MANIFEST.MF"))
assert.True(t, r.HasPath("META-INF/MANIFEST.MF"), "a path may arrive with or without the leading slash")
assert.False(t, r.HasPath("/nope"))
reader, err := r.FileContentsByLocation(locations[0])
require.NoError(t, err)
body, err := io.ReadAll(reader)
require.NoError(t, err)
require.NoError(t, reader.Close())
assert.Equal(t, "Manifest-Version: 1.0\n", string(body))
}
func TestIndex_contentsSurviveTheMoveToDisk(t *testing.T) {
r := indexOver(t, 8, jarEntriesFixture)
locations, err := r.FilesByGlob("**/*.jar")
require.NoError(t, err)
require.Len(t, locations, 1)
reader, err := r.FileContentsByLocation(locations[0])
require.NoError(t, err)
body, err := io.ReadAll(reader)
require.NoError(t, err)
require.NoError(t, reader.Close())
assert.Equal(t, "PK\x03\x04 not really", string(body))
}
func TestIndex_mimeTypesAreSniffedFromContent(t *testing.T) {
r := indexOver(t, 1<<20, map[string]string{
"notes.txt": "just some text",
"run.sh": "#!/bin/bash\necho hello\n",
})
locations, err := r.FilesByMIMEType("text/x-shellscript")
require.NoError(t, err)
assert.Equal(t, []string{"/run.sh"}, relPaths(t, r, locations))
}
func TestIndex_allLocationsCoversEveryFile(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
var got []string
for loc := range r.AllLocations(context.Background()) {
got = append(got, "/"+loc.RealPath)
}
sort.Strings(got)
assert.Equal(t, []string{
"/BOOT-INF/lib/inner.jar",
"/LICENSE",
"/META-INF/MANIFEST.MF",
"/META-INF/maven/com.example/lib/pom.properties",
"/META-INF/maven/com.example/lib/pom.xml",
"/com/example/Thing.class",
}, got)
}
func TestIndex_answersAreOrdered(t *testing.T) {
// order decides which locations group into which package when results merge into the SBOM
entries := map[string]string{}
for _, name := range []string{
"b/z.jar", "a/y.jar", "c/x.jar", "a/b/c/w.jar", "z.jar", "a.jar",
} {
entries[name] = "PK\x03\x04 not really"
}
r := indexOver(t, 1<<20, entries)
first, err := r.FilesByGlob("**/*.jar")
require.NoError(t, err)
require.Len(t, first, len(entries))
byMIME, err := r.FilesByMIMEType("application/octet-stream")
require.NoError(t, err)
for range 8 {
again, err := r.FilesByGlob("**/*.jar")
require.NoError(t, err)
assert.Equal(t, first, again)
mimeAgain, err := r.FilesByMIMEType("application/octet-stream")
require.NoError(t, err)
assert.Equal(t, byMIME, mimeAgain)
var all []string
for loc := range r.AllLocations(context.Background()) {
all = append(all, loc.RealPath)
}
assert.True(t, sort.StringsAreSorted(all))
}
var paths []string
for _, loc := range first {
paths = append(paths, loc.RealPath)
}
assert.True(t, sort.StringsAreSorted(paths), "ordered by path, not merely stable")
}
// storeWith builds an entry store holding the given entries in name order. A name ending in "/" with
// an empty body is a directory.
func storeWith(t testing.TB, maxInMemory int64, entries map[string]string) *EntryStore {
t.Helper()
store := storeFor(t, memCharge(maxInMemory))
names := make([]string, 0, len(entries))
for name := range entries {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
body := entries[name]
if body == "" && name[len(name)-1] == '/' {
require.NoError(t, store.Add(tar.Header{Name: name, Mode: 0o755, Typeflag: tar.TypeDir}, nil))
continue
}
require.NoError(t, store.Add(regularHeader(name, int64(len(body))), bytes.NewReader([]byte(body))))
}
return store
}
func indexOver(t testing.TB, maxInMemory int64, entries map[string]string) *Index {
t.Helper()
return NewIndex(storeWith(t, maxInMemory, entries), "", "outer.jar")
}
func relPaths(t *testing.T, r *Index, locations []file.Location) []string {
t.Helper()
var out []string
for _, loc := range locations {
out = append(out, "/"+loc.RealPath)
}
sort.Strings(out)
return out
}
var jarEntriesFixture = map[string]string{
"META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n",
"META-INF/maven/com.example/lib/pom.properties": "groupId=com.example\n",
"META-INF/maven/com.example/lib/pom.xml": "<project/>",
"LICENSE": "Apache License 2.0",
"BOOT-INF/lib/inner.jar": "PK\x03\x04 not really",
"com/example/Thing.class": "bytecode",
}
func TestIndex_impliedDirectoriesArePathsButNotFiles(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
assert.True(t, r.HasPath("META-INF"))
assert.True(t, r.HasPath("/META-INF/maven"))
locations, err := r.FilesByPath("META-INF")
require.NoError(t, err)
assert.Empty(t, locations)
locations, err = r.FilesByGlob("**/maven")
require.NoError(t, err)
assert.Empty(t, locations)
}
func TestIndex_invalidGlobIsAnError(t *testing.T) {
r := indexOver(t, 1<<20, jarEntriesFixture)
_, err := r.FilesByGlob("**/[")
assert.Error(t, err)
}