mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
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:
co-authored by
Keith Zantow
Alex Goodman
parent
44422853be
commit
b69259534d
@@ -0,0 +1,52 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
)
|
||||
|
||||
type CatalogerTask struct {
|
||||
prog *progress.Manual
|
||||
// Title
|
||||
Title string
|
||||
// TitleOnCompletion a string to use as title when completed
|
||||
TitleOnCompletion string
|
||||
// SubStatus indicates this progress should be rendered as a sub-item
|
||||
SubStatus bool
|
||||
// RemoveOnCompletion indicates this progress line will be removed when completed
|
||||
RemoveOnCompletion bool
|
||||
// value is the value to display -- not public as SetValue needs to be called to initialize this progress
|
||||
value string
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) init() {
|
||||
e.prog = progress.NewManual(-1)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: CatalogerTaskStarted,
|
||||
Source: e,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) SetCompleted() {
|
||||
if e.prog != nil {
|
||||
e.prog.SetCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) SetValue(value string) {
|
||||
if e.prog == nil {
|
||||
e.init()
|
||||
}
|
||||
e.value = value
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) GetValue() string {
|
||||
return e.value
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) GetMonitor() *progress.Manual {
|
||||
return e.prog
|
||||
}
|
||||
@@ -34,4 +34,7 @@ const (
|
||||
|
||||
// AttestationStarted is a partybus event that occurs when starting an SBOM attestation process
|
||||
AttestationStarted partybus.EventType = "syft-attestation-started-event"
|
||||
|
||||
// CatalogerTaskStarted is a partybus event that occurs when starting a task within a cataloger
|
||||
CatalogerTaskStarted partybus.EventType = "syft-cataloger-task-started"
|
||||
)
|
||||
|
||||
@@ -111,6 +111,19 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
||||
return path, prog, nil
|
||||
}
|
||||
|
||||
func ParseCatalogerTaskStarted(e partybus.Event) (*event.CatalogerTask, error) {
|
||||
if err := checkEventType(e.Type, event.CatalogerTaskStarted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source, ok := e.Source.(*event.CatalogerTask)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Source", e.Source)
|
||||
}
|
||||
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func ParseExit(e partybus.Event) (func() error, error) {
|
||||
if err := checkEventType(e.Type, event.Exit); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
||||
// billyFSAdapter is a fs.FS, fs.ReadDirFS, and fs.StatFS wrapping what billyfs returns
|
||||
type billyFSAdapter struct {
|
||||
fs billy.Filesystem
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) Stat(name string) (fs.FileInfo, error) {
|
||||
return b.fs.Stat(name)
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) ReadDir(name string) (out []fs.DirEntry, _ error) {
|
||||
entries, err := b.fs.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
out = append(out, billyDirEntry{fi: e})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) Open(name string) (fs.File, error) {
|
||||
f, err := b.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := b.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return billyFile{f: f, fi: fi}, nil
|
||||
}
|
||||
|
||||
var _ fs.FS = (*billyFSAdapter)(nil)
|
||||
var _ fs.ReadDirFS = (*billyFSAdapter)(nil)
|
||||
var _ fs.StatFS = (*billyFSAdapter)(nil)
|
||||
|
||||
// billyFile is a fs.File wrapping what billyfs returns
|
||||
type billyFile struct {
|
||||
f billy.File
|
||||
fi fs.FileInfo
|
||||
}
|
||||
|
||||
func (b billyFile) Stat() (fs.FileInfo, error) {
|
||||
return b.fi, nil
|
||||
}
|
||||
|
||||
func (b billyFile) Read(i []byte) (int, error) {
|
||||
return b.f.Read(i)
|
||||
}
|
||||
|
||||
func (b billyFile) Close() error {
|
||||
return b.f.Close()
|
||||
}
|
||||
|
||||
var _ fs.File = (*billyFile)(nil)
|
||||
|
||||
// billyDirEntry is a fs.DirEntry wrapping what billyfs returns
|
||||
type billyDirEntry struct {
|
||||
fi os.FileInfo
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Name() string {
|
||||
return b.fi.Name()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) IsDir() bool {
|
||||
return b.fi.IsDir()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Type() fs.FileMode {
|
||||
return b.fi.Mode()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Info() (fs.FileInfo, error) {
|
||||
return b.fi, nil
|
||||
}
|
||||
|
||||
var _ fs.DirEntry = (*billyDirEntry)(nil)
|
||||
@@ -0,0 +1,33 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_billyFSAdapter(t *testing.T) {
|
||||
r, err := git.PlainInit("test-fixtures/repo", false)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll("test-fixtures/repo/.git")
|
||||
})
|
||||
|
||||
wt, err := r.Worktree()
|
||||
require.NoError(t, err)
|
||||
f := billyFSAdapter{
|
||||
fs: wt.Filesystem,
|
||||
}
|
||||
|
||||
found := ""
|
||||
err = fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
found = path
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "LICENSE", found)
|
||||
}
|
||||
@@ -5,28 +5,47 @@ package golang
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type GoCatalogerOpts struct {
|
||||
SearchLocalModCacheLicenses bool
|
||||
LocalModCacheDir string
|
||||
}
|
||||
|
||||
// NewGoModFileCataloger returns a new Go module cataloger object.
|
||||
func NewGoModFileCataloger(opts GoCatalogerOpts) *generic.Cataloger {
|
||||
func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||
c := goModCataloger{
|
||||
licenses: newGoLicenses(opts),
|
||||
}
|
||||
return generic.NewCataloger("go-mod-file-cataloger").
|
||||
WithParserByGlobs(c.parseGoModFile, "**/go.mod")
|
||||
return &progressingCataloger{
|
||||
progress: c.licenses.progress,
|
||||
cataloger: generic.NewCataloger("go-mod-file-cataloger").
|
||||
WithParserByGlobs(c.parseGoModFile, "**/go.mod"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewGoModuleBinaryCataloger returns a new Golang cataloger object.
|
||||
func NewGoModuleBinaryCataloger(opts GoCatalogerOpts) *generic.Cataloger {
|
||||
func NewGoModuleBinaryCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||
c := goBinaryCataloger{
|
||||
licenses: newGoLicenses(opts),
|
||||
}
|
||||
return generic.NewCataloger("go-module-binary-cataloger").
|
||||
WithParserByMimeTypes(c.parseGoBinary, internal.ExecutableMIMETypeSet.List()...)
|
||||
return &progressingCataloger{
|
||||
progress: c.licenses.progress,
|
||||
cataloger: generic.NewCataloger("go-module-binary-cataloger").
|
||||
WithParserByMimeTypes(c.parseGoBinary, internal.ExecutableMIMETypeSet.List()...),
|
||||
}
|
||||
}
|
||||
|
||||
type progressingCataloger struct {
|
||||
progress *event.CatalogerTask
|
||||
cataloger *generic.Cataloger
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Name() string {
|
||||
return p.cataloger.Name()
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
defer p.progress.SetCompleted()
|
||||
return p.cataloger.Catalog(resolver)
|
||||
}
|
||||
|
||||
@@ -1,105 +1,158 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type goLicenses struct {
|
||||
searchLocalModCacheLicenses bool
|
||||
localModCacheResolver source.FileResolver
|
||||
opts GoCatalogerOpts
|
||||
localModCacheResolver source.WritableFileResolver
|
||||
progress *event.CatalogerTask
|
||||
}
|
||||
|
||||
func newGoLicenses(opts GoCatalogerOpts) goLicenses {
|
||||
return goLicenses{
|
||||
searchLocalModCacheLicenses: opts.SearchLocalModCacheLicenses,
|
||||
localModCacheResolver: modCacheResolver(opts.LocalModCacheDir),
|
||||
opts: opts,
|
||||
localModCacheResolver: modCacheResolver(opts.localModCacheDir),
|
||||
progress: &event.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "Downloading go mod",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGoPath() string {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Debug("unable to determine user home dir: %v", err)
|
||||
} else {
|
||||
goPath = path.Join(homeDir, "go")
|
||||
func remotesForModule(proxies []string, noProxy []string, module string) []string {
|
||||
for _, pattern := range noProxy {
|
||||
if matched, err := path.Match(pattern, module); err == nil && matched {
|
||||
// matched to be direct for this module
|
||||
return directProxiesOnly
|
||||
}
|
||||
}
|
||||
|
||||
return goPath
|
||||
return proxies
|
||||
}
|
||||
|
||||
// resolver needs to be shared between mod file & binary scanners so it's only scanned once
|
||||
var modCacheResolvers = map[string]source.FileResolver{}
|
||||
|
||||
func modCacheResolver(modCacheDir string) source.FileResolver {
|
||||
if modCacheDir == "" {
|
||||
goPath := defaultGoPath()
|
||||
if goPath != "" {
|
||||
modCacheDir = path.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
}
|
||||
|
||||
if r, ok := modCacheResolvers[modCacheDir]; ok {
|
||||
return r
|
||||
}
|
||||
|
||||
var r source.FileResolver
|
||||
func modCacheResolver(modCacheDir string) source.WritableFileResolver {
|
||||
var r source.WritableFileResolver
|
||||
|
||||
if modCacheDir == "" {
|
||||
log.Trace("unable to determine mod cache directory, skipping mod cache resolver")
|
||||
r = source.NewMockResolverForPaths()
|
||||
r = source.EmptyResolver{}
|
||||
} else {
|
||||
stat, err := os.Stat(modCacheDir)
|
||||
|
||||
if os.IsNotExist(err) || stat == nil || !stat.IsDir() {
|
||||
log.Tracef("unable to open mod cache directory: %s, skipping mod cache resolver", modCacheDir)
|
||||
r = source.NewMockResolverForPaths()
|
||||
r = source.EmptyResolver{}
|
||||
} else {
|
||||
r = source.NewDeferredResolverFromSource(func() (source.Source, error) {
|
||||
return source.NewFromDirectory(modCacheDir)
|
||||
})
|
||||
r = source.NewUnindexedDirectoryResolver(modCacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
modCacheResolvers[modCacheDir] = r
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []string, err error) {
|
||||
moduleName = processCaps(moduleName)
|
||||
|
||||
licenses, err = findLicenses(resolver,
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, moduleName, moduleVersion),
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion),
|
||||
)
|
||||
|
||||
if c.searchLocalModCacheLicenses && err == nil && len(licenses) == 0 {
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
licenses, err = findLicenses(c.localModCacheResolver,
|
||||
fmt.Sprintf(`**/%s@%s/*`, moduleName, moduleVersion),
|
||||
)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
// always return a non-nil slice
|
||||
// look in the local host mod cache...
|
||||
licenses, err = c.getLicensesFromLocal(moduleName, moduleVersion)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
// we did not find it yet and remote searching was enabled
|
||||
licenses, err = c.getLicensesFromRemote(moduleName, moduleVersion)
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]string, error) {
|
||||
if !c.opts.searchLocalModCacheLicenses {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]string, error) {
|
||||
if !c.opts.searchRemoteLicenses {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proxies := remotesForModule(c.opts.proxies, c.opts.noProxy, moduleName)
|
||||
|
||||
fsys, err := getModule(c.progress, proxies, moduleName, moduleVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir := moduleDir(moduleName, moduleVersion)
|
||||
|
||||
// populate the mod cache with the results
|
||||
err = fs.WalkDir(fsys, ".", func(filePath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
f, err := fsys.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.localModCacheResolver.Write(source.NewLocation(path.Join(dir, filePath)), f)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Tracef("remote proxy walk failed for: %s", moduleName)
|
||||
}
|
||||
|
||||
return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func moduleDir(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s@%s", processCaps(moduleName), moduleVersion)
|
||||
}
|
||||
|
||||
func moduleSearchGlob(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func requireCollection(licenses []string) []string {
|
||||
if licenses == nil {
|
||||
licenses = []string{}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return
|
||||
return licenses
|
||||
}
|
||||
|
||||
func findLicenses(resolver source.FileResolver, globMatch string) (out []string, err error) {
|
||||
@@ -138,3 +191,96 @@ func processCaps(s string) string {
|
||||
return "!" + strings.ToLower(s)
|
||||
})
|
||||
}
|
||||
|
||||
func getModule(progress *event.CatalogerTask, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||
for _, proxy := range proxies {
|
||||
u, _ := url.Parse(proxy)
|
||||
if proxy == "direct" {
|
||||
fsys, err = getModuleRepository(progress, moduleName, moduleVersion)
|
||||
continue
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https", "http":
|
||||
fsys, err = getModuleProxy(progress, proxy, moduleName, moduleVersion)
|
||||
case "file":
|
||||
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
|
||||
progress.SetValue(fmt.Sprintf("file: %s", p))
|
||||
fsys = os.DirFS(p)
|
||||
}
|
||||
if fsys != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getModuleProxy(progress *event.CatalogerTask, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
||||
progress.SetValue(u)
|
||||
// get the module zip
|
||||
resp, err := http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
u = fmt.Sprintf("%s/%s/@v/%s.zip", proxy, strings.ToLower(moduleName), moduleVersion)
|
||||
progress.SetValue(u)
|
||||
// try lowercasing it; some packages have mixed casing that really messes up the proxy
|
||||
resp, err = http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
|
||||
}
|
||||
}
|
||||
// read the zip
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err = zip.NewReader(bytes.NewReader(b), resp.ContentLength)
|
||||
versionPath := findVersionPath(out, ".")
|
||||
out = getSubFS(out, versionPath)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func findVersionPath(f fs.FS, dir string) string {
|
||||
list, _ := fs.ReadDir(f, dir)
|
||||
for _, entry := range list {
|
||||
name := entry.Name()
|
||||
if strings.Contains(name, "@") {
|
||||
return name
|
||||
}
|
||||
found := findVersionPath(f, path.Join(dir, name))
|
||||
if found != "" {
|
||||
return path.Join(name, found)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getModuleRepository(progress *event.CatalogerTask, moduleName string, moduleVersion string) (fs.FS, error) {
|
||||
repoName := moduleName
|
||||
parts := strings.Split(moduleName, "/")
|
||||
if len(parts) > 2 {
|
||||
repoName = fmt.Sprintf("%s/%s/%s", parts[0], parts[1], parts[2])
|
||||
}
|
||||
progress.SetValue(fmt.Sprintf("git: %s", repoName))
|
||||
f := memfs.New()
|
||||
buf := &bytes.Buffer{}
|
||||
_, err := git.Clone(memory.NewStorage(), f, &git.CloneOptions{
|
||||
URL: fmt.Sprintf("https://%s", repoName),
|
||||
ReferenceName: plumbing.NewTagReferenceName(moduleVersion), // FIXME version might be a SHA
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
Progress: buf,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w -- %s", err, buf.String())
|
||||
}
|
||||
|
||||
return billyFSAdapter{fs: f}, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -10,7 +16,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Test_LicenseSearch(t *testing.T) {
|
||||
func Test_LocalLicenseSearch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -34,10 +40,85 @@ func Test_LicenseSearch(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(GoCatalogerOpts{
|
||||
SearchLocalModCacheLicenses: true,
|
||||
LocalModCacheDir: path.Join(wd, "test-fixtures", "licenses"),
|
||||
searchLocalModCacheLicenses: true,
|
||||
localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
|
||||
})
|
||||
licenses, err := l.getLicenses(source.MockResolver{}, test.name, test.version)
|
||||
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, licenses, 1)
|
||||
|
||||
require.Equal(t, test.expected, licenses[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := &bytes.Buffer{}
|
||||
uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/")
|
||||
|
||||
parts := strings.Split(uri, "/@v/")
|
||||
modPath := parts[0]
|
||||
modVersion := parts[1]
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
testDir := path.Join(wd, "test-fixtures", "licenses", "pkg", "mod", processCaps(modPath)+"@"+modVersion)
|
||||
|
||||
archive := zip.NewWriter(buf)
|
||||
|
||||
entries, err := os.ReadDir(testDir)
|
||||
require.NoError(t, err)
|
||||
for _, f := range entries {
|
||||
// the zip files downloaded contain a path to the repo that somewhat matches where it ends up on disk,
|
||||
// so prefix entries with something similar
|
||||
writer, err := archive.Create(path.Join("github.com/something/some@version", f.Name()))
|
||||
require.NoError(t, err)
|
||||
contents, err := os.ReadFile(path.Join(testDir, f.Name()))
|
||||
require.NoError(t, err)
|
||||
_, err = writer.Write(contents)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = archive.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
w.Header().Add("Content-Length", fmt.Sprintf("%d", buf.Len()))
|
||||
|
||||
_, err = w.Write(buf.Bytes())
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "github.com/someorg/somename",
|
||||
version: "v0.3.2",
|
||||
expected: "Apache-2.0",
|
||||
},
|
||||
{
|
||||
name: "github.com/CapORG/CapProject",
|
||||
version: "v4.111.5",
|
||||
expected: "MIT",
|
||||
},
|
||||
}
|
||||
|
||||
modDir := path.Join(t.TempDir())
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(GoCatalogerOpts{
|
||||
searchRemoteLicenses: true,
|
||||
proxies: []string{server.URL},
|
||||
localModCacheDir: modDir,
|
||||
})
|
||||
|
||||
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, licenses, 1)
|
||||
@@ -74,3 +155,42 @@ func Test_processCaps(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_remotesForModule(t *testing.T) {
|
||||
allProxies := []string{"https://somewhere.org", "direct"}
|
||||
directProxy := []string{"direct"}
|
||||
|
||||
tests := []struct {
|
||||
module string
|
||||
noProxy string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
module: "github.com/anchore/syft",
|
||||
expected: allProxies,
|
||||
},
|
||||
{
|
||||
module: "github.com/anchore/sbom-action",
|
||||
noProxy: "*/anchore/*",
|
||||
expected: directProxy,
|
||||
},
|
||||
{
|
||||
module: "github.com/anchore/sbom-action",
|
||||
noProxy: "*/user/mod,*/anchore/sbom-action",
|
||||
expected: directProxy,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.module, func(t *testing.T) {
|
||||
got := remotesForModule(allProxies, strings.Split(test.noProxy, ","), test.module)
|
||||
require.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findVersionPath(t *testing.T) {
|
||||
f := os.DirFS("test-fixtures/zip-fs")
|
||||
vp := findVersionPath(f, ".")
|
||||
require.Equal(t, "github.com/someorg/somepkg@version", vp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultProxies = "https://proxy.golang.org,direct"
|
||||
directProxyOnly = "direct"
|
||||
)
|
||||
|
||||
var (
|
||||
directProxiesOnly = []string{directProxyOnly}
|
||||
)
|
||||
|
||||
type GoCatalogerOpts struct {
|
||||
searchLocalModCacheLicenses bool
|
||||
localModCacheDir string
|
||||
searchRemoteLicenses bool
|
||||
proxies []string
|
||||
noProxy []string
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithSearchLocalModCacheLicenses(input bool) GoCatalogerOpts {
|
||||
g.searchLocalModCacheLicenses = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithLocalModCacheDir(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
g.localModCacheDir = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithSearchRemoteLicenses(input bool) GoCatalogerOpts {
|
||||
g.searchRemoteLicenses = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithProxy(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
if input == "off" {
|
||||
input = directProxyOnly
|
||||
}
|
||||
g.proxies = strings.Split(input, ",")
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithNoProxy(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
g.noProxy = strings.Split(input, ",")
|
||||
return g
|
||||
}
|
||||
|
||||
// NewGoCatalogerOpts create a GoCatalogerOpts with default options, which includes:
|
||||
// - setting the default remote proxy if none is provided
|
||||
// - setting the default no proxy if none is provided
|
||||
// - setting the default local module cache dir if none is provided
|
||||
func NewGoCatalogerOpts() GoCatalogerOpts {
|
||||
g := GoCatalogerOpts{}
|
||||
|
||||
// first process the proxy settings
|
||||
if len(g.proxies) == 0 {
|
||||
goProxy := os.Getenv("GOPROXY")
|
||||
if goProxy == "" {
|
||||
goProxy = defaultProxies
|
||||
}
|
||||
g = g.WithProxy(goProxy)
|
||||
}
|
||||
|
||||
// next process the gonoproxy settings
|
||||
if len(g.noProxy) == 0 {
|
||||
goPrivate := os.Getenv("GOPRIVATE")
|
||||
goNoProxy := os.Getenv("GONOPROXY")
|
||||
// we only use the env var if it was not set explicitly
|
||||
if goPrivate != "" {
|
||||
g.noProxy = append(g.noProxy, strings.Split(goPrivate, ",")...)
|
||||
}
|
||||
|
||||
// next process the goprivate settings; we always add those
|
||||
if goNoProxy != "" {
|
||||
g.noProxy = append(g.noProxy, strings.Split(goNoProxy, ",")...)
|
||||
}
|
||||
}
|
||||
|
||||
if g.localModCacheDir == "" {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Debug("unable to determine user home dir: %v", err)
|
||||
} else {
|
||||
goPath = path.Join(homeDir, "go")
|
||||
}
|
||||
}
|
||||
if goPath != "" {
|
||||
g.localModCacheDir = path.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Options(t *testing.T) {
|
||||
type opts struct {
|
||||
local bool
|
||||
cacheDir string
|
||||
remote bool
|
||||
proxy string
|
||||
noProxy string
|
||||
}
|
||||
|
||||
homedirCacheDisabled := homedir.DisableCache
|
||||
homedir.DisableCache = true
|
||||
t.Cleanup(func() {
|
||||
homedir.DisableCache = homedirCacheDisabled
|
||||
})
|
||||
|
||||
allEnv := map[string]string{
|
||||
"HOME": "/usr/home",
|
||||
"GOPATH": "",
|
||||
"GOPROXY": "",
|
||||
"GOPRIVATE": "",
|
||||
"GONOPROXY": "",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
opts opts
|
||||
expected GoCatalogerOpts
|
||||
}{
|
||||
{
|
||||
name: "set via env defaults",
|
||||
env: map[string]string{
|
||||
"GOPATH": "/go",
|
||||
"GOPROXY": "https://my.proxy",
|
||||
"GOPRIVATE": "my.private",
|
||||
"GONOPROXY": "no.proxy",
|
||||
},
|
||||
opts: opts{},
|
||||
expected: GoCatalogerOpts{
|
||||
searchLocalModCacheLicenses: false,
|
||||
localModCacheDir: "/go/pkg/mod",
|
||||
searchRemoteLicenses: false,
|
||||
proxies: []string{"https://my.proxy"},
|
||||
noProxy: []string{"my.private", "no.proxy"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set via configuration",
|
||||
env: map[string]string{
|
||||
"GOPATH": "/go",
|
||||
"GOPROXY": "https://my.proxy",
|
||||
"GOPRIVATE": "my.private",
|
||||
"GONOPROXY": "no.proxy",
|
||||
},
|
||||
opts: opts{
|
||||
local: true,
|
||||
cacheDir: "/go-cache",
|
||||
remote: true,
|
||||
proxy: "https://alt.proxy,direct",
|
||||
noProxy: "alt.no.proxy",
|
||||
},
|
||||
expected: GoCatalogerOpts{
|
||||
searchLocalModCacheLicenses: true,
|
||||
localModCacheDir: "/go-cache",
|
||||
searchRemoteLicenses: true,
|
||||
proxies: []string{"https://alt.proxy", "direct"},
|
||||
noProxy: []string{"alt.no.proxy"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for k, v := range allEnv {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
for k, v := range test.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
got := NewGoCatalogerOpts().
|
||||
WithSearchLocalModCacheLicenses(test.opts.local).
|
||||
WithLocalModCacheDir(test.opts.cacheDir).
|
||||
WithSearchRemoteLicenses(test.opts.remote).
|
||||
WithProxy(test.opts.proxy).
|
||||
WithNoProxy(test.opts.noProxy)
|
||||
|
||||
assert.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -510,7 +510,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
)
|
||||
|
||||
c := goBinaryCataloger{}
|
||||
pkgs := c.buildGoPkgInfo(source.NewMockResolverForPaths(), location, test.mod, test.arch)
|
||||
pkgs := c.buildGoPkgInfo(source.EmptyResolver{}, location, test.mod, test.arch)
|
||||
assert.Equal(t, test.expected, pkgs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type subFS struct {
|
||||
root string
|
||||
f fs.FS
|
||||
}
|
||||
|
||||
func getSubFS(f fs.FS, root string) fs.FS {
|
||||
if s, ok := f.(fs.SubFS); ok {
|
||||
s, err := s.Sub(root)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return newSubFS(f, root)
|
||||
}
|
||||
|
||||
func newSubFS(f fs.FS, root string) fs.FS {
|
||||
return subFS{
|
||||
root: root,
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (s subFS) Open(name string) (fs.File, error) {
|
||||
if name == "." {
|
||||
return rootFile{
|
||||
s: s,
|
||||
}, nil
|
||||
}
|
||||
return s.f.Open(path.Join(s.root, name))
|
||||
}
|
||||
|
||||
type rootFile struct {
|
||||
s subFS
|
||||
}
|
||||
|
||||
func (r rootFile) Name() string {
|
||||
return "."
|
||||
}
|
||||
|
||||
func (r rootFile) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r rootFile) Mode() fs.FileMode {
|
||||
return fs.ModePerm
|
||||
}
|
||||
|
||||
func (r rootFile) ModTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (r rootFile) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r rootFile) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r rootFile) ReadDir(_ int) ([]fs.DirEntry, error) {
|
||||
return fs.ReadDir(r.s.f, r.s.root)
|
||||
}
|
||||
|
||||
func (r rootFile) Stat() (fs.FileInfo, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r rootFile) Read(_ []byte) (int, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (r rootFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ fs.File = (*rootFile)(nil)
|
||||
var _ fs.ReadDirFile = (*rootFile)(nil)
|
||||
var _ fs.FileInfo = (*rootFile)(nil)
|
||||
|
||||
type subFsFileInfo struct {
|
||||
fi fs.FileInfo
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Name() string {
|
||||
return s.fi.Name()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Size() int64 {
|
||||
return s.fi.Size()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Mode() fs.FileMode {
|
||||
return s.fi.Mode()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) ModTime() time.Time {
|
||||
return s.fi.ModTime()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) IsDir() bool {
|
||||
return s.fi.IsDir()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Sys() any {
|
||||
return s.fi.Sys()
|
||||
}
|
||||
|
||||
var _ fs.FileInfo = (*subFsFileInfo)(nil)
|
||||
@@ -0,0 +1,27 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NewSubFS(t *testing.T) {
|
||||
f := os.DirFS("test-fixtures/zip-fs")
|
||||
f = newSubFS(f, "github.com/someorg/somepkg@version")
|
||||
var names []string
|
||||
err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
names = append(names, path)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expected := []string{
|
||||
".",
|
||||
"a-file",
|
||||
"subdir",
|
||||
"subdir/subfile",
|
||||
}
|
||||
require.Equal(t, expected, names)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2022
|
||||
Acme, inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
THIS SOFTWARE IS PROVIDED BY [Name of Organization] “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL [Name of Organisation] BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+1
@@ -0,0 +1 @@
|
||||
a file
|
||||
+1
@@ -0,0 +1 @@
|
||||
another 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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user