2026-05-19 14:40:54 +05:30
|
|
|
package sandbox
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-05-21 19:34:29 +05:30
|
|
|
"io/fs"
|
2026-05-19 14:40:54 +05:30
|
|
|
|
2026-05-24 12:22:19 +05:30
|
|
|
"github.com/safedep/dry/usefulerror"
|
|
|
|
|
"github.com/safedep/pmg/errcodes"
|
2026-05-24 12:46:06 +05:30
|
|
|
"github.com/safedep/pmg/internal/ui"
|
|
|
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
2026-05-19 14:40:54 +05:30
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const ExitCodeProbeFailure = 2
|
|
|
|
|
|
|
|
|
|
var sandboxErrorExit = func(_ *cobra.Command, err error) error {
|
|
|
|
|
type exitCoder interface{ ExitCode() int }
|
|
|
|
|
if ec, ok := err.(exitCoder); ok {
|
|
|
|
|
ui.ErrorExitWithCode(err, ec.ExitCode())
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ui.ErrorExit(err)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var validDrivers = map[pmgsandbox.DriverName]struct{}{
|
|
|
|
|
pmgsandbox.DriverSeatbelt: {},
|
|
|
|
|
pmgsandbox.DriverBubblewrap: {},
|
|
|
|
|
pmgsandbox.DriverLandlock: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateDriver(name string) error {
|
|
|
|
|
if name == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if _, ok := validDrivers[pmgsandbox.DriverName(name)]; !ok {
|
|
|
|
|
return invalidArgumentError(
|
|
|
|
|
fmt.Sprintf("unknown driver %q", name),
|
|
|
|
|
"Use one of: seatbelt, bubblewrap, landlock",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func invalidArgumentError(message, help string) error {
|
2026-05-24 12:22:19 +05:30
|
|
|
return usefulerror.NewUsefulError().
|
|
|
|
|
WithCode(errcodes.InvalidArgument).
|
2026-05-19 14:40:54 +05:30
|
|
|
WithHumanError(message).
|
|
|
|
|
WithHelp(help).
|
|
|
|
|
Wrap(errors.New(message))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func notFoundError(message, help string) error {
|
2026-05-24 12:22:19 +05:30
|
|
|
return usefulerror.NewUsefulError().
|
|
|
|
|
WithCode(errcodes.NotFound).
|
2026-05-19 14:40:54 +05:30
|
|
|
WithHumanError(message).
|
|
|
|
|
WithHelp(help).
|
|
|
|
|
Wrap(errors.New(message))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 19:34:29 +05:30
|
|
|
// Idempotent: returns err unchanged when nil or already useful, so call
|
|
|
|
|
// sites can apply it without losing more precise pre-classified errors.
|
|
|
|
|
func wrapUseful(err error, code, help string) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-24 12:46:06 +05:30
|
|
|
if hasUsefulError(err) {
|
2026-05-21 19:34:29 +05:30
|
|
|
return err
|
|
|
|
|
}
|
2026-05-24 12:22:19 +05:30
|
|
|
return usefulerror.NewUsefulError().
|
2026-05-21 19:34:29 +05:30
|
|
|
WithCode(code).
|
|
|
|
|
WithHumanError(err.Error()).
|
|
|
|
|
WithHelp(help).
|
|
|
|
|
Wrap(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func profileLoadError(err error) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-24 12:46:06 +05:30
|
|
|
if hasUsefulError(err) {
|
2026-05-21 19:34:29 +05:30
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, pmgsandbox.ErrProfileNotFound):
|
2026-05-24 12:22:19 +05:30
|
|
|
return wrapUseful(err, errcodes.NotFound,
|
2026-05-21 19:34:29 +05:30
|
|
|
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.")
|
|
|
|
|
case errors.Is(err, pmgsandbox.ErrProfileInvalid):
|
2026-05-24 12:22:19 +05:30
|
|
|
return wrapUseful(err, errcodes.InvalidArgument,
|
2026-05-21 19:34:29 +05:30
|
|
|
"Check the profile YAML for syntax/schema issues and verify any 'inherits:' parent name.")
|
|
|
|
|
}
|
2026-05-24 12:22:19 +05:30
|
|
|
return wrapUseful(err, errcodes.Unknown,
|
2026-05-21 19:34:29 +05:30
|
|
|
"Failed to load the sandbox profile. Run with --verbose for the underlying cause.")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:46:06 +05:30
|
|
|
func hasUsefulError(err error) bool {
|
|
|
|
|
// AsUsefulError also runs global converters for plain errors like
|
|
|
|
|
// fs.ErrPermission. Contextual wrappers must only skip errors that already
|
|
|
|
|
// carry UsefulError details, otherwise generic converters hide command help.
|
|
|
|
|
var usefulErr usefulerror.UsefulError
|
|
|
|
|
return errors.As(err, &usefulErr)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 19:34:29 +05:30
|
|
|
func registryInitError(err error) error {
|
2026-05-24 12:22:19 +05:30
|
|
|
return wrapUseful(err, ioErrorCode(err, errcodes.Unknown),
|
2026-05-21 19:34:29 +05:30
|
|
|
"Failed to initialise the sandbox profile registry. Run with --verbose for details.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ioErrorCode(err error, fallback string) string {
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, fs.ErrPermission):
|
2026-05-24 12:22:19 +05:30
|
|
|
return errcodes.PermissionDenied
|
2026-05-21 19:34:29 +05:30
|
|
|
case errors.Is(err, fs.ErrNotExist):
|
2026-05-24 12:22:19 +05:30
|
|
|
return errcodes.NotFound
|
2026-05-21 19:34:29 +05:30
|
|
|
}
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 14:40:54 +05:30
|
|
|
func writeJSONIndent(out io.Writer, v any) error {
|
|
|
|
|
enc := json.NewEncoder(out)
|
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
|
return enc.Encode(v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func renderTable(out io.Writer, rows [][]string, after func(rowIdx int) error) error {
|
2026-05-26 12:21:39 +05:30
|
|
|
return ui.RenderTable(out, rows, after)
|
2026-05-19 14:40:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstColumnIndent(rows [][]string) string {
|
2026-05-26 12:21:39 +05:30
|
|
|
return ui.FirstColumnIndent(rows)
|
2026-05-19 14:40:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func truncate(s string, n int) string {
|
2026-05-26 12:21:39 +05:30
|
|
|
return ui.Truncate(s, n)
|
2026-05-19 14:40:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func truncateLeft(s string, n int) string {
|
2026-05-26 12:21:39 +05:30
|
|
|
return ui.TruncateLeft(s, n)
|
2026-05-19 14:40:54 +05:30
|
|
|
}
|