New internal/imagepull/ subpackage. Three concerns, each independently testable: Pull (imagepull.go): - github.com/google/go-containerregistry's remote.Image with the linux/amd64 platform pinned. Anonymous pulls only for v1. - Layer blobs cached on disk via cache.NewFilesystemCache under <cacheDir>/blobs/sha256/<hex> — OCI-standard layout so skopeo/crane could co-exist later. - Eagerly touches every layer once so network errors surface at Pull time, not deep in Flatten. Flatten (flatten.go): - Replays layers oldest-first into destDir. - Whiteout-aware: .wh.<name> deletes the named entry, .wh..wh..opq wipes the parent directory's contents from prior layers. - Path-traversal hardening mirrored from kernelcat extractTar: reject .., absolute paths, and symlinks/hardlinks whose resolved target escapes destDir. - Handles tar.TypeReg, TypeDir, TypeSymlink, TypeLink. Skips device/fifo nodes silently (need privilege; udev/devtmpfs handles them in the guest). BuildExt4 (ext4.go): - Truncates outFile to sizeBytes, then runs `mkfs.ext4 -F -d <srcDir> -E root_owner=0:0`. No mount, no sudo, no loopback. - 64 MiB floor; callers handle real sizing with content-aware headroom. - File ownership in the resulting ext4 reflects srcDir's on-disk ownership — runner's uid/gid since extraction was unprivileged. Documented in package doc as a Phase A v1 limitation; Phase B will add a debugfs- or tar2ext4-based ownership fixup. paths.Layout gains OCICacheDir at $XDG_CACHE_HOME/banger/oci/, ensured at startup alongside the other dirs. Tests use go-containerregistry's in-process registry to push and pull synthetic multi-layer images. Cover: layer caching round-trip, whiteout + opaque-marker handling, path-traversal rejection, unsafe symlink rejection, real mkfs.ext4 round-trip (skipped if mkfs.ext4 absent), and tiny-size rejection. go-containerregistry v0.21.5 added as a direct dep, plus its transitive closure (containerd/stargz, opencontainers/go-digest, docker/cli config helpers, etc). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
package paths
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Layout struct {
|
|
ConfigHome string
|
|
StateHome string
|
|
CacheHome string
|
|
RuntimeHome string
|
|
ConfigDir string
|
|
StateDir string
|
|
CacheDir string
|
|
RuntimeDir string
|
|
SocketPath string
|
|
DBPath string
|
|
DaemonLog string
|
|
VMsDir string
|
|
ImagesDir string
|
|
KernelsDir string
|
|
OCICacheDir string
|
|
}
|
|
|
|
func Resolve() (Layout, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return Layout{}, err
|
|
}
|
|
configHome := getenvDefault("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
stateHome := getenvDefault("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
|
|
cacheHome := getenvDefault("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
|
runtimeHome := os.Getenv("XDG_RUNTIME_DIR")
|
|
if runtimeHome == "" {
|
|
runtimeHome = filepath.Join(os.TempDir(), fmt.Sprintf("banger-runtime-%d", os.Getuid()))
|
|
}
|
|
|
|
layout := Layout{
|
|
ConfigHome: configHome,
|
|
StateHome: stateHome,
|
|
CacheHome: cacheHome,
|
|
RuntimeHome: runtimeHome,
|
|
ConfigDir: filepath.Join(configHome, "banger"),
|
|
StateDir: filepath.Join(stateHome, "banger"),
|
|
CacheDir: filepath.Join(cacheHome, "banger"),
|
|
RuntimeDir: filepath.Join(runtimeHome, "banger"),
|
|
}
|
|
layout.SocketPath = filepath.Join(layout.RuntimeDir, "bangerd.sock")
|
|
layout.DBPath = filepath.Join(layout.StateDir, "state.db")
|
|
layout.DaemonLog = filepath.Join(layout.StateDir, "bangerd.log")
|
|
layout.VMsDir = filepath.Join(layout.StateDir, "vms")
|
|
layout.ImagesDir = filepath.Join(layout.StateDir, "images")
|
|
layout.KernelsDir = filepath.Join(layout.StateDir, "kernels")
|
|
layout.OCICacheDir = filepath.Join(layout.CacheDir, "oci")
|
|
return layout, nil
|
|
}
|
|
|
|
func Ensure(layout Layout) error {
|
|
for _, dir := range []string{layout.ConfigDir, layout.StateDir, layout.CacheDir, layout.RuntimeDir, layout.VMsDir, layout.ImagesDir, layout.KernelsDir, layout.OCICacheDir} {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var executablePath = os.Executable
|
|
|
|
func BangerdPath() (string, error) {
|
|
if env := os.Getenv("BANGER_DAEMON_BIN"); env != "" {
|
|
return env, nil
|
|
}
|
|
exe, err := executablePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := filepath.Dir(exe)
|
|
for _, candidate := range []string{
|
|
filepath.Join(dir, "bangerd"),
|
|
filepath.Join(dir, "bangerd.exe"),
|
|
} {
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
return "", errors.New("bangerd binary not found next to banger; run `make build`")
|
|
}
|
|
|
|
func CompanionBinaryPath(name string) (string, error) {
|
|
envNames := []string{
|
|
"BANGER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(name)) + "_BIN",
|
|
}
|
|
if trimmed, ok := strings.CutPrefix(name, "banger-"); ok {
|
|
envNames = append(envNames, "BANGER_"+strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(trimmed))+"_BIN")
|
|
}
|
|
for _, envName := range envNames {
|
|
if env := strings.TrimSpace(os.Getenv(envName)); env != "" {
|
|
return env, nil
|
|
}
|
|
}
|
|
exe, err := executablePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
exeDir := filepath.Dir(exe)
|
|
for _, candidate := range []string{
|
|
filepath.Join(exeDir, name),
|
|
filepath.Join(exeDir, "..", "lib", "banger", name),
|
|
filepath.Join(exeDir, "..", "libexec", "banger", name),
|
|
} {
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s companion binary not found; run `make build` or reinstall banger", name)
|
|
}
|
|
|
|
func getenvDefault(key, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|