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 (#272)
* 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>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -62,6 +63,56 @@ func notFoundError(message, help string) error {
|
||||
Wrap(errors.New(message))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if _, ok := usefulerror.AsUsefulError(err); ok {
|
||||
return err
|
||||
}
|
||||
return usefulerror.Useful().
|
||||
WithCode(code).
|
||||
WithHumanError(err.Error()).
|
||||
WithHelp(help).
|
||||
Wrap(err)
|
||||
}
|
||||
|
||||
func profileLoadError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := usefulerror.AsUsefulError(err); ok {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, pmgsandbox.ErrProfileNotFound):
|
||||
return wrapUseful(err, usefulerror.ErrCodeNotFound,
|
||||
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.")
|
||||
case errors.Is(err, pmgsandbox.ErrProfileInvalid):
|
||||
return wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
|
||||
"Check the profile YAML for syntax/schema issues and verify any 'inherits:' parent name.")
|
||||
}
|
||||
return wrapUseful(err, usefulerror.ErrCodeUnknown,
|
||||
"Failed to load the sandbox profile. Run with --verbose for the underlying cause.")
|
||||
}
|
||||
|
||||
func registryInitError(err error) error {
|
||||
return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown),
|
||||
"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):
|
||||
return usefulerror.ErrCodePermissionDenied
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return usefulerror.ErrCodeNotFound
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func writeJSONIndent(out io.Writer, v any) error {
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -68,7 +69,7 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *
|
||||
|
||||
registry, err := factory()
|
||||
if err != nil {
|
||||
return &diffOpError{err: err}
|
||||
return &diffOpError{err: registryInitError(err)}
|
||||
}
|
||||
|
||||
dataA, err := materialize(registry, nameA, opts)
|
||||
@@ -82,7 +83,8 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *
|
||||
|
||||
if bytes.Equal(dataA, dataB) {
|
||||
if _, err := fmt.Fprintln(errOut, "profiles are identical"); err != nil {
|
||||
return &diffOpError{err: err}
|
||||
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
|
||||
"Failed to write diff output. Check that stderr is writable.")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -96,16 +98,20 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *
|
||||
}
|
||||
text, err := difflib.GetUnifiedDiffString(diff)
|
||||
if err != nil {
|
||||
return &diffOpError{err: fmt.Errorf("failed to render diff: %w", err)}
|
||||
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: err}
|
||||
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: err}
|
||||
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
|
||||
"Failed to write diff output. Check that stdout is writable.")}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +124,16 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile
|
||||
Home: opts.home,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, profileLoadError(err)
|
||||
}
|
||||
|
||||
if opts.driver != "" {
|
||||
return platform.Render(pmgsandbox.DriverName(opts.driver), policy)
|
||||
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.
|
||||
@@ -130,7 +141,9 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile
|
||||
|
||||
data, err := yaml.Marshal(policy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal resolved policy %s: %w", name, err)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -96,6 +97,9 @@ func TestProfileDiffUnknownProfile(t *testing.T) {
|
||||
assert.Empty(t, stdout)
|
||||
assert.Empty(t, stderr)
|
||||
assert.Contains(t, err.Error(), "no-such-profile-xyz")
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok, "diff error should expose a UsefulError through Unwrap")
|
||||
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
|
||||
}
|
||||
|
||||
func TestProfileDiffUnknownDriver(t *testing.T) {
|
||||
@@ -107,6 +111,9 @@ func TestProfileDiffUnknownDriver(t *testing.T) {
|
||||
assert.Empty(t, stdout)
|
||||
assert.Empty(t, stderr)
|
||||
assert.Contains(t, err.Error(), "unknown driver")
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok, "diff error should expose a UsefulError through Unwrap")
|
||||
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
|
||||
}
|
||||
|
||||
func TestProfileDiffMissingProfileShowsUsage(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -59,7 +60,7 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor
|
||||
|
||||
registry, err := factory()
|
||||
if err != nil {
|
||||
return err
|
||||
return registryInitError(err)
|
||||
}
|
||||
|
||||
userDir := registry.UserProfileDir()
|
||||
@@ -93,17 +94,23 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor
|
||||
"Choose a different profile name, or edit the existing profile file to merge changes.",
|
||||
)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to stat %s: %w", target, err)
|
||||
return wrapUseful(fmt.Errorf("failed to stat %s: %w", target, err),
|
||||
ioErrorCode(err, usefulerror.ErrCodeUnknown),
|
||||
"Could not stat the target profile path. Check the user profile directory permissions.")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(userDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create user profile directory %s: %w", userDir, err)
|
||||
return wrapUseful(fmt.Errorf("failed to create user profile directory %s: %w", userDir, err),
|
||||
ioErrorCode(err, usefulerror.ErrCodePermissionDenied),
|
||||
"Could not create the user profile directory. Check filesystem permissions for "+userDir+".")
|
||||
}
|
||||
|
||||
content := renderScaffold(name, opts.description, opts.from, pms, placeholderPM)
|
||||
|
||||
if err := os.WriteFile(target, []byte(content), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", target, err)
|
||||
return wrapUseful(fmt.Errorf("failed to write %s: %w", target, err),
|
||||
ioErrorCode(err, usefulerror.ErrCodePermissionDenied),
|
||||
"Could not write the scaffolded profile. Check filesystem permissions for "+target+".")
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(out, target)
|
||||
|
||||
@@ -56,12 +56,12 @@ func (e *lintFailError) ExitCode() int { return ExitCodeLintFail }
|
||||
func runProfileLint(out io.Writer, name string, opts *profileLintOptions, factory registryFactory) error {
|
||||
registry, err := factory()
|
||||
if err != nil {
|
||||
return err
|
||||
return registryInitError(err)
|
||||
}
|
||||
|
||||
policy, resolvedName, err := resolveProfileForLint(name, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
return profileLoadError(err)
|
||||
}
|
||||
|
||||
issues := pmgsandbox.LintProfile(policy)
|
||||
@@ -89,7 +89,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p
|
||||
if _, ok := registry.BuiltinProfileYAML(name); ok {
|
||||
policy, err := registry.GetProfile(name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", profileLoadError(err)
|
||||
}
|
||||
return policy, name, nil
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p
|
||||
if s.Source == pmgsandbox.ProfileSourceUser && s.Name == name {
|
||||
policy, loadErr := registry.LoadCustomProfile(s.Path)
|
||||
if loadErr != nil {
|
||||
return nil, "", loadErr
|
||||
return nil, "", profileLoadError(loadErr)
|
||||
}
|
||||
return policy, s.Path, nil
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p
|
||||
if _, statErr := os.Stat(name); statErr == nil {
|
||||
policy, loadErr := registry.LoadCustomProfile(name)
|
||||
if loadErr != nil {
|
||||
return nil, "", loadErr
|
||||
return nil, "", profileLoadError(loadErr)
|
||||
}
|
||||
return policy, name, nil
|
||||
}
|
||||
|
||||
@@ -196,6 +196,24 @@ func TestProfileLintMissingTargetShowsUsage(t *testing.T) {
|
||||
assert.Contains(t, stdout.String(), "pmg sandbox profile lint npm-restrictive")
|
||||
}
|
||||
|
||||
func TestProfileLint_InvalidYAMLReturnsInvalidArgument(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "broken.yml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("name: broken\npackage_managers:\n - npm\n invalid: : :\n"), 0o644))
|
||||
|
||||
cmd := newProfileLintCommand(newTestRegistry(t, dir))
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs([]string{"broken"})
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
|
||||
}
|
||||
|
||||
func TestProfileLint_LiteralPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeUserProfileLint(t, dir, "literal", `name: literal
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
pmgsandbox "github.com/safedep/pmg/sandbox"
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -39,12 +40,13 @@ func newProfileListCommand(factory registryFactory) *cobra.Command {
|
||||
func runProfileList(out io.Writer, opts *profileListOptions, factory registryFactory) error {
|
||||
registry, err := factory()
|
||||
if err != nil {
|
||||
return err
|
||||
return registryInitError(err)
|
||||
}
|
||||
|
||||
summaries, err := registry.ListProfiles()
|
||||
if err != nil {
|
||||
return err
|
||||
return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown),
|
||||
"Failed to enumerate sandbox profiles. Check the user profile directory permissions.")
|
||||
}
|
||||
|
||||
if opts.jsonOut {
|
||||
|
||||
@@ -3,6 +3,9 @@ package sandbox
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -11,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pmgsandbox "github.com/safedep/pmg/sandbox"
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
)
|
||||
|
||||
func newTestRegistry(t *testing.T, userDir string) registryFactory {
|
||||
@@ -100,6 +104,43 @@ func TestProfileListShadowedTag(t *testing.T) {
|
||||
assert.Contains(t, stdout.String(), "SHADOWED")
|
||||
}
|
||||
|
||||
func TestProfileListRegistryFailureReturnsUseful(t *testing.T) {
|
||||
factory := func() (pmgsandbox.ProfileRegistry, error) {
|
||||
return nil, errors.New("boom")
|
||||
}
|
||||
|
||||
cmd := newProfileListCommand(factory)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, usefulerror.ErrCodeUnknown, usefulErr.Code())
|
||||
assert.Contains(t, err.Error(), "boom")
|
||||
}
|
||||
|
||||
func TestProfileListRegistryPermissionErrorReturnsPermissionDenied(t *testing.T) {
|
||||
factory := func() (pmgsandbox.ProfileRegistry, error) {
|
||||
return nil, fmt.Errorf("read dir: %w", fs.ErrPermission)
|
||||
}
|
||||
|
||||
cmd := newProfileListCommand(factory)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, usefulerror.ErrCodePermissionDenied, usefulErr.Code())
|
||||
}
|
||||
|
||||
func TestProfileListRejectsUnexpectedArgs(t *testing.T) {
|
||||
cmd := newProfileListCommand(newTestRegistry(t, ""))
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/safedep/dry/log"
|
||||
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"
|
||||
)
|
||||
@@ -53,7 +54,7 @@ func runProfileShow(out io.Writer, name string, opts *profileShowOptions, factor
|
||||
|
||||
registry, err := factory()
|
||||
if err != nil {
|
||||
return err
|
||||
return registryInitError(err)
|
||||
}
|
||||
|
||||
if opts.driver != "" {
|
||||
@@ -70,7 +71,7 @@ func runProfileShow(out io.Writer, name string, opts *profileShowOptions, factor
|
||||
func runProfileShowRaw(out io.Writer, name string, opts *profileShowOptions, registry pmgsandbox.ProfileRegistry) error {
|
||||
source, path, data, err := loadProfileSource(name, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
return profileLoadError(err)
|
||||
}
|
||||
|
||||
if opts.jsonOut {
|
||||
@@ -93,7 +94,7 @@ func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions
|
||||
Home: opts.home,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return profileLoadError(err)
|
||||
}
|
||||
|
||||
if opts.jsonOut {
|
||||
@@ -106,7 +107,9 @@ func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions
|
||||
|
||||
data, err := yaml.Marshal(policy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal resolved policy: %w", err)
|
||||
return wrapUseful(fmt.Errorf("failed to marshal resolved policy: %w", err),
|
||||
usefulerror.ErrCodeUnknown,
|
||||
"Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.")
|
||||
}
|
||||
|
||||
_, err = out.Write(data)
|
||||
@@ -119,12 +122,13 @@ func runProfileShowDriver(out io.Writer, name string, opts *profileShowOptions,
|
||||
Home: opts.home,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return profileLoadError(err)
|
||||
}
|
||||
|
||||
rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy)
|
||||
if err != nil {
|
||||
return err
|
||||
return wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
|
||||
"Could not render the policy for the requested driver. Verify the driver is supported on this host.")
|
||||
}
|
||||
|
||||
if opts.jsonOut {
|
||||
|
||||
@@ -148,4 +148,7 @@ func TestProfileShowDriverNonNativeErrors(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, stderr.String())
|
||||
assert.Contains(t, err.Error(), "not available")
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok, "non-native driver error should be reported as a UsefulError")
|
||||
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package sandbox
|
||||
|
||||
import "errors"
|
||||
|
||||
// Wrap these with %w so cmd-layer code can classify via errors.Is
|
||||
// instead of inspecting message text.
|
||||
var (
|
||||
ErrProfileNotFound = errors.New("sandbox profile not found")
|
||||
ErrProfileInvalid = errors.New("sandbox profile invalid")
|
||||
)
|
||||
+11
-11
@@ -66,12 +66,12 @@ func (r *defaultProfileRegistry) loadBuiltinProfiles() error {
|
||||
|
||||
policy, err := parsePolicy(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse profile %s: %w", entry.Name(), err)
|
||||
return fmt.Errorf("failed to parse profile %s: %w: %w", entry.Name(), ErrProfileInvalid, err)
|
||||
}
|
||||
|
||||
// Basic validation (without inheritance resolution)
|
||||
if err := policy.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid profile %s: %w", entry.Name(), err)
|
||||
return fmt.Errorf("invalid profile %s: %w: %w", entry.Name(), ErrProfileInvalid, err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
@@ -93,7 +93,7 @@ func (r *defaultProfileRegistry) loadBuiltinProfiles() error {
|
||||
|
||||
// Validate after inheritance resolution
|
||||
if err := policy.ValidateResolved(); err != nil {
|
||||
return fmt.Errorf("invalid profile %s after inheritance: %w", name, err)
|
||||
return fmt.Errorf("invalid profile %s after inheritance: %w: %w", name, ErrProfileInvalid, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,12 +112,12 @@ func (r *defaultProfileRegistry) resolveInheritance(child *SandboxPolicy) error
|
||||
// Look up parent profile (must be a built-in profile)
|
||||
parent, exists := r.profiles[child.Inherits]
|
||||
if !exists {
|
||||
return fmt.Errorf("parent profile '%s' not found (only built-in profiles can be inherited)", child.Inherits)
|
||||
return fmt.Errorf("%w: parent profile '%s' (only built-in profiles can be inherited)", ErrProfileNotFound, child.Inherits)
|
||||
}
|
||||
|
||||
// Prevent inheritance chains (parent must not itself inherit)
|
||||
if parent.Inherits != "" {
|
||||
return fmt.Errorf("inheritance chains not allowed: parent profile '%s' inherits from '%s'", parent.Name, parent.Inherits)
|
||||
return fmt.Errorf("%w: inheritance chains not allowed: parent profile '%s' inherits from '%s'", ErrProfileInvalid, parent.Name, parent.Inherits)
|
||||
}
|
||||
|
||||
// Merge parent into child
|
||||
@@ -153,7 +153,7 @@ func (r *defaultProfileRegistry) GetProfile(name string) (*SandboxPolicy, error)
|
||||
return r.LoadCustomProfile(name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("sandbox profile not found: %s (not a built-in profile, no matching user profile, and file does not exist)", name)
|
||||
return nil, fmt.Errorf("%w: %s (not a built-in profile, no matching user profile, and file does not exist)", ErrProfileNotFound, name)
|
||||
}
|
||||
|
||||
// findUserProfileByName looks for `<name>.yml` then `<name>.yaml` under
|
||||
@@ -221,12 +221,12 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy,
|
||||
|
||||
policy, err := parsePolicy(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse custom profile %s: %w", path, err)
|
||||
return nil, fmt.Errorf("failed to parse custom profile %s: %w: %w", path, ErrProfileInvalid, err)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if err := policy.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid custom profile %s: %w", path, err)
|
||||
return nil, fmt.Errorf("invalid custom profile %s: %w: %w", path, ErrProfileInvalid, err)
|
||||
}
|
||||
|
||||
// Resolve inheritance if present
|
||||
@@ -236,12 +236,12 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy,
|
||||
r.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("custom profile %s inherits from unknown profile '%s' (only built-in profiles can be inherited)", path, policy.Inherits)
|
||||
return nil, fmt.Errorf("%w: custom profile %s inherits from unknown profile '%s' (only built-in profiles can be inherited)", ErrProfileNotFound, path, policy.Inherits)
|
||||
}
|
||||
|
||||
// Prevent inheritance chains
|
||||
if parent.Inherits != "" {
|
||||
return nil, fmt.Errorf("custom profile %s: parent profile '%s' inherits from '%s' (chains not allowed)", path, parent.Name, parent.Inherits)
|
||||
return nil, fmt.Errorf("%w: custom profile %s parent '%s' inherits from '%s' (chains not allowed)", ErrProfileInvalid, path, parent.Name, parent.Inherits)
|
||||
}
|
||||
|
||||
// Merge parent into child
|
||||
@@ -251,7 +251,7 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy,
|
||||
|
||||
// Validate after inheritance resolution
|
||||
if err := policy.ValidateResolved(); err != nil {
|
||||
return nil, fmt.Errorf("invalid custom profile %s after inheritance: %w", path, err)
|
||||
return nil, fmt.Errorf("invalid custom profile %s after inheritance: %w: %w", path, ErrProfileInvalid, err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -24,6 +25,33 @@ func TestNewDefaultProfileRegistry(t *testing.T) {
|
||||
assert.NotNil(t, pypiRestrictive)
|
||||
}
|
||||
|
||||
func TestGetProfileUnknownWrapsErrProfileNotFound(t *testing.T) {
|
||||
registry, err := newDefaultProfileRegistry()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = registry.GetProfile("definitely-not-a-real-profile-name")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrProfileNotFound),
|
||||
"unknown profile should wrap ErrProfileNotFound, got %T: %v", err, err)
|
||||
}
|
||||
|
||||
func TestLoadCustomProfileInvalidYAMLWrapsErrProfileInvalid(t *testing.T) {
|
||||
registry, err := newDefaultProfileRegistry()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "bad-policy-*.yml")
|
||||
assert.NoError(t, err)
|
||||
defer func() { _ = tempFile.Close() }()
|
||||
|
||||
_, err = tempFile.WriteString("name: bad\npackage_managers:\n - npm\n invalid: : :\n")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = registry.LoadCustomProfile(tempFile.Name())
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrProfileInvalid),
|
||||
"YAML parse failure should wrap ErrProfileInvalid, got %T: %v", err, err)
|
||||
}
|
||||
|
||||
func TestLoadCustomProfile(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -188,6 +216,8 @@ func TestLoadCustomProfileWithInheritance(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, policy)
|
||||
assert.Contains(t, err.Error(), "inherits from unknown profile")
|
||||
assert.True(t, errors.Is(err, ErrProfileNotFound),
|
||||
"missing parent profile should wrap ErrProfileNotFound")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user