From f3b008d1b8d367ab5f87164916901cc9d29b0c41 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Tue, 23 Dec 2025 12:35:06 +0100 Subject: [PATCH] plugin: fix returning in-protocol errors from plugins Fixes Foxboron/age-plugin-tpm#31 --- plugin/client_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++ plugin/plugin.go | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/plugin/client_test.go b/plugin/client_test.go index 35b0eca..2a82fc3 100644 --- a/plugin/client_test.go +++ b/plugin/client_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "testing" "filippo.io/age" @@ -34,6 +35,15 @@ func TestMain(m *testing.M) { return testPQCRecipient{}, nil }) os.Exit(p.Main()) + case "age-plugin-error": + p, _ := New("error") + p.HandleRecipient(func(data []byte) (age.Recipient, error) { + return nil, errors.New("oh my, an error occurred") + }) + p.HandleIdentity(func(data []byte) (age.Identity, error) { + return nil, errors.New("oh my, an error occurred") + }) + os.Exit(p.Main()) default: os.Exit(m.Run()) } @@ -163,3 +173,55 @@ func TestNotFound(t *testing.T) { t.Errorf("expected error to wrap exec.ErrNotFound, got: %v", err) } } + +func TestPluginError(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows support is TODO") + } + temp := t.TempDir() + testOnlyPluginPath = temp + t.Cleanup(func() { testOnlyPluginPath = "" }) + ex, err := os.Executable() + if err != nil { + t.Fatal(err) + } + if err := os.Link(ex, filepath.Join(temp, "age-plugin-error")); err != nil { + t.Fatal(err) + } + if err := os.Chmod(filepath.Join(temp, "age-plugin-error"), 0755); err != nil { + t.Fatal(err) + } + + r := EncodeRecipient("error", nil) + testPluginRecipient, err := NewRecipient(r, &ClientUI{}) + if err != nil { + t.Fatal(err) + } + if _, err := age.Encrypt(io.Discard, testPluginRecipient); err == nil { + t.Errorf("expected error from plugin") + } else if !strings.Contains(err.Error(), "oh my, an error occurred") { + t.Errorf("expected plugin error, got: %v", err) + } + + buf := &bytes.Buffer{} + id, err := age.GenerateHybridIdentity() + if err != nil { + t.Fatal(err) + } + w, err := age.Encrypt(buf, id.Recipient()) + if err != nil { + t.Fatal(err) + } + w.Close() + + i := EncodeIdentity("error", nil) + testPluginIdentity, err := NewIdentity(i, &ClientUI{}) + if err != nil { + t.Fatal(err) + } + if _, err := age.Decrypt(buf, testPluginIdentity); err == nil { + t.Errorf("expected error from plugin") + } else if !strings.Contains(err.Error(), "oh my, an error occurred") { + t.Errorf("expected plugin error, got: %v", err) + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 9ec3a45..d0ca76c 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -654,7 +654,7 @@ func expectUnsupported(sr *format.StanzaReader) error { func (p *Plugin) writeError(args []string, err error) error { s := &format.Stanza{Type: "error", Args: args} s.Body = []byte(err.Error()) - if err := s.Marshal(p.stderr); err != nil { + if err := s.Marshal(p.stdout); err != nil { return fmt.Errorf("failed to write error stanza: %v", err) } if err := expectOk(p.sr); err != nil {