A decoded string owns its bytes

zap reads text zero-copy over the frame that arrived, and Call releases that
frame to the pool when it returns, so a reply's strings pointed into memory the
next call overwrites. The generated codecs already copy text for this reason;
the reflective decoder now does the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-24 10:32:35 -07:00
co-authored by Claude Opus 5.5
parent 1cff0a19e9
commit 8bf54e1f98
2 changed files with 39 additions and 1 deletions
+34
View File
@@ -0,0 +1,34 @@
package zapenc_test
import (
"testing"
"github.com/zap-proto/zip/internal/zapenc"
)
// A decoded value owns its strings. The transport hands Unmarshal a pooled frame
// and reuses it for the next call, so a string that still pointed into the frame
// would change under whoever kept it.
func TestDecodedTextOutlivesTheFrame(t *testing.T) {
type site struct {
Bucket string
Prefix string
Opt *string
Inner inner
}
s := "gone"
frame, err := zapenc.Marshal(&site{Bucket: "hanzo-sites", Prefix: "hanzo/bootnode", Opt: &s, Inner: inner{Slug: "bootnode"}})
if err != nil {
t.Fatal(err)
}
var got site
if err := zapenc.Unmarshal(frame, &got); err != nil {
t.Fatal(err)
}
for i := range frame {
frame[i] = 'x'
}
if got.Bucket != "hanzo-sites" || got.Prefix != "hanzo/bootnode" || got.Opt == nil || *got.Opt != "gone" || got.Inner.Slug != "bootnode" {
t.Fatalf("decoded strings follow the frame: %+v opt=%v", got, got.Opt)
}
}
+5 -1
View File
@@ -52,6 +52,7 @@ import (
"math"
"reflect"
"strconv"
"strings"
"sync"
zap "github.com/zap-proto/go"
@@ -593,7 +594,10 @@ func readField(o zap.Object, f field, target reflect.Value) error {
case kFloat64:
target.SetFloat(o.Float64(f.offset))
case kText:
target.SetString(o.Text(f.offset))
// zap reads text zero-copy, over the frame that arrived, and the caller's
// transport reuses that frame once the call returns. The decoded value
// outlives the call, so it is copied here, as the generated codecs copy.
target.SetString(strings.Clone(o.Text(f.offset)))
case kBytes:
if b := o.Bytes(f.offset); len(b) > 0 {
target.SetBytes(append([]byte(nil), b...))