feat: Support scanning license files in golang packages over the network (#1630)

Signed-off-by: Avi Deitcher <avi@deitcher.net>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Avi Deitcher
2023-04-14 15:13:29 -04:00
committed by GitHub
co-authored by Keith Zantow Alex Goodman
parent 44422853be
commit b69259534d
29 changed files with 2475 additions and 102 deletions
+6 -3
View File
@@ -923,16 +923,19 @@ func TestDirectoryResolver_DoNotAddVirtualPathsToTree(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-prune-indexing", "")
require.NoError(t, err)
allRealPaths := resolver.tree.AllRealPaths()
var allRealPaths []file.Path
for l := range resolver.AllLocations() {
allRealPaths = append(allRealPaths, file.Path(l.RealPath))
}
pathSet := file.NewPathSet(allRealPaths...)
assert.False(t,
pathSet.Contains("/before-path/file.txt"),
pathSet.Contains("before-path/file.txt"),
"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
)
assert.False(t,
pathSet.Contains("/a-path/file.txt"),
pathSet.Contains("a-path/file.txt"),
"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
)
+45
View File
@@ -0,0 +1,45 @@
package source
import (
"io"
)
type EmptyResolver struct{}
func (e EmptyResolver) FileContentsByLocation(_ Location) (io.ReadCloser, error) {
return nil, nil
}
func (e EmptyResolver) HasPath(_ string) bool {
return false
}
func (e EmptyResolver) FilesByPath(_ ...string) ([]Location, error) {
return nil, nil
}
func (e EmptyResolver) FilesByGlob(_ ...string) ([]Location, error) {
return nil, nil
}
func (e EmptyResolver) FilesByMIMEType(_ ...string) ([]Location, error) {
return nil, nil
}
func (e EmptyResolver) RelativeFileByPath(_ Location, _ string) *Location {
return nil
}
func (e EmptyResolver) AllLocations() <-chan Location {
return nil
}
func (e EmptyResolver) FileMetadataByLocation(_ Location) (FileMetadata, error) {
return FileMetadata{}, nil
}
func (e EmptyResolver) Write(_ Location, _ io.Reader) error {
return nil
}
var _ WritableFileResolver = (*EmptyResolver)(nil)
+6
View File
@@ -57,3 +57,9 @@ type FileLocationResolver interface {
// - returns locations for any file or directory
AllLocations() <-chan Location
}
type WritableFileResolver interface {
FileResolver
Write(location Location, reader io.Reader) error
}
+4
View File
@@ -204,3 +204,7 @@ func (r MockResolver) FilesByBasenameGlob(_ ...string) ([]Location, error) {
// TODO implement me
panic("implement me")
}
func (r MockResolver) Write(_ Location, _ io.Reader) error {
return nil
}
+553
View File
@@ -0,0 +1,553 @@
package source
import (
"fmt"
"io"
"io/fs"
"os"
"path"
"sort"
"strings"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"golang.org/x/exp/slices"
"github.com/anchore/syft/internal/log"
)
type UnindexedDirectoryResolver struct {
ls afero.Lstater
lr afero.LinkReader
base string
dir string
fs afero.Fs
}
func NewUnindexedDirectoryResolver(dir string) WritableFileResolver {
return NewUnindexedDirectoryResolverFS(afero.NewOsFs(), dir, "")
}
func NewUnindexedDirectoryResolverRooted(dir string, base string) WritableFileResolver {
return NewUnindexedDirectoryResolverFS(afero.NewOsFs(), dir, base)
}
func NewUnindexedDirectoryResolverFS(fs afero.Fs, dir string, base string) WritableFileResolver {
ls, ok := fs.(afero.Lstater)
if !ok {
panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs))
}
lr, ok := fs.(afero.LinkReader)
if !ok {
panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs))
}
expanded, err := homedir.Expand(dir)
if err == nil {
dir = expanded
}
if base != "" {
expanded, err = homedir.Expand(base)
if err == nil {
base = expanded
}
}
wd, err := os.Getwd()
if err == nil {
if !path.IsAbs(dir) {
dir = path.Clean(path.Join(wd, dir))
}
if base != "" && !path.IsAbs(base) {
base = path.Clean(path.Join(wd, base))
}
}
return UnindexedDirectoryResolver{
base: base,
dir: dir,
fs: fs,
ls: ls,
lr: lr,
}
}
func (u UnindexedDirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
p := u.absPath(u.scrubInputPath(location.RealPath))
f, err := u.fs.Open(p)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("unable to get contents of directory: %s", location.RealPath)
}
return f, nil
}
// - full symlink resolution should be performed on all requests
// - returns locations for any file or directory
func (u UnindexedDirectoryResolver) HasPath(p string) bool {
locs, err := u.filesByPath(true, true, p)
return err == nil && len(locs) > 0
}
func (u UnindexedDirectoryResolver) canLstat(p string) bool {
_, _, err := u.ls.LstatIfPossible(u.absPath(p))
return err == nil
}
func (u UnindexedDirectoryResolver) isRegularFile(p string) bool {
fi, _, err := u.ls.LstatIfPossible(u.absPath(p))
return err == nil && !fi.IsDir()
}
func (u UnindexedDirectoryResolver) scrubInputPath(p string) string {
if path.IsAbs(p) {
p = p[1:]
}
return path.Clean(p)
}
func (u UnindexedDirectoryResolver) scrubResolutionPath(p string) string {
if u.base != "" {
if path.IsAbs(p) {
p = p[1:]
}
for strings.HasPrefix(p, "../") {
p = p[3:]
}
}
return path.Clean(p)
}
func (u UnindexedDirectoryResolver) absPath(p string) string {
if u.base != "" {
if path.IsAbs(p) {
p = p[1:]
}
for strings.HasPrefix(p, "../") {
p = p[3:]
}
p = path.Join(u.base, p)
return path.Clean(p)
}
if path.IsAbs(p) {
return p
}
return path.Clean(path.Join(u.dir, p))
}
// - full symlink resolution should be performed on all requests
// - only returns locations to files (NOT directories)
func (u UnindexedDirectoryResolver) FilesByPath(paths ...string) (out []Location, _ error) {
return u.filesByPath(true, false, paths...)
}
func (u UnindexedDirectoryResolver) filesByPath(resolveLinks bool, includeDirs bool, paths ...string) (out []Location, _ error) {
// sort here for stable output
sort.Strings(paths)
nextPath:
for _, p := range paths {
p = u.scrubInputPath(p)
if u.canLstat(p) && (includeDirs || u.isRegularFile(p)) {
l := u.newLocation(p, resolveLinks)
if l == nil {
continue
}
// only include the first entry we find
for i := range out {
existing := &out[i]
if existing.RealPath == l.RealPath {
if l.VirtualPath == "" {
existing.VirtualPath = ""
}
continue nextPath
}
}
out = append(out, *l)
}
}
return
}
// - full symlink resolution should be performed on all requests
// - if multiple paths to the same file are found, the best single match should be returned
// - only returns locations to files (NOT directories)
func (u UnindexedDirectoryResolver) FilesByGlob(patterns ...string) (out []Location, _ error) {
return u.filesByGlob(true, false, patterns...)
}
func (u UnindexedDirectoryResolver) filesByGlob(resolveLinks bool, includeDirs bool, patterns ...string) (out []Location, _ error) {
f := unindexedDirectoryResolverFS{
u: u,
}
var paths []string
for _, p := range patterns {
opts := []doublestar.GlobOption{doublestar.WithNoFollow()}
if !includeDirs {
opts = append(opts, doublestar.WithFilesOnly())
}
found, err := doublestar.Glob(f, p, opts...)
if err != nil {
return nil, err
}
paths = append(paths, found...)
}
return u.filesByPath(resolveLinks, includeDirs, paths...)
}
func (u UnindexedDirectoryResolver) FilesByMIMEType(_ ...string) ([]Location, error) {
panic("FilesByMIMEType unsupported")
}
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
// This is helpful when attempting to find a file that is in the same layer or lower as another file.
func (u UnindexedDirectoryResolver) RelativeFileByPath(l Location, p string) *Location {
p = path.Clean(path.Join(l.RealPath, p))
locs, err := u.filesByPath(true, false, p)
if err != nil || len(locs) == 0 {
return nil
}
l = locs[0]
p = l.RealPath
if u.isRegularFile(p) {
return u.newLocation(p, true)
}
return nil
}
// - NO symlink resolution should be performed on results
// - returns locations for any file or directory
func (u UnindexedDirectoryResolver) AllLocations() <-chan Location {
out := make(chan Location)
go func() {
defer close(out)
err := afero.Walk(u.fs, u.absPath("."), func(p string, info fs.FileInfo, err error) error {
p = strings.TrimPrefix(p, u.dir)
if p == "" {
return nil
}
p = strings.TrimPrefix(p, "/")
out <- Location{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: p,
},
},
}
return nil
})
if err != nil {
log.Debug(err)
}
}()
return out
}
func (u UnindexedDirectoryResolver) FileMetadataByLocation(_ Location) (FileMetadata, error) {
panic("FileMetadataByLocation unsupported")
}
func (u UnindexedDirectoryResolver) Write(location Location, reader io.Reader) error {
filePath := location.RealPath
if path.IsAbs(filePath) {
filePath = filePath[1:]
}
absPath := u.absPath(filePath)
return afero.WriteReader(u.fs, absPath, reader)
}
var _ FileResolver = (*UnindexedDirectoryResolver)(nil)
var _ WritableFileResolver = (*UnindexedDirectoryResolver)(nil)
func (u UnindexedDirectoryResolver) newLocation(filePath string, resolveLinks bool) *Location {
filePath = path.Clean(filePath)
virtualPath := ""
realPath := filePath
if resolveLinks {
paths := u.resolveLinks(filePath)
if len(paths) > 1 {
realPath = paths[len(paths)-1]
if realPath != path.Clean(filePath) {
virtualPath = paths[0]
}
}
if len(paths) == 0 {
// this file does not exist, don't return a location
return nil
}
}
return &Location{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: realPath,
},
VirtualPath: virtualPath,
},
}
}
//nolint:gocognit
func (u UnindexedDirectoryResolver) resolveLinks(filePath string) []string {
var visited []string
out := []string{}
resolvedPath := ""
parts := strings.Split(filePath, "/")
for i := 0; i < len(parts); i++ {
part := parts[i]
if resolvedPath == "" {
resolvedPath = part
} else {
resolvedPath = path.Clean(path.Join(resolvedPath, part))
}
resolvedPath = u.scrubResolutionPath(resolvedPath)
if resolvedPath == ".." {
resolvedPath = ""
continue
}
absPath := u.absPath(resolvedPath)
if slices.Contains(visited, absPath) {
return nil // circular links can't resolve
}
visited = append(visited, absPath)
fi, wasLstat, err := u.ls.LstatIfPossible(absPath)
if fi == nil || err != nil {
// this file does not exist
return nil
}
for wasLstat && u.isSymlink(fi) {
next, err := u.lr.ReadlinkIfPossible(absPath)
if err == nil {
if !path.IsAbs(next) {
next = path.Clean(path.Join(path.Dir(resolvedPath), next))
}
next = u.scrubResolutionPath(next)
absPath = u.absPath(next)
if slices.Contains(visited, absPath) {
return nil // circular links can't resolve
}
visited = append(visited, absPath)
fi, wasLstat, err = u.ls.LstatIfPossible(absPath)
if fi == nil || err != nil {
// this file does not exist
return nil
}
if i < len(parts) {
out = append(out, path.Join(resolvedPath, path.Join(parts[i+1:]...)))
}
if u.base != "" && path.IsAbs(next) {
next = next[1:]
}
resolvedPath = next
}
}
}
out = append(out, resolvedPath)
return out
}
func (u UnindexedDirectoryResolver) isSymlink(fi os.FileInfo) bool {
return fi.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink
}
// ------------------------- fs.FS ------------------------------
// unindexedDirectoryResolverFS wraps the UnindexedDirectoryResolver as a fs.FS, fs.ReadDirFS, and fs.StatFS
type unindexedDirectoryResolverFS struct {
u UnindexedDirectoryResolver
}
// resolve takes a virtual path and returns the resolved absolute or relative path and file info
func (f unindexedDirectoryResolverFS) resolve(filePath string) (resolved string, fi fs.FileInfo, err error) {
parts := strings.Split(filePath, "/")
var visited []string
for i, part := range parts {
if i > 0 {
resolved = path.Clean(path.Join(resolved, part))
} else {
resolved = part
}
abs := f.u.absPath(resolved)
fi, _, err = f.u.ls.LstatIfPossible(abs)
if err != nil {
return
}
for f.u.isSymlink(fi) {
if slices.Contains(visited, resolved) {
return resolved, fi, fmt.Errorf("link cycle detected at: %s", f.u.absPath(resolved))
}
visited = append(visited, resolved)
link, err := f.u.lr.ReadlinkIfPossible(abs)
if err != nil {
return resolved, fi, err
}
if !path.IsAbs(link) {
link = path.Clean(path.Join(path.Dir(abs), link))
link = strings.TrimPrefix(link, abs)
} else if f.u.base != "" {
link = path.Clean(path.Join(f.u.base, link[1:]))
}
resolved = link
abs = f.u.absPath(resolved)
fi, _, err = f.u.ls.LstatIfPossible(abs)
if err != nil {
return resolved, fi, err
}
}
}
return resolved, fi, err
}
func (f unindexedDirectoryResolverFS) ReadDir(name string) (out []fs.DirEntry, _ error) {
p, _, err := f.resolve(name)
if err != nil {
return nil, err
}
entries, err := afero.ReadDir(f.u.fs, f.u.absPath(p))
if err != nil {
return nil, err
}
for _, e := range entries {
isDir := e.IsDir()
_, fi, _ := f.resolve(path.Join(name, e.Name()))
if fi != nil && fi.IsDir() {
isDir = true
}
out = append(out, unindexedDirectoryResolverDirEntry{
unindexedDirectoryResolverFileInfo: newFsFileInfo(f.u, e.Name(), isDir, e),
})
}
return out, nil
}
func (f unindexedDirectoryResolverFS) Stat(name string) (fs.FileInfo, error) {
fi, err := f.u.fs.Stat(f.u.absPath(name))
if err != nil {
return nil, err
}
return newFsFileInfo(f.u, name, fi.IsDir(), fi), nil
}
func (f unindexedDirectoryResolverFS) Open(name string) (fs.File, error) {
_, err := f.u.fs.Open(f.u.absPath(name))
if err != nil {
return nil, err
}
return unindexedDirectoryResolverFile{
u: f.u,
path: name,
}, nil
}
var _ fs.FS = (*unindexedDirectoryResolverFS)(nil)
var _ fs.StatFS = (*unindexedDirectoryResolverFS)(nil)
var _ fs.ReadDirFS = (*unindexedDirectoryResolverFS)(nil)
type unindexedDirectoryResolverDirEntry struct {
unindexedDirectoryResolverFileInfo
}
func (f unindexedDirectoryResolverDirEntry) Name() string {
return f.name
}
func (f unindexedDirectoryResolverDirEntry) IsDir() bool {
return f.isDir
}
func (f unindexedDirectoryResolverDirEntry) Type() fs.FileMode {
return f.mode
}
func (f unindexedDirectoryResolverDirEntry) Info() (fs.FileInfo, error) {
return f, nil
}
var _ fs.DirEntry = (*unindexedDirectoryResolverDirEntry)(nil)
type unindexedDirectoryResolverFile struct {
u UnindexedDirectoryResolver
path string
}
func (f unindexedDirectoryResolverFile) Stat() (fs.FileInfo, error) {
fi, err := f.u.fs.Stat(f.u.absPath(f.path))
if err != nil {
return nil, err
}
return newFsFileInfo(f.u, fi.Name(), fi.IsDir(), fi), nil
}
func (f unindexedDirectoryResolverFile) Read(_ []byte) (int, error) {
panic("Read not implemented")
}
func (f unindexedDirectoryResolverFile) Close() error {
panic("Close not implemented")
}
var _ fs.File = (*unindexedDirectoryResolverFile)(nil)
type unindexedDirectoryResolverFileInfo struct {
u UnindexedDirectoryResolver
name string
size int64
mode fs.FileMode
modTime time.Time
isDir bool
sys any
}
func newFsFileInfo(u UnindexedDirectoryResolver, name string, isDir bool, fi os.FileInfo) unindexedDirectoryResolverFileInfo {
return unindexedDirectoryResolverFileInfo{
u: u,
name: name,
size: fi.Size(),
mode: fi.Mode() & ^fs.ModeSymlink, // pretend nothing is a symlink
modTime: fi.ModTime(),
isDir: isDir,
// sys: fi.Sys(), // what values does this hold?
}
}
func (f unindexedDirectoryResolverFileInfo) Name() string {
return f.name
}
func (f unindexedDirectoryResolverFileInfo) Size() int64 {
return f.size
}
func (f unindexedDirectoryResolverFileInfo) Mode() fs.FileMode {
return f.mode
}
func (f unindexedDirectoryResolverFileInfo) ModTime() time.Time {
return f.modTime
}
func (f unindexedDirectoryResolverFileInfo) IsDir() bool {
return f.isDir
}
func (f unindexedDirectoryResolverFileInfo) Sys() any {
return f.sys
}
var _ fs.FileInfo = (*unindexedDirectoryResolverFileInfo)(nil)
@@ -0,0 +1,744 @@
//go:build !windows
// +build !windows
package source
import (
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/file"
)
func Test_UnindexedDirectoryResolver_Basic(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
r := NewUnindexedDirectoryResolver(path.Join(wd, "test-fixtures"))
locations, err := r.FilesByGlob("image-symlinks/*")
require.NoError(t, err)
require.Len(t, locations, 5)
}
func Test_UnindexedDirectoryResolver_FilesByPath_relativeRoot(t *testing.T) {
cases := []struct {
name string
relativeRoot string
input string
expected []string
}{
{
name: "should find a file from an absolute input",
relativeRoot: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path",
relativeRoot: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path (root above cwd)",
relativeRoot: "../",
input: "sbom/sbom.go",
expected: []string{
"sbom/sbom.go",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver(c.relativeRoot)
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, len(c.expected))
s := strset.New()
for _, actual := range refs {
s.Add(actual.RealPath)
}
assert.ElementsMatch(t, c.expected, s.List())
})
}
}
func Test_UnindexedDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) {
cases := []struct {
name string
relativeRoot string
input string
expected []string
}{
{
name: "should find a file from an absolute input",
relativeRoot: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path",
relativeRoot: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path (root above cwd)",
relativeRoot: "../",
input: "sbom/sbom.go",
expected: []string{
"sbom/sbom.go",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// note: this test is all about asserting correct functionality when the given analysis path
// is an absolute path
absRoot, err := filepath.Abs(c.relativeRoot)
require.NoError(t, err)
resolver := NewUnindexedDirectoryResolver(absRoot)
assert.NoError(t, err)
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, len(c.expected))
s := strset.New()
for _, actual := range refs {
s.Add(actual.RealPath)
}
assert.ElementsMatch(t, c.expected, s.List())
})
}
}
func Test_UnindexedDirectoryResolver_FilesByPath(t *testing.T) {
cases := []struct {
name string
root string
input string
expected string
refCount int
forcePositiveHasPath bool
}{
{
name: "finds a file (relative)",
root: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
name: "finds a file with relative indirection",
root: "./test-fixtures/../test-fixtures",
input: "image-symlinks/file-1.txt",
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
name: "managed non-existing files (relative)",
root: "./test-fixtures/",
input: "test-fixtures/image-symlinks/bogus.txt",
refCount: 0,
},
{
name: "finds a file (absolute)",
root: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
name: "directories ignored",
root: "./test-fixtures/",
input: "/image-symlinks",
refCount: 0,
forcePositiveHasPath: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver(c.root)
hasPath := resolver.HasPath(c.input)
if !c.forcePositiveHasPath {
if c.refCount != 0 && !hasPath {
t.Errorf("expected HasPath() to indicate existence, but did not")
} else if c.refCount == 0 && hasPath {
t.Errorf("expected HasPath() to NOT indicate existence, but does")
}
} else if !hasPath {
t.Errorf("expected HasPath() to indicate existence, but did not (force path)")
}
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, c.refCount)
for _, actual := range refs {
assert.Equal(t, c.expected, actual.RealPath)
}
})
}
}
func Test_UnindexedDirectoryResolver_MultipleFilesByPath(t *testing.T) {
cases := []struct {
name string
input []string
refCount int
}{
{
name: "finds multiple files",
input: []string{"image-symlinks/file-1.txt", "image-symlinks/file-2.txt"},
refCount: 2,
},
{
name: "skips non-existing files",
input: []string{"image-symlinks/bogus.txt", "image-symlinks/file-1.txt"},
refCount: 1,
},
{
name: "does not return anything for non-existing directories",
input: []string{"non-existing/bogus.txt", "non-existing/file-1.txt"},
refCount: 0,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures")
refs, err := resolver.FilesByPath(c.input...)
assert.NoError(t, err)
if len(refs) != c.refCount {
t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount)
}
})
}
}
func Test_UnindexedDirectoryResolver_FilesByGlobMultiple(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures")
refs, err := resolver.FilesByGlob("**/image-symlinks/file*")
assert.NoError(t, err)
assert.Len(t, refs, 2)
}
func Test_UnindexedDirectoryResolver_FilesByGlobRecursive(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/image-symlinks")
refs, err := resolver.FilesByGlob("**/*.txt")
assert.NoError(t, err)
assert.Len(t, refs, 6)
}
func Test_UnindexedDirectoryResolver_FilesByGlobSingle(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures")
refs, err := resolver.FilesByGlob("**/image-symlinks/*1.txt")
assert.NoError(t, err)
assert.Len(t, refs, 1)
assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath)
}
func Test_UnindexedDirectoryResolver_FilesByPath_ResolvesSymlinks(t *testing.T) {
tests := []struct {
name string
fixture string
}{
{
name: "one degree",
fixture: "link_to_new_readme",
},
{
name: "two degrees",
fixture: "link_to_link_to_new_readme",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-simple")
refs, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
require.Len(t, refs, 1)
reader, err := resolver.FileContentsByLocation(refs[0])
require.NoError(t, err)
actual, err := io.ReadAll(reader)
require.NoError(t, err)
expected, err := os.ReadFile("test-fixtures/symlinks-simple/readme")
require.NoError(t, err)
require.Equal(t, string(expected), string(actual))
})
}
}
func Test_UnindexedDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
// let's make certain that "dev/place" is not ignored, since it is not "/dev/place"
resolver := NewUnindexedDirectoryResolver("test-fixtures/system_paths/target")
// all paths should be found (non filtering matches a path)
locations, err := resolver.FilesByGlob("**/place")
assert.NoError(t, err)
// 4: within target/
// 1: target/link --> relative path to "place" // NOTE: this is filtered out since it not unique relative to outside_root/link_target/place
// 1: outside_root/link_target/place
assert.Len(t, locations, 5)
// ensure that symlink indexing outside of root worked
testLocation := "../outside_root/link_target/place"
ok := false
for _, location := range locations {
if strings.HasSuffix(location.RealPath, testLocation) {
ok = true
}
}
if !ok {
t.Fatalf("could not find test location=%q", testLocation)
}
}
func Test_UnindexedDirectoryResover_IndexingNestedSymLinks(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-simple")
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// check that we can access the same file via 1 symlink
locations, err = resolver.FilesByPath("./link_to_new_readme")
require.NoError(t, err)
require.Len(t, locations, 1)
assert.Equal(t, "readme", locations[0].RealPath)
assert.Equal(t, "link_to_new_readme", locations[0].VirtualPath)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
require.NoError(t, err)
require.Len(t, locations, 1)
assert.Equal(t, "readme", locations[0].RealPath)
assert.Equal(t, "link_to_link_to_new_readme", locations[0].VirtualPath)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByGlob("**/link_*")
require.NoError(t, err)
require.Len(t, locations, 1) // you would think this is 2, however, they point to the same file, and glob only returns unique files
// returned locations can be in any order
expectedVirtualPaths := []string{
"link_to_link_to_new_readme",
//"link_to_new_readme", // we filter out this one because the first symlink resolves to the same file
}
expectedRealPaths := []string{
"readme",
}
actualRealPaths := strset.New()
actualVirtualPaths := strset.New()
for _, a := range locations {
actualVirtualPaths.Add(a.VirtualPath)
actualRealPaths.Add(a.RealPath)
}
assert.ElementsMatch(t, expectedVirtualPaths, actualVirtualPaths.List())
assert.ElementsMatch(t, expectedRealPaths, actualRealPaths.List())
}
func Test_UnindexedDirectoryResover_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-multiple-roots/root")
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// check that we can access the same file via 2 symlinks (link_to_link_to_readme -> link_to_readme -> readme)
locations, err = resolver.FilesByPath("./link_to_link_to_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// something looks wrong here
t.Failed()
}
func Test_UnindexedDirectoryResover_RootViaSymlink(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinked-root/nested/link-root")
locations, err := resolver.FilesByPath("./file1.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
locations, err = resolver.FilesByPath("./nested/file2.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
locations, err = resolver.FilesByPath("./nested/linked-file1.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
}
func Test_UnindexedDirectoryResolver_FileContentsByLocation(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
r := NewUnindexedDirectoryResolver(path.Join(cwd, "test-fixtures/image-simple"))
require.NoError(t, err)
tests := []struct {
name string
location Location
expects string
err bool
}{
{
name: "use file reference for content requests",
location: NewLocation("file-1.txt"),
expects: "this file has contents",
},
{
name: "error on empty file reference",
location: NewLocationFromDirectory("doesn't matter", file.Reference{}),
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := r.FileContentsByLocation(test.location)
if test.err {
require.Error(t, err)
return
}
require.NoError(t, err)
if test.expects != "" {
b, err := io.ReadAll(actual)
require.NoError(t, err)
assert.Equal(t, test.expects, string(b))
}
})
}
}
func Test_UnindexedDirectoryResover_SymlinkLoopWithGlobsShouldResolve(t *testing.T) {
test := func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-loop")
locations, err := resolver.FilesByGlob("**/file.target")
require.NoError(t, err)
require.Len(t, locations, 1)
assert.Equal(t, "devices/loop0/file.target", locations[0].RealPath)
}
testWithTimeout(t, 5*time.Second, test)
}
func Test_UnindexedDirectoryResolver_FilesByPath_baseRoot(t *testing.T) {
cases := []struct {
name string
root string
input string
expected []string
}{
{
name: "should find the base file",
root: "./test-fixtures/symlinks-base/",
input: "./base",
expected: []string{
"base",
},
},
{
name: "should follow a link with a pivoted root",
root: "./test-fixtures/symlinks-base/",
input: "./foo",
expected: []string{
"base",
},
},
{
name: "should follow a relative link with extra parents",
root: "./test-fixtures/symlinks-base/",
input: "./bar",
expected: []string{
"base",
},
},
{
name: "should follow an absolute link with extra parents",
root: "./test-fixtures/symlinks-base/",
input: "./baz",
expected: []string{
"base",
},
},
{
name: "should follow an absolute link with extra parents",
root: "./test-fixtures/symlinks-base/",
input: "./sub/link",
expected: []string{
"sub/item",
},
},
{
name: "should follow chained pivoted link",
root: "./test-fixtures/symlinks-base/",
input: "./chain",
expected: []string{
"base",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolverRooted(c.root, c.root)
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, len(c.expected))
s := strset.New()
for _, actual := range refs {
s.Add(actual.RealPath)
}
assert.ElementsMatch(t, c.expected, s.List())
})
}
}
func Test_UnindexedDirectoryResolver_resolvesLinks(t *testing.T) {
tests := []struct {
name string
runner func(FileResolver) []Location
expected []Location
}{
{
name: "by glob to links",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
// for that reason we need to place **/ in front (which is not the same for other resolvers)
actualLocations, err := resolver.FilesByGlob("**/*ink-*")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
NewVirtualLocation("file-1.txt", "link-1"),
NewVirtualLocation("file-2.txt", "link-2"),
// we already have this real file path via another link, so only one is returned
// NewVirtualLocation("file-2.txt", "link-indirect"),
NewVirtualLocation("file-3.txt", "link-within"),
},
},
{
name: "by basename",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
actualLocations, err := resolver.FilesByGlob("**/file-2.txt")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// this has two copies in the base image, which overwrites the same location
NewLocation("file-2.txt"),
},
},
{
name: "by basename glob",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
actualLocations, err := resolver.FilesByGlob("**/file-?.txt")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
NewLocation("file-1.txt"),
NewLocation("file-2.txt"),
NewLocation("file-3.txt"),
NewLocation("parent/file-4.txt"),
},
},
{
name: "by basename glob to links",
runner: func(resolver FileResolver) []Location {
actualLocations, err := resolver.FilesByGlob("**/link-*")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
NewVirtualLocationFromDirectory("file-1.txt", "link-1", file.Reference{RealPath: "file-1.txt"}),
NewVirtualLocationFromDirectory("file-2.txt", "link-2", file.Reference{RealPath: "file-2.txt"}),
// we already have this real file path via another link, so only one is returned
//NewVirtualLocationFromDirectory("file-2.txt", "link-indirect", file.Reference{RealPath: "file-2.txt"}),
NewVirtualLocationFromDirectory("file-3.txt", "link-within", file.Reference{RealPath: "file-3.txt"}),
},
},
{
name: "by extension",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
actualLocations, err := resolver.FilesByGlob("**/*.txt")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
NewLocation("file-1.txt"),
NewLocation("file-2.txt"),
NewLocation("file-3.txt"),
NewLocation("parent/file-4.txt"),
},
},
{
name: "by path to degree 1 link",
runner: func(resolver FileResolver) []Location {
// links resolve to the final file
actualLocations, err := resolver.FilesByPath("/link-2")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
NewVirtualLocation("file-2.txt", "link-2"),
},
},
{
name: "by path to degree 2 link",
runner: func(resolver FileResolver) []Location {
// multiple links resolves to the final file
actualLocations, err := resolver.FilesByPath("/link-indirect")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
NewVirtualLocation("file-2.txt", "link-indirect"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-from-image-symlinks-fixture")
actual := test.runner(resolver)
compareLocations(t, test.expected, actual)
})
}
}
func Test_UnindexedDirectoryResolver_DoNotAddVirtualPathsToTree(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-prune-indexing")
allLocations := resolver.AllLocations()
var allRealPaths []file.Path
for l := range allLocations {
allRealPaths = append(allRealPaths, file.Path(l.RealPath))
}
pathSet := file.NewPathSet(allRealPaths...)
assert.False(t,
pathSet.Contains("before-path/file.txt"),
"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
)
assert.False(t,
pathSet.Contains("a-path/file.txt"),
"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
)
}
func Test_UnindexedDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/system_paths")
dirLoc := NewLocation("arg/foo")
reader, err := resolver.FileContentsByLocation(dirLoc)
require.Error(t, err)
require.Nil(t, reader)
}
func Test_UnindexedDirectoryResolver_AllLocations(t *testing.T) {
resolver := NewUnindexedDirectoryResolver("./test-fixtures/symlinks-from-image-symlinks-fixture")
paths := strset.New()
for loc := range resolver.AllLocations() {
if strings.HasPrefix(loc.RealPath, "/") {
// ignore outside of the fixture root for now
continue
}
paths.Add(loc.RealPath)
}
expected := []string{
"file-1.txt",
"file-2.txt",
"file-3.txt",
"link-1",
"link-2",
"link-dead",
"link-indirect",
"link-within",
"parent",
"parent-link",
"parent/file-4.txt",
}
pathsList := paths.List()
sort.Strings(pathsList)
assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
}
func Test_WritableUnindexedDirectoryResolver(t *testing.T) {
tmpdir := t.TempDir()
p := "some/path/file"
c := "some contents"
dr := NewUnindexedDirectoryResolver(tmpdir)
locations, err := dr.FilesByPath(p)
require.NoError(t, err)
require.Len(t, locations, 0)
err = dr.Write(NewLocation(p), strings.NewReader(c))
require.NoError(t, err)
locations, err = dr.FilesByPath(p)
require.NoError(t, err)
require.Len(t, locations, 1)
reader, err := dr.FileContentsByLocation(locations[0])
require.NoError(t, err)
bytes, err := io.ReadAll(reader)
require.Equal(t, c, string(bytes))
}