103 lines
3.6 KiB
Go
103 lines
3.6 KiB
Go
package managedruntime
|
|
|
|
import (
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const npxExecutionKeyLength = 16
|
|
|
|
// NpxExecutionCacheKey returns npm's deterministic _npx execution-tree key
|
|
// for one trusted package spec. The package spec must be the exact value that
|
|
// the managed runtime command passes to npx.
|
|
func NpxExecutionCacheKey(packageSpec string) string {
|
|
digest := sha512.Sum512([]byte(packageSpec))
|
|
return hex.EncodeToString(digest[:])[:npxExecutionKeyLength]
|
|
}
|
|
|
|
// ValidateExactPackageSpec accepts only a package name followed by a stable
|
|
// version. Cache repair must not accept a path or an unversioned npm selector.
|
|
func ValidateExactPackageSpec(packageSpec string) error {
|
|
if strings.TrimSpace(packageSpec) != packageSpec || packageSpec == "" {
|
|
return errors.New("managed runtime package spec is not exact")
|
|
}
|
|
if strings.ContainsAny(packageSpec, "\x00\r\n\\") || strings.Contains(packageSpec, "..") {
|
|
return errors.New("managed runtime package spec is invalid")
|
|
}
|
|
separator := strings.LastIndexByte(packageSpec, '@')
|
|
if separator <= 0 || separator == len(packageSpec)-1 {
|
|
return errors.New("managed runtime package spec must include a version")
|
|
}
|
|
if err := validateManagedPackageName(packageSpec[:separator]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := ParseStableVersion(packageSpec[separator+1:]); err != nil {
|
|
return fmt.Errorf("managed runtime package spec version: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateManagedPackageName(packageName string) error {
|
|
if filepath.IsAbs(packageName) || strings.Contains(packageName, ":") || strings.HasSuffix(packageName, "/") {
|
|
return errors.New("managed runtime package spec is path-like")
|
|
}
|
|
if !strings.HasPrefix(packageName, "@") {
|
|
if strings.ContainsAny(packageName, "@/") {
|
|
return errors.New("managed runtime package spec is path-like")
|
|
}
|
|
return nil
|
|
}
|
|
if strings.Contains(packageName[1:], "@") {
|
|
return errors.New("managed runtime package spec has an invalid scope")
|
|
}
|
|
parts := strings.Split(packageName, "/")
|
|
if len(parts) != 2 || parts[0] == "@" || parts[1] == "" {
|
|
return errors.New("managed runtime package spec has an invalid scope")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveNpxExecutionTree removes only the _npx tree derived from packageSpec.
|
|
// It refuses broad roots and symlinked paths so a cache repair cannot escape
|
|
// the exact trusted execution key.
|
|
func RemoveNpxExecutionTree(cacheRoot, packageSpec string) error {
|
|
root, err := validateNpmCacheRoot(cacheRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
packageSpec = strings.TrimSpace(packageSpec)
|
|
if packageSpec == "" || strings.ContainsRune(packageSpec, '\x00') || strings.Contains(packageSpec, "..") {
|
|
return errors.New("managed runtime package spec is invalid")
|
|
}
|
|
|
|
key := NpxExecutionCacheKey(packageSpec)
|
|
return removeNpxExecutionTree(root, key)
|
|
}
|
|
|
|
func validateNpmCacheRoot(cacheRoot string) (string, error) {
|
|
cacheRoot = strings.TrimSpace(cacheRoot)
|
|
if cacheRoot == "" || !filepath.IsAbs(cacheRoot) {
|
|
return "", fmt.Errorf("npm cache root is not absolute: %q", cacheRoot)
|
|
}
|
|
root := filepath.Clean(cacheRoot)
|
|
if root == string(filepath.Separator) || filepath.Base(root) == "_npx" {
|
|
return "", errors.New("refusing to invalidate npm cache root")
|
|
}
|
|
if info, err := os.Lstat(root); err == nil {
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return "", fmt.Errorf("refusing to invalidate execution cache through symlink: %s", root)
|
|
}
|
|
if !info.IsDir() {
|
|
return "", fmt.Errorf("npm cache root is not a directory: %s", root)
|
|
}
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return "", fmt.Errorf("inspect npm cache root: %w", err)
|
|
}
|
|
return root, nil
|
|
}
|