diff --git a/internal/file/zip_file_manifest_test.go b/internal/file/zip_file_manifest_test.go index 203605e51..f89e88ca9 100644 --- a/internal/file/zip_file_manifest_test.go +++ b/internal/file/zip_file_manifest_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package file import ( diff --git a/internal/file/zip_file_traversal_test.go b/internal/file/zip_file_traversal_test.go index bf32528ac..6c7a9ea46 100644 --- a/internal/file/zip_file_traversal_test.go +++ b/internal/file/zip_file_traversal_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package file import ( diff --git a/internal/file/zip_read_closer_test.go b/internal/file/zip_read_closer_test.go index 1ea251665..349bfcc9b 100644 --- a/internal/file/zip_read_closer_test.go +++ b/internal/file/zip_read_closer_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package file import ( diff --git a/internal/formats/common/testutils/utils.go b/internal/formats/common/testutils/utils.go index a5990d4a2..0a086c1e5 100644 --- a/internal/formats/common/testutils/utils.go +++ b/internal/formats/common/testutils/utils.go @@ -2,6 +2,7 @@ package testutils import ( "bytes" + "strings" "testing" "github.com/anchore/go-presenter" @@ -51,6 +52,7 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which should be tested independently + redactors = append(redactors, carriageRedactor) for _, r := range redactors { actual = r(actual) expected = r(expected) @@ -79,6 +81,7 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which should be tested independently + redactors = append(redactors, carriageRedactor) for _, r := range redactors { actual = r(actual) expected = r(expected) @@ -138,6 +141,11 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO } } +func carriageRedactor(s []byte) []byte { + msg := strings.ReplaceAll(string(s), "\r\n", "\n") + return []byte(msg) +} + func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { _, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks) _, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks) diff --git a/syft/source/directory_resolver.go b/syft/source/directory_resolver.go index 0d329a07e..6a3fe31e6 100644 --- a/syft/source/directory_resolver.go +++ b/syft/source/directory_resolver.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "runtime" "strings" "github.com/anchore/syft/internal" @@ -21,6 +22,8 @@ import ( "github.com/wagoodman/go-progress" ) +const WindowsOS = "windows" + var unixSystemRuntimePrefixes = []string{ "/proc", "/dev", @@ -143,6 +146,11 @@ func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error) return "", nil } + // here we check to see if we need to normalize paths to posix on the way in coming from windows + if runtime.GOOS == WindowsOS { + path = windowsToPosix(path) + } + newRoot, err := r.addPathToIndex(path, info) if r.isFileAccessErr(path, err) { return "", nil @@ -258,6 +266,11 @@ func (r directoryResolver) requestPath(userPath string) (string, error) { } func (r directoryResolver) responsePath(path string) string { + // check to see if we need to encode back to Windows from posix + if runtime.GOOS == WindowsOS { + path = posixToWindows(path) + } + // always return references relative to the request path (not absolute path) if filepath.IsAbs(path) { // we need to account for the cwd relative to the running process and the given root for the directory resolver @@ -314,6 +327,10 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error) continue } + if runtime.GOOS == WindowsOS { + userStrPath = windowsToPosix(userStrPath) + } + exists, ref, err := r.fileTree.File(file.Path(userStrPath)) if err == nil && exists { references = append(references, NewLocationFromDirectory(r.responsePath(userStrPath), *ref)) @@ -367,7 +384,13 @@ func (r directoryResolver) FileContentsByLocation(location Location) (io.ReadClo // by preference or these files are not readable by the current user). return nil, fmt.Errorf("file content is inaccessible path=%q", location.ref.RealPath) } - return file.NewLazyReadCloser(string(location.ref.RealPath)), nil + // RealPath is posix so for windows directory resolver we need to translate + // to its true on disk path. + filePath := string(location.ref.RealPath) + if runtime.GOOS == WindowsOS { + filePath = posixToWindows(filePath) + } + return file.NewLazyReadCloser(filePath), nil } func (r directoryResolver) isInIndex(location Location) bool { @@ -409,6 +432,33 @@ func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error) return locations, nil } +func windowsToPosix(windowsPath string) (posixPath string) { + // volume should be encoded at the start (e.g /c/) where c is the volume + volumeName := filepath.VolumeName(windowsPath) + pathWithoutVolume := strings.TrimPrefix(windowsPath, volumeName) + volumeLetter := strings.ToLower(strings.TrimSuffix(volumeName, ":")) + + // translate non-escaped backslash to forwardslash + translatedPath := strings.ReplaceAll(pathWithoutVolume, "\\", "/") + + // always have `/` as the root... join all components, e.g.: + // convert: C:\\some\windows\Place + // into: /c/some/windows/Place + return path.Clean("/" + strings.Join([]string{volumeLetter, translatedPath}, "/")) +} + +func posixToWindows(posixPath string) (windowsPath string) { + // decode the volume (e.g. /c/ --> C:\\) - There should always be a volume name. + pathFields := strings.Split(posixPath, "/") + volumeName := strings.ToUpper(pathFields[1]) + `:\\` + + // translate non-escaped forward slashes into backslashes + remainingTranslatedPath := strings.Join(pathFields[2:], "\\") + + // combine volume name and backslash components + return filepath.Clean(volumeName + remainingTranslatedPath) +} + func isUnixSystemRuntimePath(path string, _ os.FileInfo) bool { return internal.HasAnyOfPrefixes(path, unixSystemRuntimePrefixes...) } @@ -421,8 +471,8 @@ func isUnallowableFileType(_ string, info os.FileInfo) bool { switch newFileTypeFromMode(info.Mode()) { case CharacterDevice, Socket, BlockDevice, FIFONode, IrregularFile: return true - // note: symlinks that point to these files may still get by. We handle this later in processing to help prevent - // against infinite links traversal. + // note: symlinks that point to these files may still get by. + // We handle this later in processing to help prevent against infinite links traversal. } return false diff --git a/syft/source/directory_resolver_test.go b/syft/source/directory_resolver_test.go index d820574b2..a95634aaf 100644 --- a/syft/source/directory_resolver_test.go +++ b/syft/source/directory_resolver_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package source import ( @@ -175,12 +178,12 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) { hasPath := resolver.HasPath(c.input) if !c.forcePositiveHasPath { if c.refCount != 0 && !hasPath { - t.Errorf("expected HasPath() to indicate existance, but did not") + t.Errorf("expected HasPath() to indicate existence, but did not") } else if c.refCount == 0 && hasPath { - t.Errorf("expeced HasPath() to NOT indicate existance, but does") + t.Errorf("expected HasPath() to NOT indicate existence, but does") } } else if !hasPath { - t.Errorf("expected HasPath() to indicate existance, but did not (force path)") + t.Errorf("expected HasPath() to indicate existence, but did not (force path)") } refs, err := resolver.FilesByPath(c.input) @@ -553,7 +556,6 @@ func Test_indexAllRoots(t *testing.T) { } func Test_directoryResolver_FilesByMIMEType(t *testing.T) { - tests := []struct { fixturePath string mimeType string @@ -567,7 +569,6 @@ func Test_directoryResolver_FilesByMIMEType(t *testing.T) { } for _, test := range tests { t.Run(test.fixturePath, func(t *testing.T) { - resolver, err := newDirectoryResolver(test.fixturePath) assert.NoError(t, err) locations, err := resolver.FilesByMIMEType(test.mimeType) @@ -648,7 +649,6 @@ func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) { locations, err = resolver.FilesByPath("./link_to_link_to_readme") require.NoError(t, err) assert.Len(t, locations, 1) - } func Test_directoryResolver_FileContentsByLocation(t *testing.T) { @@ -683,10 +683,9 @@ func Test_directoryResolver_FileContentsByLocation(t *testing.T) { if test.err { require.Error(t, err) return - } else { - require.NoError(t, err) } + require.NoError(t, err) if test.expects != "" { b, err := ioutil.ReadAll(actual) require.NoError(t, err) diff --git a/syft/source/directory_resolver_windows_test.go b/syft/source/directory_resolver_windows_test.go new file mode 100644 index 000000000..18cbb7856 --- /dev/null +++ b/syft/source/directory_resolver_windows_test.go @@ -0,0 +1,126 @@ +package source + +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) + } + }) + } +} diff --git a/syft/source/file_metadata_test.go b/syft/source/file_metadata_test.go index db714d0f7..3bdedb42f 100644 --- a/syft/source/file_metadata_test.go +++ b/syft/source/file_metadata_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package source import ( diff --git a/syft/source/source_test.go b/syft/source/source_test.go index 98e77454f..f253fed13 100644 --- a/syft/source/source_test.go +++ b/syft/source/source_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package source import (