mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* fix(sandbox): classify helper-tool errors with usefulerror Sandbox helper commands (profile lint/diff/show/init/list) used to bubble up plain fmt.Errorf chains from the registry layer, which the TUI then classified as Unknown and decorated with a bug-report link. Wrap each error path at the cmd/sandbox boundary so the TUI prints NotFound, InvalidArgument, or PermissionDenied with actionable hints instead. Closes #269 * refactor(sandbox): classify registry errors via sentinel wrapping Replace the fragile substring match in profileLoadError with errors.Is against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid sentinels. Every fmt.Errorf in registry.go that previously communicated "missing" or "malformed" by message text now wraps the corresponding sentinel, so the cmd layer can classify without inspecting strings. * fix(sandbox): detect IO error class when wrapping helper errors Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with ioErrorCode, which inspects the error chain for fs.ErrPermission and fs.ErrNotExist before falling back. Applied to runProfileList (where an unreadable user profile directory now classifies as PermissionDenied), registryInitError, and the stat/MkdirAll/WriteFile paths in profile init. Also drop redundant doc comments on helpers whose names are self-evident. --------- Co-authored-by: Claude <noreply@anthropic.com>
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/pmezard/go-difflib/difflib"
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
|
"github.com/safedep/pmg/sandbox/platform"
|
|
"github.com/safedep/pmg/usefulerror"
|
|
"github.com/spf13/cobra"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const ExitCodeDiffError = 2
|
|
|
|
type profileDiffOptions struct {
|
|
driver string
|
|
cwd string
|
|
home string
|
|
}
|
|
|
|
func newProfileDiffCommand(factory registryFactory) *cobra.Command {
|
|
opts := &profileDiffOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "diff <a> <b>",
|
|
Short: "Diff two resolved sandbox profiles (or their driver-rendered output)",
|
|
Example: " pmg sandbox profile diff npm-restrictive pypi-restrictive",
|
|
Args: cobra.ExactArgs(2),
|
|
SilenceErrors: false,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := runProfileDiff(cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], args[1], opts, factory)
|
|
if err != nil {
|
|
if _, isDiff := err.(*diffPresentError); isDiff {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
return sandboxErrorExit(cmd, err)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&opts.driver, "driver", "", "Render each policy for a specific driver before diffing: seatbelt|bubblewrap|landlock")
|
|
cmd.Flags().StringVar(&opts.cwd, "cwd", "", "Override ${CWD} during expansion (defaults to current working directory)")
|
|
cmd.Flags().StringVar(&opts.home, "home", "", "Override ${HOME} during expansion (defaults to current user home)")
|
|
return cmd
|
|
}
|
|
|
|
type diffPresentError struct{}
|
|
|
|
func (e *diffPresentError) Error() string { return "" }
|
|
func (e *diffPresentError) ExitCode() int { return 1 }
|
|
|
|
type diffOpError struct{ err error }
|
|
|
|
func (e *diffOpError) Error() string { return e.err.Error() }
|
|
func (e *diffOpError) Unwrap() error { return e.err }
|
|
func (e *diffOpError) ExitCode() int { return ExitCodeDiffError }
|
|
|
|
func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *profileDiffOptions, factory registryFactory) error {
|
|
if err := validateDriver(opts.driver); err != nil {
|
|
return &diffOpError{err: err}
|
|
}
|
|
|
|
registry, err := factory()
|
|
if err != nil {
|
|
return &diffOpError{err: registryInitError(err)}
|
|
}
|
|
|
|
dataA, err := materialize(registry, nameA, opts)
|
|
if err != nil {
|
|
return &diffOpError{err: err}
|
|
}
|
|
dataB, err := materialize(registry, nameB, opts)
|
|
if err != nil {
|
|
return &diffOpError{err: err}
|
|
}
|
|
|
|
if bytes.Equal(dataA, dataB) {
|
|
if _, err := fmt.Fprintln(errOut, "profiles are identical"); err != nil {
|
|
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
|
|
"Failed to write diff output. Check that stderr is writable.")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
diff := difflib.UnifiedDiff{
|
|
A: difflib.SplitLines(string(dataA)),
|
|
B: difflib.SplitLines(string(dataB)),
|
|
FromFile: nameA,
|
|
ToFile: nameB,
|
|
Context: 3,
|
|
}
|
|
text, err := difflib.GetUnifiedDiffString(diff)
|
|
if err != nil {
|
|
return &diffOpError{err: wrapUseful(fmt.Errorf("failed to render diff: %w", err),
|
|
usefulerror.ErrCodeUnknown,
|
|
"Could not render the unified diff. Re-run with --verbose for the underlying cause.")}
|
|
}
|
|
|
|
if _, err := io.WriteString(out, text); err != nil {
|
|
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
|
|
"Failed to write diff output. Check that stdout is writable.")}
|
|
}
|
|
|
|
if !strings.HasSuffix(text, "\n") {
|
|
if _, err := fmt.Fprintln(out); err != nil {
|
|
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
|
|
"Failed to write diff output. Check that stdout is writable.")}
|
|
}
|
|
}
|
|
|
|
return &diffPresentError{}
|
|
}
|
|
|
|
func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profileDiffOptions) ([]byte, error) {
|
|
policy, err := registry.ResolveProfile(name, pmgsandbox.ResolveOptions{
|
|
CWD: opts.cwd,
|
|
Home: opts.home,
|
|
})
|
|
if err != nil {
|
|
return nil, profileLoadError(err)
|
|
}
|
|
|
|
if opts.driver != "" {
|
|
rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy)
|
|
if err != nil {
|
|
return nil, wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
|
|
"Could not render the policy for the requested driver. Verify the driver is supported on this host.")
|
|
}
|
|
return rendered, nil
|
|
}
|
|
|
|
// Strip inherits since both sides are post-resolution.
|
|
policy.Inherits = ""
|
|
|
|
data, err := yaml.Marshal(policy)
|
|
if err != nil {
|
|
return nil, wrapUseful(fmt.Errorf("failed to marshal resolved policy %s: %w", name, err),
|
|
usefulerror.ErrCodeUnknown,
|
|
"Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.")
|
|
}
|
|
return data, nil
|
|
}
|