Files
syft/syft/internal/fileresolver/chroot_context.go
T
Alex Goodman 6ac7afb439 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>
2026-09-30 09:08:13 -04:00

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)
}