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

257 lines
6.6 KiB
Go

package archive
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/tmpdir"
)
// WorkDirAt returns a WorkDir over a directory that already exists, so a test can inspect it
// afterwards; Remove leaves it alone.
func WorkDirAt(dir string) *WorkDir {
return &WorkDir{created: true, path: dir}
}
func (w *WorkDir) wasCreated() bool {
return w != nil && w.remove != nil
}
// heldInMemory reports the entry content the store is still holding in memory.
func (s *EntryStore) heldInMemory() int64 {
var total int64
for _, entry := range s.entries {
total += int64(len(entry.mem))
}
return total
}
// held reports what this charge is currently holding, in memory and on disk.
func (c *Charge) held() (memory, disk int64) {
if c == nil {
return 0, 0
}
c.limiter.mu.Lock()
defer c.limiter.mu.Unlock()
return c.memory, c.disk
}
// unseekable hides a reader's Seek and ReadAt, forcing content through the held path.
type unseekable struct{ io.Reader }
// testEntry is one entry of a fixture archive: a directory when dir is set, a symlink when link is
// set, otherwise a regular file.
type testEntry struct {
name string
body string
link string
dir bool
mode fs.FileMode
}
func fileEntries(files map[string]string) []testEntry {
names := make([]string, 0, len(files))
for name := range files {
names = append(names, name)
}
// limits bite as the walk proceeds, so entry order decides what lands before a truncation
sort.Strings(names)
entries := make([]testEntry, 0, len(names))
for _, name := range names {
entries = append(entries, testEntry{name: name, body: files[name]})
}
return entries
}
func zipBytes(t testing.TB, files map[string]string) []byte {
return zipFromEntries(t, fileEntries(files))
}
func zipFromEntries(t testing.TB, entries []testEntry) []byte {
t.Helper()
var buf bytes.Buffer
w := zip.NewWriter(&buf)
for _, e := range entries {
hdr := &zip.FileHeader{Name: e.name}
body := e.body
switch {
case e.dir:
hdr.SetMode(fs.ModeDir | e.modeOr(0o755))
case e.link != "":
hdr.SetMode(fs.ModeSymlink | 0o777)
body = e.link
default:
hdr.SetMode(e.modeOr(0o644))
}
fw, err := w.CreateHeader(hdr)
require.NoError(t, err)
_, err = fw.Write([]byte(body))
require.NoError(t, err)
}
require.NoError(t, w.Close())
return buf.Bytes()
}
func tarGzBytes(t testing.TB, files map[string]string) []byte {
return tarGzFromEntries(t, fileEntries(files))
}
func tarGzFromEntries(t testing.TB, entries []testEntry) []byte {
t.Helper()
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
for _, e := range entries {
hdr := &tar.Header{Name: e.name, Mode: int64(e.modeOr(0o644)), Size: int64(len(e.body))}
switch {
case e.dir:
hdr.Typeflag, hdr.Mode, hdr.Size = tar.TypeDir, int64(e.modeOr(0o755)), 0
case e.link != "":
hdr.Typeflag, hdr.Linkname, hdr.Mode, hdr.Size = tar.TypeSymlink, e.link, 0o777, 0
}
require.NoError(t, tw.WriteHeader(hdr))
_, err := tw.Write([]byte(e.body))
require.NoError(t, err)
}
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
return buf.Bytes()
}
func (e testEntry) modeOr(fallback fs.FileMode) fs.FileMode {
if e.mode != 0 {
return e.mode
}
return fallback
}
func regularHeader(name string, size int64) tar.Header {
return tar.Header{Name: name, Size: size, Mode: 0o600, Typeflag: tar.TypeReg}
}
// storeFor makes a store in the test's own temp space with the given charge.
func storeFor(t testing.TB, charge *Charge) *EntryStore {
t.Helper()
s := NewEntryStore("test-archive", WorkDirAt(t.TempDir()), charge)
t.Cleanup(func() { _ = s.Close() })
return s
}
// storeDir is the directory a store spills into.
func storeDir(t testing.TB, s *EntryStore) string {
t.Helper()
dir, err := s.workDir.Path()
require.NoError(t, err)
return dir
}
func readEntry(t testing.TB, r ReaderAtSeeker) string {
t.Helper()
_, err := r.Seek(0, io.SeekStart)
require.NoError(t, err)
b, err := io.ReadAll(r)
require.NoError(t, err)
return string(b)
}
// storedEntry is one entry read back out of a store.
type storedEntry struct {
body string
linkTarget string
mode fs.FileMode
typeflag byte
}
func readStore(t testing.TB, s *EntryStore) map[string]storedEntry {
t.Helper()
out := map[string]storedEntry{}
for _, entry := range s.Entries() {
out[entry.Header.Name] = storedEntry{
body: readEntry(t, s.Open(entry)),
linkTarget: entry.Header.Linkname,
mode: entry.Header.FileInfo().Mode(),
typeflag: entry.Header.Typeflag,
}
}
return out
}
func storedNames(t testing.TB, s *EntryStore) []string {
t.Helper()
var names []string
for _, entry := range s.Entries() {
names = append(names, entry.Header.Name)
}
sort.Strings(names)
return names
}
// memCharge is a charge against a memory bound with disk unbounded.
func memCharge(maxMemory int64) *Charge {
return NewLimiter(Limits{MaxMemoryBytes: maxMemory, MaxDiskBytes: -1}).Charge()
}
// diskCharge is a charge against a disk bound with memory refusing everything.
func diskCharge(maxDisk int64) *Charge {
return NewLimiter(Limits{MaxDiskBytes: maxDisk}).Charge()
}
// filesIn lists the regular files directly in dir.
func filesIn(t testing.TB, dir string) []string {
t.Helper()
entries, err := os.ReadDir(dir)
require.NoError(t, err)
var names []string
for _, e := range entries {
if !e.IsDir() {
names = append(names, e.Name())
}
}
return names
}
// scanContext returns a context whose temp root is a fresh directory, and that directory.
func scanContext(t testing.TB) (context.Context, string) {
t.Helper()
root := t.TempDir()
return tmpdir.WithValue(context.Background(), tmpdir.FromPath(root)), root
}
// workDirsUnder returns the archive work directories directly under a scan temp root.
func workDirsUnder(t testing.TB, root string) []string {
t.Helper()
children, err := os.ReadDir(root)
require.NoError(t, err)
var out []string
for _, child := range children {
if child.IsDir() && strings.HasPrefix(child.Name(), workDirName) {
out = append(out, filepath.Join(root, child.Name()))
}
}
return out
}
// extractBytes extracts an archive into a store built over the given charge.
func extractBytes(t testing.TB, data []byte, name string, charge *Charge) (*EntryStore, bool) {
t.Helper()
store := storeFor(t, charge)
content := bytes.NewReader(data)
format := identifyFormat(context.Background(), name, content)
require.NotNil(t, format, "fixture must identify as an archive")
truncated, err := extractInto(context.Background(), format, content, store, nil)
require.NoError(t, err)
return store, truncated
}