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
@@ -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)
}
+30 -11
View File
@@ -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)
}
+198 -52
View File
@@ -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
}
+124 -4
View File
@@ -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)
}
+114
View File
@@ -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
}
+99
View File
@@ -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)
})
}
+117
View File
@@ -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)
+27
View File
@@ -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.