mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
* 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>
224 lines
6.6 KiB
Go
224 lines
6.6 KiB
Go
package fileresolver
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/anchore/syft/syft/internal/windows"
|
|
)
|
|
|
|
// ChrootContext helps to modify path from a real filesystem to a chroot-like filesystem, taking into account
|
|
// the user given root, the base path (if any) to consider as the root, and the current working directory.
|
|
// Note: this only works on a real filesystem, not on a virtual filesystem (such as a stereoscope filetree).
|
|
type ChrootContext struct {
|
|
root string
|
|
rootRelativeToBase string
|
|
base string
|
|
cwd string
|
|
cwdRelativeToRoot string
|
|
}
|
|
|
|
func NewChrootContextFromCWD(root, base string) (*ChrootContext, error) {
|
|
currentWD, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get current working directory: %w", err)
|
|
}
|
|
|
|
return NewChrootContext(root, base, currentWD)
|
|
}
|
|
|
|
func NewChrootContext(root, base, cwd string) (*ChrootContext, error) {
|
|
cleanRoot, err := NormalizeRootDirectory(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cleanBase, err := NormalizeBaseDirectory(base)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// we need to track the relative path from root to base (if set) so that request paths can un-apply the base path
|
|
// changes from any incoming requests.
|
|
var rootRelativeToBase string
|
|
if cleanBase != cleanRoot && cleanBase != "" {
|
|
absRoot := cleanRoot
|
|
if !filepath.IsAbs(cleanRoot) {
|
|
absRoot = filepath.Join(cwd, cleanRoot)
|
|
}
|
|
|
|
rootRelativeToBase, err = filepath.Rel(absRoot, cleanBase) // validate that base is within root
|
|
if err != nil {
|
|
return nil, fmt.Errorf("base path %q is not within root path %q: %w", cleanBase, cleanRoot, err)
|
|
}
|
|
}
|
|
|
|
chroot := &ChrootContext{
|
|
root: cleanRoot,
|
|
rootRelativeToBase: rootRelativeToBase,
|
|
base: cleanBase,
|
|
cwd: cwd,
|
|
}
|
|
|
|
return chroot, chroot.ChangeDirectory(cwd)
|
|
}
|
|
|
|
func NormalizeRootDirectory(root string) (string, error) {
|
|
cleanRoot, err := filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not evaluate root=%q symlinks: %w", root, err)
|
|
}
|
|
return cleanRoot, nil
|
|
}
|
|
|
|
func NormalizeBaseDirectory(base string) (string, error) {
|
|
if base == "" {
|
|
return "", nil
|
|
}
|
|
|
|
cleanBase, err := filepath.EvalSymlinks(base)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not evaluate base=%q symlinks: %w", base, err)
|
|
}
|
|
|
|
return filepath.Abs(cleanBase)
|
|
}
|
|
|
|
// Root returns the root path with all symlinks evaluated.
|
|
func (r ChrootContext) Root() string {
|
|
return r.root
|
|
}
|
|
|
|
// Base returns the absolute base path with all symlinks evaluated.
|
|
func (r ChrootContext) Base() string {
|
|
return r.base
|
|
}
|
|
|
|
// ChangeRoot swaps the path for the chroot.
|
|
func (r *ChrootContext) ChangeRoot(dir string) error {
|
|
newR, err := NewChrootContext(dir, r.base, r.cwd)
|
|
if err != nil {
|
|
return fmt.Errorf("could not change root: %w", err)
|
|
}
|
|
|
|
*r = *newR
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChangeDirectory changes the current working directory so that any relative paths passed
|
|
// into ToNativePath() and ToChrootPath() honor the new CWD. If the process changes the CWD in-flight, this should be
|
|
// called again to ensure correct functionality of ToNativePath() and ToChrootPath().
|
|
func (r *ChrootContext) ChangeDirectory(dir string) error {
|
|
var (
|
|
cwdRelativeToRoot string
|
|
err error
|
|
)
|
|
|
|
dir, err = filepath.Abs(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("could not determine absolute path to CWD: %w", err)
|
|
}
|
|
|
|
if path.IsAbs(r.root) {
|
|
cwdRelativeToRoot, err = filepath.Rel(dir, r.root)
|
|
if err != nil {
|
|
return fmt.Errorf("could not determine given root path to CWD: %w", err)
|
|
}
|
|
} else {
|
|
cwdRelativeToRoot = filepath.Clean(r.root)
|
|
}
|
|
|
|
r.cwd = dir
|
|
r.cwdRelativeToRoot = cwdRelativeToRoot
|
|
return nil
|
|
}
|
|
|
|
// ToNativePath takes a path in the context of the chroot-like filesystem and converts it to a path in the underlying fs domain.
|
|
func (r ChrootContext) ToNativePath(chrootPath string) (string, error) {
|
|
responsePath := chrootPath
|
|
|
|
if filepath.IsAbs(responsePath) {
|
|
// don't allow input to potentially hop above root path (and still un-apply any base paths)
|
|
responsePath = path.Join(r.root, r.rootRelativeToBase, responsePath)
|
|
} else {
|
|
// ensure we take into account any relative difference between the root path and the CWD for relative requests
|
|
responsePath = path.Join(r.cwdRelativeToRoot, responsePath)
|
|
}
|
|
|
|
var err error
|
|
responsePath, err = filepath.Abs(responsePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return responsePath, nil
|
|
}
|
|
|
|
func (r ChrootContext) ToNativeGlob(chrootPath string) (string, error) {
|
|
// split on any *
|
|
parts := strings.Split(chrootPath, "*")
|
|
if len(parts) == 0 || parts[0] == "" {
|
|
// either this is an empty string or a path that starts with * so there is nothing we can do
|
|
return chrootPath, nil
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
// this has no glob, treat it like a path
|
|
return r.ToNativePath(chrootPath)
|
|
}
|
|
|
|
responsePath := parts[0]
|
|
|
|
if filepath.IsAbs(responsePath) {
|
|
// don't allow input to potentially hop above root path
|
|
responsePath = path.Join(r.root, responsePath)
|
|
} else {
|
|
// ensure we take into account any relative difference between the root path and the CWD for relative requests
|
|
responsePath = path.Join(r.cwdRelativeToRoot, responsePath)
|
|
}
|
|
|
|
var err error
|
|
responsePath, err = filepath.Abs(responsePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hasSlash := strings.HasSuffix(parts[0], "/")
|
|
parts[0] = responsePath
|
|
if hasSlash {
|
|
parts[0] = strings.TrimRight(parts[0], "/") + "/"
|
|
}
|
|
|
|
return strings.Join(parts, "*"), nil
|
|
}
|
|
|
|
// ToChrootPath takes a path from the underlying fs domain and converts it to a path that is relative to the current root context.
|
|
func (r ChrootContext) ToChrootPath(nativePath string) string {
|
|
responsePath := nativePath
|
|
// check to see if we need to encode back to Windows from posix
|
|
if windows.HostRunningOnWindows() {
|
|
responsePath = windows.FromPosix(responsePath)
|
|
}
|
|
|
|
// clean references to the request path (either the root, or the base if set)
|
|
if filepath.IsAbs(responsePath) {
|
|
var prefix string
|
|
if r.base != "" {
|
|
prefix = r.base
|
|
} else {
|
|
// we need to account for the cwd relative to the running process and the given root for the directory resolver
|
|
prefix = filepath.Clean(filepath.Join(r.cwd, r.cwdRelativeToRoot))
|
|
prefix += string(filepath.Separator)
|
|
}
|
|
responsePath = strings.TrimPrefix(responsePath, prefix)
|
|
}
|
|
|
|
// 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)
|
|
}
|