From 01dc78ccc3d3a0434f7bb6c4a494899171ca69f9 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Thu, 6 Jan 2022 11:39:04 -0500 Subject: [PATCH] 683 windows filepath (#735) Support Windows Directory Resolver Add function that converts windows to posix functionality Add function that converts posix to windows Add build tags to remove windows developer environment errors redact carriage return specific windows issues Signed-off-by: Christopher Phillips --- internal/file/zip_file_manifest_test.go | 3 + internal/file/zip_file_traversal_test.go | 3 + internal/file/zip_read_closer_test.go | 3 + internal/formats/common/testutils/utils.go | 8 ++ syft/source/directory_resolver.go | 56 +++++++- syft/source/directory_resolver_test.go | 15 +-- .../source/directory_resolver_windows_test.go | 126 ++++++++++++++++++ syft/source/file_metadata_test.go | 3 + syft/source/source_test.go | 3 + 9 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 syft/source/directory_resolver_windows_test.go 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 (