mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
Fix missing file hashes when running on windows system (#5341)
* test: add windows unit test job and capture dir resolver path bug - add a `Unit tests (Windows)` job running a new `make unit:windows` task, scoped to the packages that own host path handling (tests needing linux docker fixtures are skipped) - `Unit tests` is now a gate job over the Linux and Windows unit jobs - add a resolver test asserting that locations from `AllLocations` resolve again via `FilesByPath` and use posix paths. This is expected to fail on windows today, which is why file digests come back empty for `dir:` scans (#5325) - fix windows-only test files that no longer compiled Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: drop verbose output from integration tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: windows path handling outside of the file lookup bug - chroot paths are always posix (`ToChrootPath` no longer returns backslash paths on windows), same for the directory source name derived from `base` - `windows.FromPosix` no longer panics on input that isn't volume-encoded (e.g. relative or already native paths) - skip tests on windows that rely on symlink fixtures, shell script fixtures, or emulate windows paths on a posix host - disable `core.autocrlf` on the windows CI job so fixture digests match linux This leaves only the `FilesByPath` lookup failures from #5325. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test: skip posix-only fileresolver tests on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: split contained paths on posix separators on windows - `allContainedPaths` cleaned with the native separator but split on `/`, so a root reached through a symlink never had its ancestors indexed on windows - index assertions in tests now key on the posix path the index actually stores - skip symlink fixture tests and a posix-only test helper on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: revert unrelated go.sum churn and allContainedPaths change - restore `go.sum` and `.make/go.sum` (accidentally populated by a local `-mod=mod` invocation) - revert the `allContainedPaths` separator change: production passes native windows paths there, and splitting them on `/` produces drive-relative ancestors, so skip the posix-only test on windows instead - annotate the remaining `filepath` usages that operate on native paths Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: search the file tree with posix paths on windows (#5325) The directory and file resolvers key their file tree on posix paths (`/c/some/path`), but `FilesByPath`, `HasPath` and root-anchored `FilesByGlob` searched it with the native path from the chroot context (`C:\some\path`). On windows every lookup silently missed, so anything that resolves files by path found nothing. This is why file digests were empty for `dir:` scans. Requests are now converted to posix before searching the tree. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# check out text files with lf on every platform (regardless of core.autocrlf), letting git detect binary files
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# fixtures that must keep their crlf line endings byte for byte
|
||||||
|
syft/linux/testdata/unprintable -text
|
||||||
|
syft/pkg/cataloger/bitnami/testdata/no-rel/opt/bitnami/redis/.spdx-redis.spdx -text
|
||||||
|
syft/pkg/cataloger/java/testdata/jar-metadata/**/META-INF/MANIFEST.MF -text
|
||||||
@@ -49,6 +49,13 @@ jobs:
|
|||||||
Unit-Test:
|
Unit-Test:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
name: "Unit tests"
|
name: "Unit tests"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [Unit-Test-Linux, Unit-Test-Windows]
|
||||||
|
steps:
|
||||||
|
- run: echo All unit tests passed!
|
||||||
|
|
||||||
|
Unit-Test-Linux:
|
||||||
|
name: "Unit tests (Linux)"
|
||||||
runs-on: *test-runner
|
runs-on: *test-runner
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -68,6 +75,27 @@ jobs:
|
|||||||
- name: Check for capability drift
|
- name: Check for capability drift
|
||||||
run: make check-capability-drift
|
run: make check-capability-drift
|
||||||
|
|
||||||
|
Unit-Test-Windows:
|
||||||
|
name: "Unit tests (Windows)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
# go-make's setup only (not ./.github/actions/bootstrap): no apt, binny tools, or fixture cache are needed
|
||||||
|
- name: Setup go + go-make tooling
|
||||||
|
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||||
|
with:
|
||||||
|
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
# the full unit suite leans on linux docker fixtures, so this runs the windows-relevant subset
|
||||||
|
- name: Run unit tests
|
||||||
|
run: make unit:windows
|
||||||
|
|
||||||
Integration-Test:
|
Integration-Test:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
name: "Integration tests"
|
name: "Integration tests"
|
||||||
|
|||||||
+19
-1
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -52,7 +53,7 @@ func main() {
|
|||||||
raceFlag = " -race"
|
raceFlag = " -race"
|
||||||
}
|
}
|
||||||
Run(
|
Run(
|
||||||
"go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
"go test -count=1 -timeout=30m"+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
||||||
run.Env("GODEBUG", "dontfreezetheworld=1"),
|
run.Env("GODEBUG", "dontfreezetheworld=1"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -96,6 +97,23 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// windows unit tests: deliberately not hooked into "test" (no RunsOn). The full unit suite leans on
|
||||||
|
// linux docker fixture images and posix-only assumptions, so this covers the packages that own
|
||||||
|
// host path handling (where windows bugs live) and skips the tests that need docker image fixtures.
|
||||||
|
// ponytail: curated package list, widen it as more of the suite is made windows-safe.
|
||||||
|
Task{
|
||||||
|
Name: "unit:windows",
|
||||||
|
Description: "run the windows-relevant subset of unit tests",
|
||||||
|
Run: func() {
|
||||||
|
Run("go test -count=1"+
|
||||||
|
` -skip "Image|Squash|AllLayers|MixFileTypes|GivenCoordinates"`+
|
||||||
|
" ./syft/internal/windows/... ./syft/internal/fileresolver/... ./syft/source/directorysource/... ./syft/source/filesource/..."+
|
||||||
|
" ./syft/file ./syft/file/cataloger/filedigest/... ./syft/file/cataloger/filecontent/...",
|
||||||
|
run.Stdout(os.Stderr),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// default validation pipeline (replaces Taskfile `default`/`pr-validations`/`validations`).
|
// default validation pipeline (replaces Taskfile `default`/`pr-validations`/`validations`).
|
||||||
Task{
|
Task{
|
||||||
Name: "default",
|
Name: "default",
|
||||||
|
|||||||
@@ -216,5 +216,8 @@ func (r ChrootContext) ToChrootPath(nativePath string) string {
|
|||||||
responsePath = strings.TrimPrefix(responsePath, prefix)
|
responsePath = strings.TrimPrefix(responsePath, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
return responsePath
|
// chroot paths are posix regardless of host (these end up as location paths in the SBOM). This uses
|
||||||
|
// filepath (not path) since responsePath is native here (FromPosix above, then trimmed of a native prefix),
|
||||||
|
// and only filepath.ToSlash knows the host separator to convert.
|
||||||
|
return filepath.ToSlash(responsePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package fileresolver
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -10,6 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test_ChrootContext_RequestResponse(t *testing.T) {
|
func Test_ChrootContext_RequestResponse(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("fixture relies on symlinks, which git does not check out as links on windows by default")
|
||||||
|
}
|
||||||
// /
|
// /
|
||||||
// somewhere/
|
// somewhere/
|
||||||
// outside.txt
|
// outside.txt
|
||||||
@@ -563,6 +567,9 @@ func TestNewChrootContext_BaseValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestToNativeGlob(t *testing.T) {
|
func TestToNativeGlob(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("chroot context and expectations are posix paths")
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
chrootContext ChrootContext
|
chrootContext ChrootContext
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -18,6 +17,7 @@ import (
|
|||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/file"
|
"github.com/anchore/stereoscope/pkg/file"
|
||||||
|
"github.com/anchore/syft/syft/internal/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
type indexerMock struct {
|
type indexerMock struct {
|
||||||
@@ -138,6 +138,9 @@ func TestDirectoryIndexer_handleFileAccessErr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDirectoryIndexer_IncludeRootPathInIndex(t *testing.T) {
|
func TestDirectoryIndexer_IncludeRootPathInIndex(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("indexes the posix filesystem root")
|
||||||
|
}
|
||||||
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
||||||
if path != "/" {
|
if path != "/" {
|
||||||
return fs.SkipDir
|
return fs.SkipDir
|
||||||
@@ -206,11 +209,7 @@ func TestDirectoryIndexer_index(t *testing.T) {
|
|||||||
info, err := os.Stat(test.path)
|
info, err := os.Stat(test.path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// note: the index uses absolute paths, so assertions MUST keep this in mind
|
p := indexedPath(t, test.path)
|
||||||
cwd, err := os.Getwd()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p := file.Path(path.Join(cwd, test.path))
|
|
||||||
assert.Equal(t, true, tree.HasPath(p))
|
assert.Equal(t, true, tree.HasPath(p))
|
||||||
exists, ref, err := tree.File(p)
|
exists, ref, err := tree.File(p)
|
||||||
assert.Equal(t, true, exists)
|
assert.Equal(t, true, exists)
|
||||||
@@ -226,6 +225,9 @@ func TestDirectoryIndexer_index(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDirectoryIndexer_index_for_AncestorSymlinks(t *testing.T) {
|
func TestDirectoryIndexer_index_for_AncestorSymlinks(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("fixture relies on symlinks, which git does not check out as links on windows by default")
|
||||||
|
}
|
||||||
// note: this test is testing the effects from NewFromDirectory, indexTree, and addPathToIndex
|
// note: this test is testing the effects from NewFromDirectory, indexTree, and addPathToIndex
|
||||||
_, filename, _, ok := runtime.Caller(0)
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -256,11 +258,7 @@ func TestDirectoryIndexer_index_for_AncestorSymlinks(t *testing.T) {
|
|||||||
info, err := os.Stat(test.path)
|
info, err := os.Stat(test.path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// note: the index uses absolute paths, so assertions MUST keep this in mind
|
p := indexedPath(t, test.path)
|
||||||
cwd, err := os.Getwd()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p := file.Path(path.Join(cwd, test.path))
|
|
||||||
assert.Equal(t, true, tree.HasPath(p))
|
assert.Equal(t, true, tree.HasPath(p))
|
||||||
exists, ref, err := tree.File(p)
|
exists, ref, err := tree.File(p)
|
||||||
assert.Equal(t, true, exists)
|
assert.Equal(t, true, exists)
|
||||||
@@ -287,6 +285,9 @@ func TestDirectoryIndexer_index_survive_badSymlink(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T) {
|
func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("relies on symlinks and posix permissions")
|
||||||
|
}
|
||||||
// a symlink that resolves into a directory we don't have permission to traverse should be skipped
|
// a symlink that resolves into a directory we don't have permission to traverse should be skipped
|
||||||
// with a warning like any other inaccessible path, not abort the entire scan (see #3286).
|
// with a warning like any other inaccessible path, not abort the entire scan (see #3286).
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
@@ -323,6 +324,9 @@ func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) {
|
func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("fixture relies on symlinks, which git does not check out as links on windows by default")
|
||||||
|
}
|
||||||
var observedPaths []string
|
var observedPaths []string
|
||||||
pathObserver := func(_, p string, _ os.FileInfo, _ error) error {
|
pathObserver := func(_, p string, _ os.FileInfo, _ error) error {
|
||||||
fields := strings.Split(p, "testdata/symlinks-prune-indexing")
|
fields := strings.Split(p, "testdata/symlinks-prune-indexing")
|
||||||
@@ -430,6 +434,11 @@ func TestDirectoryIndexer_IndexesAllTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_allContainedPaths(t *testing.T) {
|
func Test_allContainedPaths(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// ponytail: production passes native windows paths here, which these posix cases don't represent.
|
||||||
|
// Ancestor indexing for a root reached through a symlink is likely broken on windows; fix separately.
|
||||||
|
t.Skip("cases are posix paths")
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -485,6 +494,9 @@ func Test_allContainedPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_relativePath(t *testing.T) {
|
func Test_relativePath(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("exercises a posix-only test helper")
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
basePath string
|
basePath string
|
||||||
@@ -548,3 +560,17 @@ func relativePath(basePath, givenPath string) string {
|
|||||||
|
|
||||||
return relPath
|
return relPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indexedPath returns how the given cwd-relative path is keyed in the index (absolute and always posix)
|
||||||
|
func indexedPath(t *testing.T, rel string) file.Path {
|
||||||
|
t.Helper()
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
// filepath (not path): cwd is a native host path, so it must be joined with the host separator before
|
||||||
|
// being converted to the posix form the index is keyed on
|
||||||
|
p := filepath.Join(cwd, rel)
|
||||||
|
if windows.HostRunningOnWindows() {
|
||||||
|
p = windows.ToPosix(p)
|
||||||
|
}
|
||||||
|
return file.Path(p)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package fileresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a location handed out by the resolver should be resolvable again by its own path. This is what the file
|
||||||
|
// digest cataloger relies on, and it silently found nothing on windows (see #5325).
|
||||||
|
func TestDirectory_LocationPathsRoundTrip(t *testing.T) {
|
||||||
|
// resolve symlinks up front (e.g. /var -> /private/var on macOS) so the cwd prefix matches the indexed paths
|
||||||
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "sub", "dir"), 0o755))
|
||||||
|
for _, p := range []string{"top.txt", filepath.Join("sub", "mid.txt"), filepath.Join("sub", "dir", "deep.txt")} {
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(root, p), []byte(p), 0o600))
|
||||||
|
}
|
||||||
|
|
||||||
|
// mirror `syft dir:.` from within the scanned directory
|
||||||
|
t.Chdir(root)
|
||||||
|
|
||||||
|
resolver, err := NewFromDirectory(".", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var regular int
|
||||||
|
for loc := range resolver.AllLocations(context.Background()) {
|
||||||
|
md, err := resolver.FileMetadataByLocation(loc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if md.Type != stereoscopeFile.TypeRegular {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
regular++
|
||||||
|
|
||||||
|
assert.NotContains(t, loc.RealPath, `\`, "location paths should be posix")
|
||||||
|
|
||||||
|
found, err := resolver.FilesByPath(loc.RealPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, found, 1, "unable to resolve %q by its own path", loc.RealPath)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 3, regular)
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package fileresolver
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func Test_windowsToPosix(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
windowsPath string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantPosixPath string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "basic case",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:\some\windows\place`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/some/windows/place",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "escaped case",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:\\some\\windows\\place`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/some/windows/place",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forward slash",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:/foo/bar`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/foo/bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mix slash",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:\foo/bar\`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/foo/bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "case sensitive case",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:\Foo/bAr\`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/Foo/bAr",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "special char case",
|
|
||||||
args: args{
|
|
||||||
windowsPath: `C:\ふー\バー`,
|
|
||||||
},
|
|
||||||
wantPosixPath: "/c/ふー/バー",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if gotPosixPath := windowsToPosix(tt.args.windowsPath); gotPosixPath != tt.wantPosixPath {
|
|
||||||
t.Errorf("windowsToPosix() = %v, want %v", gotPosixPath, tt.wantPosixPath)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_posixToWindows(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
posixPath string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantWindowsPath string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "basic case",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/some/windows/place",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:\some\windows\place`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "escaped case",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/some/windows/place",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:\\some\\windows\\place`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forward slash",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/foo/bar",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:/foo/bar`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mix slash",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/foo/bar",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:\foo/bar\`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "case sensitive case",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/Foo/bAr",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:\Foo/bAr\`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "special char case",
|
|
||||||
args: args{
|
|
||||||
posixPath: "/c/ふー/バー",
|
|
||||||
},
|
|
||||||
wantWindowsPath: `C:\ふー\バー`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if gotWindowsPath := posixToWindows(tt.args.posixPath); gotWindowsPath != tt.wantWindowsPath {
|
|
||||||
t.Errorf("posixToWindows() = %v, want %v", gotWindowsPath, tt.wantWindowsPath)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,11 @@ package fileresolver
|
|||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/file"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// - Verify that both the parent and the path are indexed
|
// - Verify that both the parent and the path are indexed
|
||||||
@@ -38,11 +36,7 @@ func Test_index(t *testing.T) {
|
|||||||
info, err := os.Stat(test.path)
|
info, err := os.Stat(test.path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// note: the index uses absolute paths, so assertions MUST keep this in mind
|
p := indexedPath(t, test.path)
|
||||||
cwd, err := os.Getwd()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p := file.Path(path.Join(cwd, test.path))
|
|
||||||
assert.Equal(t, true, tree.HasPath(p))
|
assert.Equal(t, true, tree.HasPath(p))
|
||||||
exists, ref, err := tree.File(p)
|
exists, ref, err := tree.File(p)
|
||||||
assert.Equal(t, true, exists)
|
assert.Equal(t, true, exists)
|
||||||
@@ -71,7 +65,9 @@ func Test_ignoresPathIfFiltered(t *testing.T) {
|
|||||||
testPath := "testdata/system_paths/target/home/place"
|
testPath := "testdata/system_paths/target/home/place"
|
||||||
cwd, cwdErr := os.Getwd()
|
cwd, cwdErr := os.Getwd()
|
||||||
require.NoError(t, cwdErr)
|
require.NoError(t, cwdErr)
|
||||||
ignorePath := path.Join(cwd, testPath)
|
// filepath (not path): filters see the native absolute path (before posix normalization), so the
|
||||||
|
// expected value must be joined with the host separator to match on windows
|
||||||
|
ignorePath := filepath.Join(cwd, testPath)
|
||||||
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
||||||
if path == ignorePath {
|
if path == ignorePath {
|
||||||
return ErrSkipPath
|
return ErrSkipPath
|
||||||
@@ -91,7 +87,8 @@ func Test_ignoresPathIfParentFiltered(t *testing.T) {
|
|||||||
|
|
||||||
cwd, cwdErr := os.Getwd()
|
cwd, cwdErr := os.Getwd()
|
||||||
require.NoError(t, cwdErr)
|
require.NoError(t, cwdErr)
|
||||||
ignorePath := path.Join(cwd, parentPath)
|
// filepath (not path): see Test_ignoresPathIfFiltered
|
||||||
|
ignorePath := filepath.Join(cwd, parentPath)
|
||||||
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
filterFn := func(_, path string, _ os.FileInfo, _ error) error {
|
||||||
if path == ignorePath {
|
if path == ignorePath {
|
||||||
return fs.SkipDir
|
return fs.SkipDir
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
@@ -34,8 +35,17 @@ func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
|
|||||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestPath converts a user (chroot) path into the path the file tree is keyed on. The tree is always posix
|
||||||
|
// (windows paths are volume-encoded, e.g. /c/some/path), while ToNativePath yields a native host path.
|
||||||
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
||||||
return r.Chroot.ToNativePath(userPath)
|
nativePath, err := r.Chroot.ToNativePath(userPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if windows.HostRunningOnWindows() {
|
||||||
|
return windows.ToPosix(nativePath), nil
|
||||||
|
}
|
||||||
|
return nativePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
|
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
|
||||||
@@ -85,10 +95,6 @@ func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if windows.HostRunningOnWindows() {
|
|
||||||
userStrPath = windows.ToPosix(userStrPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ref.HasReference() {
|
if ref.HasReference() {
|
||||||
references = append(references,
|
references = append(references,
|
||||||
file.NewVirtualLocationFromDirectory(
|
file.NewVirtualLocationFromDirectory(
|
||||||
@@ -103,8 +109,19 @@ func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
|||||||
return references, nil
|
return references, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestGlob is the glob equivalent of requestPath.
|
||||||
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
|
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
|
||||||
return r.Chroot.ToNativeGlob(pattern)
|
glob, err := r.Chroot.ToNativeGlob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// only a glob that was anchored to the root comes back native. Globs that start with a wildcard
|
||||||
|
// (e.g. **/foo) are returned untouched and are already posix, and ToPosix would root them at "/".
|
||||||
|
// This uses filepath (not path) since we are asking whether this is a native absolute path.
|
||||||
|
if windows.HostRunningOnWindows() && filepath.IsAbs(glob) {
|
||||||
|
return windows.ToPosix(glob), nil
|
||||||
|
}
|
||||||
|
return glob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
|
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ func ToPosix(windowsPath string) (posixPath string) {
|
|||||||
func FromPosix(posixPath string) (windowsPath string) {
|
func FromPosix(posixPath string) (windowsPath string) {
|
||||||
// decode the volume (e.g. /c/<path> --> C:\\) - There should always be a volume name.
|
// decode the volume (e.g. /c/<path> --> C:\\) - There should always be a volume name.
|
||||||
// The volume may be a UNC path (e.g. /\\localhost\C$\ --> \\localhost\C$\)
|
// The volume may be a UNC path (e.g. /\\localhost\C$\ --> \\localhost\C$\)
|
||||||
|
if !strings.HasPrefix(posixPath, "/") {
|
||||||
|
// not a volume-encoded posix path (e.g. relative or already native), so there is nothing to decode
|
||||||
|
return posixPath
|
||||||
|
}
|
||||||
pathFields := strings.Split(posixPath, "/")
|
pathFields := strings.Split(posixPath, "/")
|
||||||
if len(pathFields) < 2 {
|
if len(pathFields) < 2 {
|
||||||
// no separator means no volume to decode
|
// no separator means no volume to decode
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToPosix(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
`C:\some\windows\place`: "/c/some/windows/place",
|
||||||
|
`C:\\some\\windows\\place`: "/c/some/windows/place",
|
||||||
|
`C:/foo/bar`: "/c/foo/bar",
|
||||||
|
`C:\foo/bar\`: "/c/foo/bar",
|
||||||
|
`C:\Foo/bAr\`: "/c/Foo/bAr",
|
||||||
|
`C:\ふー\バー`: "/c/ふー/バー",
|
||||||
|
}
|
||||||
|
for in, want := range tests {
|
||||||
|
assert.Equal(t, want, ToPosix(in), in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromPosix(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"/c/some/windows/place": `C:\some\windows\place`,
|
||||||
|
"/c/Foo/bAr": `C:\Foo\bAr`,
|
||||||
|
"/c/ふー/バー": `C:\ふー\バー`,
|
||||||
|
// non volume-encoded input is left alone
|
||||||
|
"relative/path": "relative/path",
|
||||||
|
`D:\already\native`: `D:\already\native`,
|
||||||
|
}
|
||||||
|
for in, want := range tests {
|
||||||
|
assert.Equal(t, want, FromPosix(in), in)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,7 +216,8 @@ func cleanDirPath(path, base string) string {
|
|||||||
|
|
||||||
relPath, err := filepath.Rel(cleanBase, cleanRoot)
|
relPath, err := filepath.Rel(cleanBase, cleanRoot)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
path = relPath
|
// filepath.Rel yields a native path, but source names are posix like every other path syft reports
|
||||||
|
path = filepath.ToSlash(relPath)
|
||||||
}
|
}
|
||||||
// this is odd, but this means we can't use base
|
// this is odd, but this means we can't use base
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
@@ -325,6 +326,9 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
|
func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("emulates windows paths on a posix host, see directory_source_win_test.go for the windows equivalent")
|
||||||
|
}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
root string
|
root string
|
||||||
@@ -465,10 +469,11 @@ func Test_DirectorySource_ID(t *testing.T) {
|
|||||||
testutil.Chdir(t, "..") // run with source/testdata
|
testutil.Chdir(t, "..") // run with source/testdata
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cfg Config
|
cfg Config
|
||||||
want artifact.ID
|
want artifact.ID
|
||||||
wantErr require.ErrorAssertionFunc
|
wantErr require.ErrorAssertionFunc
|
||||||
|
skipOnWindows bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
@@ -486,6 +491,8 @@ func Test_DirectorySource_ID(t *testing.T) {
|
|||||||
name: "with odd unclean path through non-existent directory",
|
name: "with odd unclean path through non-existent directory",
|
||||||
cfg: Config{Path: "testdata/does-not-exist/../"},
|
cfg: Config{Path: "testdata/does-not-exist/../"},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
|
// windows cleans the path lexically, so the non-existent directory is never visited
|
||||||
|
skipOnWindows: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to a file (not a directory)",
|
name: "to a file (not a directory)",
|
||||||
@@ -543,6 +550,9 @@ func Test_DirectorySource_ID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.skipOnWindows && runtime.GOOS == "windows" {
|
||||||
|
t.Skip("posix-specific path behavior")
|
||||||
|
}
|
||||||
if tt.wantErr == nil {
|
if tt.wantErr == nil {
|
||||||
tt.wantErr = require.NoError
|
tt.wantErr = require.NoError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ func Test_DirectorySource_crossPlatformExclusions(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, f := range fns {
|
for _, f := range fns {
|
||||||
result := f(test.path, nil, nil)
|
// a matched exclusion is signaled by a skip error
|
||||||
require.Equal(t, test.match, result)
|
err := f("", test.path, nil, nil)
|
||||||
|
require.Equal(t, test.match, err != nil)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -96,6 +97,9 @@ func TestNewFromFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewFromFile_WithArchive(t *testing.T) {
|
func TestNewFromFile_WithArchive(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("archive fixtures are generated with a shell script")
|
||||||
|
}
|
||||||
testutil.Chdir(t, "..") // run with source/testdata
|
testutil.Chdir(t, "..") // run with source/testdata
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user