mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* fix(sandbox): bind parent dir for globstar allow_write on bwrap
Fine-grained per-path mounts under read-only project binds broke pip
install into in-project .venv directories. Always mount the parent tree
for ** write rules instead.
Fixes #315
* test(sandbox): tighten globstar bind assertions and ensure ~/.npm exists for e2e
Strengthen TestBubblewrapAllowWriteGlobstarBindsParentOnly to verify the
parent dir is writably bound and the child path is read-only bound, not
just substring presence. Pre-create ~/.npm in the e2e harness so
bubblewrap --bind-try does not skip the npm cache dir on fresh runners.
* switch pnpm to /tmp in sandbox e2e
* test(sandbox): update glob ** test for parent-bind semantics
Globstar allow_write now binds the parent dir only (e2e740d), so the
test should assert the parent is writably bound and child subdirs are
not individually bound, instead of substring-matching subdir names.
* fix(sandbox): bind correct base dir for in-pattern globstar allow_write
Globstar allow_write previously used extractGlobParentDir, which walks past
the first ** and yields the wrong root for patterns like /a/b/**/d/**/e.
Introduce extractGlobstarWriteBaseDir, which takes the prefix before the
first /**, and use it in processWriteRule. Also dedup the coarse-fallback
parent-bind loop to mirror the read-rule fallback.
127 lines
3.9 KiB
Go
127 lines
3.9 KiB
Go
//go:build linux
|
|
|
|
package platform
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/safedep/dry/log"
|
|
"github.com/safedep/pmg/sandbox/util"
|
|
)
|
|
|
|
// expandGlobstarPattern expands patterns containing ** (recursive glob),
|
|
// which filepath.Glob does not support. Splits the pattern at ** into a base
|
|
// path and a suffix, walks the base with a depth limit, and collects entries
|
|
// whose path ends with the suffix.
|
|
//
|
|
// If the base path does not yet exist, returns []string{basePath} so callers
|
|
// can still grant coverage to the parent directory (matters for fresh
|
|
// node_modules / pnpm caches that haven't been created yet).
|
|
func expandGlobstarPattern(pattern string, maxDepth, maxPaths int) ([]string, error) {
|
|
parts := strings.Split(pattern, "**")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("only one ** globstar supported per pattern")
|
|
}
|
|
|
|
basePath := strings.TrimSuffix(parts[0], "/")
|
|
suffix := strings.TrimPrefix(parts[1], "/")
|
|
|
|
if basePath == "" {
|
|
log.Debugf("Skipping globstar pattern '%s' with empty base path (would walk from root)", pattern)
|
|
return []string{}, nil
|
|
}
|
|
|
|
expandedBase, err := util.ExpandVariables(basePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to expand base path: %w", err)
|
|
}
|
|
basePath = expandedBase
|
|
|
|
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
|
return []string{basePath}, nil
|
|
}
|
|
|
|
matches := []string{}
|
|
if err := walkGlobWithDepthLimit(basePath, suffix, maxDepth, maxPaths, &matches); err != nil {
|
|
return nil, fmt.Errorf("failed to walk directory tree: %w", err)
|
|
}
|
|
return matches, nil
|
|
}
|
|
|
|
// walkGlobWithDepthLimit walks a directory tree from root, appending paths
|
|
// whose suffix matches `suffix`. Stops at maxDepth levels (when > 0) and
|
|
// after collecting maxPaths entries.
|
|
func walkGlobWithDepthLimit(root, suffix string, maxDepth, maxPaths int, matches *[]string) error {
|
|
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
depth := 0
|
|
if relPath != "." {
|
|
depth = len(strings.Split(relPath, string(filepath.Separator)))
|
|
}
|
|
if maxDepth > 0 && depth > maxDepth {
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if suffix == "" || strings.HasSuffix(path, suffix) {
|
|
*matches = append(*matches, path)
|
|
if len(*matches) >= maxPaths {
|
|
return filepath.SkipAll
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// extractGlobParentDir extracts the parent directory of a glob pattern. Used
|
|
// for coarse-grained fallback when expansion yields too many paths or the
|
|
// target tree is unsuitable for fine-grained rules.
|
|
//
|
|
// Examples:
|
|
// - ${CWD}/node_modules/** → ${CWD}/node_modules
|
|
// - ${HOME}/.cache/pnpm/** → ${HOME}/.cache/pnpm
|
|
// - /tmp/*.txt → /tmp
|
|
// - /usr/lib/**/*.so → /usr/lib
|
|
func extractGlobParentDir(pattern string) string {
|
|
pattern = strings.TrimSuffix(pattern, "/**")
|
|
pattern = strings.TrimSuffix(pattern, "/*")
|
|
|
|
idx := strings.IndexAny(pattern, "*?[")
|
|
if idx >= 0 {
|
|
pattern = pattern[:idx]
|
|
pattern = filepath.Dir(pattern)
|
|
}
|
|
pattern = strings.TrimSuffix(pattern, string(filepath.Separator))
|
|
|
|
if pattern == "" || pattern == string(filepath.Separator) {
|
|
return "."
|
|
}
|
|
return pattern
|
|
}
|
|
|
|
// extractGlobstarWriteBaseDir returns the directory to bind read-write for an
|
|
// allow_write globstar pattern. It uses the path before the first "/**" so
|
|
// suffix-only profiles (${CWD}/.venv/**) and in-pattern globstars (/a/b/**/d/**/e)
|
|
// both bind the intended tree root (/a/b for the latter). Falls back to
|
|
// extractGlobParentDir when the pattern has ** but no "/**" segment.
|
|
func extractGlobstarWriteBaseDir(pattern string) string {
|
|
if i := strings.Index(pattern, "/**"); i >= 0 {
|
|
base := strings.TrimSuffix(pattern[:i], string(filepath.Separator))
|
|
if base == "" {
|
|
return string(filepath.Separator)
|
|
}
|
|
return base
|
|
}
|
|
return extractGlobParentDir(pattern)
|
|
}
|