From eed3ea96df712ea69b182d407be90d0cd05bf12b Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 15 Apr 2026 15:27:59 +0200 Subject: [PATCH 1/2] Add noduplicates directive This adds `//msgp:noduplicates [TypeA TypeB ...]` to generator files. This will actively guards against duplicate keys when decoding/unmarshaling structs and maps. Duplicate entries should never be generated as output and generally is no issue unless there is some secondary checks that are inspecting streams (last entry on stream wins). Shimmed keys - late check after shim, before assignment. Catches both identical wire keys AND different wire keys that resolve to the same map key after shimming (e.g. "Foo" vs "foo" with a lowercasing shim). AI Assisted (mostly tests) --- _generated/noduplicates.go | 97 +++ _generated/noduplicates_binkey.go | 22 + _generated/noduplicates_shim.go | 26 + _generated/noduplicates_specific.go | 24 + _generated/noduplicates_test.go | 1188 +++++++++++++++++++++++++++ gen/decode.go | 42 +- gen/spec.go | 59 +- gen/unmarshal.go | 42 +- msgp/errors.go | 9 + parse/directives.go | 19 + parse/getast.go | 12 +- 11 files changed, 1529 insertions(+), 11 deletions(-) create mode 100644 _generated/noduplicates.go create mode 100644 _generated/noduplicates_binkey.go create mode 100644 _generated/noduplicates_shim.go create mode 100644 _generated/noduplicates_specific.go create mode 100644 _generated/noduplicates_test.go diff --git a/_generated/noduplicates.go b/_generated/noduplicates.go new file mode 100644 index 00000000..b5a4ca2d --- /dev/null +++ b/_generated/noduplicates.go @@ -0,0 +1,97 @@ +//msgp:noduplicates + +package _generated + +//go:generate msgp + +// NoDupSimple is a basic struct with a few fields. +type NoDupSimple struct { + Foo string `msg:"Foo"` + Bar int `msg:"Bar"` + Baz bool `msg:"Baz"` +} + +// NoDupMap is a standalone map type. +type NoDupMap map[string]string + +// NoDupNested has a struct with a map field. +type NoDupNested struct { + Name string `msg:"Name"` + Items map[string]int `msg:"Items"` + Tags map[string]string `msg:"Tags"` +} + +// NoDupManyFields exercises the bitmask across byte boundaries (>8 fields). +type NoDupManyFields struct { + F01 string `msg:"f01"` + F02 string `msg:"f02"` + F03 string `msg:"f03"` + F04 string `msg:"f04"` + F05 string `msg:"f05"` + F06 string `msg:"f06"` + F07 string `msg:"f07"` + F08 string `msg:"f08"` + F09 string `msg:"f09"` + F10 string `msg:"f10"` +} + +// NoDupMapInt is a map with int values. +type NoDupMapInt map[string]int + +// NoDupRichTypes covers pointer, slice, bytes, float, and nested struct fields. +type NoDupRichTypes struct { + Str string `msg:"str"` + Num float64 `msg:"num"` + Data []byte `msg:"data"` + PtrStr *string `msg:"ptr_str"` + Strings []string `msg:"strings"` + Inner NoDupInner `msg:"inner"` + PtrInner *NoDupInner `msg:"ptr_inner"` + MapSlice map[string][]int `msg:"map_slice"` +} + +// NoDupInner is used as a nested struct field. +type NoDupInner struct { + X int `msg:"x"` + Y string `msg:"y"` +} + +// NoDupOmitempty has omitempty fields mixed with regular fields. +type NoDupOmitempty struct { + Required string `msg:"required"` + Optional string `msg:"optional,omitempty"` + Flag bool `msg:"flag,omitempty"` + Count int `msg:"count"` +} + +// NoDupAllownil has allownil fields. +type NoDupAllownil struct { + Name string `msg:"name"` + Vals []byte `msg:"vals,allownil"` + Items []int `msg:"items,allownil"` + Mapping map[string]string `msg:"mapping,allownil"` +} + +// NoDupMapComplex is a map with complex values. +type NoDupMapComplex map[string]NoDupInner + +// NoDup17Fields tests 17 fields (3 bytes in bitmask). +type NoDup17Fields struct { + A01 int `msg:"a01"` + A02 int `msg:"a02"` + A03 int `msg:"a03"` + A04 int `msg:"a04"` + A05 int `msg:"a05"` + A06 int `msg:"a06"` + A07 int `msg:"a07"` + A08 int `msg:"a08"` + A09 int `msg:"a09"` + A10 int `msg:"a10"` + A11 int `msg:"a11"` + A12 int `msg:"a12"` + A13 int `msg:"a13"` + A14 int `msg:"a14"` + A15 int `msg:"a15"` + A16 int `msg:"a16"` + A17 int `msg:"a17"` +} diff --git a/_generated/noduplicates_binkey.go b/_generated/noduplicates_binkey.go new file mode 100644 index 00000000..fbca67c8 --- /dev/null +++ b/_generated/noduplicates_binkey.go @@ -0,0 +1,22 @@ +package _generated + +//go:generate msgp -unexported -v + +//msgp:maps binkeys +//msgp:noduplicates + +// NoDupBinKeyInt is a map with int keys (binary-encoded). +type NoDupBinKeyInt map[int]string + +// NoDupBinKeyUint32 is a map with uint32 keys (binary-encoded). +type NoDupBinKeyUint32 map[uint32]int + +// NoDupBinKeyStruct has both string-keyed and binary-keyed maps. +type NoDupBinKeyStruct struct { + Name string `msg:"name"` + IntMap map[int]string `msg:"int_map"` + StrMap map[string]int `msg:"str_map"` +} + +// NoDupBinKeyArray is a map with array keys. +type NoDupBinKeyArray map[[2]byte]int diff --git a/_generated/noduplicates_shim.go b/_generated/noduplicates_shim.go new file mode 100644 index 00000000..c04c894c --- /dev/null +++ b/_generated/noduplicates_shim.go @@ -0,0 +1,26 @@ +package _generated + +import "strings" + +//go:generate msgp -v + +//msgp:maps shim +//msgp:noduplicates + +//msgp:shim NoDupShimKey as:string using:noDupShimEnc/noDupShimDec + +// NoDupShimKey is a key type that normalizes to lowercase. +type NoDupShimKey string + +func noDupShimEnc(k NoDupShimKey) string { return strings.ToLower(string(k)) } +func noDupShimDec(s string) NoDupShimKey { return NoDupShimKey(strings.ToLower(s)) } + +// NoDupShimMap is a map with shimmed keys that normalize to lowercase. +// Two different wire strings (e.g. "Foo" and "foo") shim to the same key. +type NoDupShimMap map[NoDupShimKey]int + +// NoDupShimStruct has a shimmed-key map as a field. +type NoDupShimStruct struct { + Name string `msg:"name"` + Data map[NoDupShimKey]string `msg:"data"` +} diff --git a/_generated/noduplicates_specific.go b/_generated/noduplicates_specific.go new file mode 100644 index 00000000..8da043d6 --- /dev/null +++ b/_generated/noduplicates_specific.go @@ -0,0 +1,24 @@ +package _generated + +//go:generate msgp + +//msgp:noduplicates NoDupTargeted NoDupTargetedMap + +// NoDupTargeted has the directive applied - should reject duplicates. +type NoDupTargeted struct { + Alpha string `msg:"alpha"` + Beta int `msg:"beta"` + Gamma bool `msg:"gamma"` +} + +// NoDupTargetedMap has the directive applied as a map type. +type NoDupTargetedMap map[string]int + +// NoDupUntargeted does NOT have the directive - duplicates should be silently accepted. +type NoDupUntargeted struct { + Alpha string `msg:"alpha"` + Beta int `msg:"beta"` +} + +// NoDupUntargetedMap does NOT have the directive - duplicates silently accepted. +type NoDupUntargetedMap map[string]string diff --git a/_generated/noduplicates_test.go b/_generated/noduplicates_test.go new file mode 100644 index 00000000..919fb5d3 --- /dev/null +++ b/_generated/noduplicates_test.go @@ -0,0 +1,1188 @@ +package _generated + +import ( + "bytes" + "errors" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +// --- helpers --- + +func mustUnmarshal(t *testing.T, buf []byte, v msgp.Unmarshaler) { + t.Helper() + _, err := v.UnmarshalMsg(buf) + if err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } +} + +func mustDecode(t *testing.T, buf []byte, v msgp.Decodable) { + t.Helper() + reader := msgp.NewReader(bytes.NewReader(buf)) + err := v.DecodeMsg(reader) + if err != nil { + t.Fatalf("unexpected decode error: %v", err) + } +} + +func expectDup(t *testing.T, err error) { + t.Helper() + if !errors.Is(err, msgp.ErrDuplicateEntry) { + t.Fatalf("expected ErrDuplicateEntry, got %v", err) + } +} + +func expectNoDup(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ---------- NoDupSimple basic struct ---------- + +func TestNoDupSimple_RoundTrip(t *testing.T) { + orig := NoDupSimple{Foo: "hello", Bar: 42, Baz: true} + + t.Run("MarshalUnmarshal", func(t *testing.T) { + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupSimple + mustUnmarshal(t, buf, &out) + if out != orig { + t.Fatalf("mismatch: got %+v, want %+v", out, orig) + } + }) + t.Run("EncodeDecode", func(t *testing.T) { + var buf bytes.Buffer + w := msgp.NewWriter(&buf) + expectNoDup(t, orig.EncodeMsg(w)) + w.Flush() + var out NoDupSimple + mustDecode(t, buf.Bytes(), &out) + if out != orig { + t.Fatalf("mismatch: got %+v, want %+v", out, orig) + } + }) +} + +func TestNoDupSimple_DuplicateField(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "Bar") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "Foo") // duplicate + buf = msgp.AppendString(buf, "second") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupSimple + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupSimple + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupSimple_UnknownFieldsOK(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "hello") + buf = msgp.AppendString(buf, "Unknown1") + buf = msgp.AppendString(buf, "skip1") + buf = msgp.AppendString(buf, "Unknown2") + buf = msgp.AppendString(buf, "skip2") + + var out NoDupSimple + mustUnmarshal(t, buf, &out) + if out.Foo != "hello" { + t.Fatalf("expected Foo=hello, got %q", out.Foo) + } +} + +func TestNoDupSimple_EmptyMessage(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 0) + var out NoDupSimple + mustUnmarshal(t, buf, &out) +} + +func TestNoDupSimple_SingleField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "hello") + var out NoDupSimple + mustUnmarshal(t, buf, &out) + if out.Foo != "hello" { + t.Fatalf("expected Foo=hello, got %q", out.Foo) + } +} + +func TestNoDupSimple_AllFieldsDuplicated(t *testing.T) { + // Each field appears exactly once - should work. + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "f") + buf = msgp.AppendString(buf, "Bar") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "Baz") + buf = msgp.AppendBool(buf, true) + var out NoDupSimple + mustUnmarshal(t, buf, &out) +} + +func TestNoDupSimple_DuplicateSecondField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "Bar") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "Baz") + buf = msgp.AppendBool(buf, true) + buf = msgp.AppendString(buf, "Bar") // dup of second field + buf = msgp.AppendInt(buf, 2) + var out NoDupSimple + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupSimple_DuplicateThirdField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "Baz") + buf = msgp.AppendBool(buf, false) + buf = msgp.AppendString(buf, "Baz") // dup of third field + buf = msgp.AppendBool(buf, true) + var out NoDupSimple + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +// ---------- NoDupMap standalone map ---------- + +func TestNoDupMap_RoundTrip(t *testing.T) { + orig := NoDupMap{"a": "1", "b": "2", "c": "3"} + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupMap + mustUnmarshal(t, buf, &out) + if len(out) != len(orig) { + t.Fatalf("length mismatch: got %d, want %d", len(out), len(orig)) + } +} + +func TestNoDupMap_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "key1") + buf = msgp.AppendString(buf, "val1") + buf = msgp.AppendString(buf, "key2") + buf = msgp.AppendString(buf, "val2") + buf = msgp.AppendString(buf, "key1") + buf = msgp.AppendString(buf, "val3") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupMap + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupMap + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupMap_EmptyAndSingle(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 0) + var out NoDupMap + mustUnmarshal(t, buf, &out) + }) + t.Run("Single", func(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "k") + buf = msgp.AppendString(buf, "v") + var out NoDupMap + mustUnmarshal(t, buf, &out) + if out["k"] != "v" { + t.Fatal("mismatch") + } + }) +} + +// ---------- NoDupMapInt ---------- + +func TestNoDupMapInt_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "b") + buf = msgp.AppendInt(buf, 2) + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendInt(buf, 3) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupMapInt + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupMapInt + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +// ---------- NoDupMapComplex: map[string]NoDupInner ---------- + +func TestNoDupMapComplex_DuplicateOuterKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + // first entry + buf = msgp.AppendString(buf, "obj1") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 10) + // duplicate outer key + buf = msgp.AppendString(buf, "obj1") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 20) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupMapComplex + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupMapComplex + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupMapComplex_DuplicateInnerField(t *testing.T) { + // Outer keys are unique but the inner struct has a duplicate field. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "obj1") + buf = msgp.AppendMapHeader(buf, 3) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 10) + buf = msgp.AppendString(buf, "y") + buf = msgp.AppendString(buf, "hi") + buf = msgp.AppendString(buf, "x") // dup inner field + buf = msgp.AppendInt(buf, 20) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupMapComplex + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupMapComplex + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupMapComplex_RoundTrip(t *testing.T) { + orig := NoDupMapComplex{ + "a": {X: 1, Y: "hello"}, + "b": {X: 2, Y: "world"}, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupMapComplex + mustUnmarshal(t, buf, &out) + if len(out) != 2 { + t.Fatalf("expected 2 entries, got %d", len(out)) + } +} + +// ---------- NoDupNested: struct with map fields ---------- + +func TestNoDupNested_RoundTrip(t *testing.T) { + orig := NoDupNested{ + Name: "test", + Items: map[string]int{"a": 1, "b": 2}, + Tags: map[string]string{"x": "y"}, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupNested + mustUnmarshal(t, buf, &out) + if out.Name != orig.Name { + t.Fatalf("Name mismatch") + } +} + +func TestNoDupNested_DuplicateStructField(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "Name") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "Items") + buf = msgp.AppendMapHeader(buf, 0) + buf = msgp.AppendString(buf, "Name") + buf = msgp.AppendString(buf, "second") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupNested + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupNested + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupNested_DuplicateMapKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "Items") + buf = msgp.AppendMapHeader(buf, 3) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "y") + buf = msgp.AppendInt(buf, 2) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 3) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupNested + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupNested + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +// ---------- NoDupManyFields: 10 fields, 2-byte bitmask ---------- + +func TestNoDupManyFields_RoundTrip(t *testing.T) { + orig := NoDupManyFields{ + F01: "a", F02: "b", F03: "c", F04: "d", F05: "e", + F06: "f", F07: "g", F08: "h", F09: "i", F10: "j", + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupManyFields + mustUnmarshal(t, buf, &out) + if out != orig { + t.Fatalf("mismatch") + } +} + +func TestNoDupManyFields_DupFirstField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "f01") + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendString(buf, "f01") + buf = msgp.AppendString(buf, "b") + var out NoDupManyFields + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupManyFields_DupField8(t *testing.T) { + // f08 is bit 7 of byte 0 - the last bit in the first byte. + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "f08") + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendString(buf, "f08") + buf = msgp.AppendString(buf, "b") + var out NoDupManyFields + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupManyFields_DupField9(t *testing.T) { + // f09 is bit 0 of byte 1 - crosses the byte boundary. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "f09") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "f01") + buf = msgp.AppendString(buf, "ok") + buf = msgp.AppendString(buf, "f09") + buf = msgp.AppendString(buf, "second") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupManyFields + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupManyFields + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupManyFields_DupLastField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "f10") + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendString(buf, "f10") + buf = msgp.AppendString(buf, "b") + var out NoDupManyFields + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +// ---------- NoDup17Fields: 17 fields, 3-byte bitmask ---------- + +func TestNoDup17Fields_RoundTrip(t *testing.T) { + orig := NoDup17Fields{ + A01: 1, A02: 2, A03: 3, A04: 4, A05: 5, A06: 6, A07: 7, A08: 8, + A09: 9, A10: 10, A11: 11, A12: 12, A13: 13, A14: 14, A15: 15, A16: 16, A17: 17, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDup17Fields + mustUnmarshal(t, buf, &out) + if out != orig { + t.Fatalf("mismatch") + } +} + +func TestNoDup17Fields_DupField17(t *testing.T) { + // a17 is bit 0 of byte 2. + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "a17") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "a17") + buf = msgp.AppendInt(buf, 2) + var out NoDup17Fields + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDup17Fields_DupField16(t *testing.T) { + // a16 is bit 7 of byte 1 - last bit of second byte. + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "a16") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "a16") + buf = msgp.AppendInt(buf, 2) + var out NoDup17Fields + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +// ---------- NoDupRichTypes: mixed field types ---------- + +func TestNoDupRichTypes_RoundTrip(t *testing.T) { + s := "ptr" + orig := NoDupRichTypes{ + Str: "hello", + Num: 3.14, + Data: []byte{1, 2, 3}, + PtrStr: &s, + Strings: []string{"a", "b"}, + Inner: NoDupInner{X: 10, Y: "y"}, + PtrInner: &NoDupInner{X: 20, Y: "z"}, + MapSlice: map[string][]int{"k": {1, 2}}, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupRichTypes + mustUnmarshal(t, buf, &out) + if out.Str != orig.Str || out.Num != orig.Num { + t.Fatalf("basic field mismatch") + } + if *out.PtrStr != *orig.PtrStr { + t.Fatalf("PtrStr mismatch") + } +} + +func TestNoDupRichTypes_DupStringField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "str") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "str") + buf = msgp.AppendString(buf, "second") + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupNumField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "num") + buf = msgp.AppendFloat64(buf, 1.0) + buf = msgp.AppendString(buf, "num") + buf = msgp.AppendFloat64(buf, 2.0) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupBytesField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "data") + buf = msgp.AppendBytes(buf, []byte{1}) + buf = msgp.AppendString(buf, "data") + buf = msgp.AppendBytes(buf, []byte{2}) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupPointerField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "ptr_str") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "ptr_str") + buf = msgp.AppendString(buf, "second") + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupSliceField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "strings") + buf = msgp.AppendArrayHeader(buf, 1) + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendString(buf, "strings") + buf = msgp.AppendArrayHeader(buf, 1) + buf = msgp.AppendString(buf, "b") + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupNestedStructField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "inner") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "inner") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 2) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupInsideNestedStruct(t *testing.T) { + // inner struct itself has a duplicate field + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "inner") + buf = msgp.AppendMapHeader(buf, 3) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "y") + buf = msgp.AppendString(buf, "ok") + buf = msgp.AppendString(buf, "x") // dup inside inner + buf = msgp.AppendInt(buf, 2) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupPtrStructField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "ptr_inner") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "ptr_inner") + buf = msgp.AppendMapHeader(buf, 1) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 2) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupRichTypes_DupMapSliceKey(t *testing.T) { + // map_slice field has a duplicate key + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "map_slice") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendString(buf, "k") + buf = msgp.AppendArrayHeader(buf, 1) + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "k") // dup map key + buf = msgp.AppendArrayHeader(buf, 1) + buf = msgp.AppendInt(buf, 2) + var out NoDupRichTypes + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +// ---------- NoDupOmitempty: interaction with omitempty ---------- + +func TestNoDupOmitempty_RoundTrip(t *testing.T) { + orig := NoDupOmitempty{Required: "r", Optional: "o", Flag: true, Count: 5} + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupOmitempty + mustUnmarshal(t, buf, &out) + if out != orig { + t.Fatalf("mismatch: got %+v, want %+v", out, orig) + } +} + +func TestNoDupOmitempty_DupRequiredField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "required") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "required") + buf = msgp.AppendString(buf, "second") + var out NoDupOmitempty + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupOmitempty_DupOmitemptyField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "optional") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "optional") + buf = msgp.AppendString(buf, "second") + var out NoDupOmitempty + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupOmitempty_PartialFieldsOK(t *testing.T) { + // Only some fields present - no duplicates. + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "required") + buf = msgp.AppendString(buf, "val") + buf = msgp.AppendString(buf, "count") + buf = msgp.AppendInt(buf, 42) + var out NoDupOmitempty + mustUnmarshal(t, buf, &out) + if out.Required != "val" || out.Count != 42 { + t.Fatalf("mismatch: got %+v", out) + } +} + +// ---------- NoDupAllownil: interaction with allownil ---------- + +func TestNoDupAllownil_RoundTrip(t *testing.T) { + orig := NoDupAllownil{ + Name: "test", + Vals: []byte{1, 2}, + Items: []int{3, 4}, + Mapping: map[string]string{"k": "v"}, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupAllownil + mustUnmarshal(t, buf, &out) + if out.Name != orig.Name { + t.Fatal("name mismatch") + } +} + +func TestNoDupAllownil_NilFieldsThenDup(t *testing.T) { + // Send nil for allownil fields, then a duplicate struct field. + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "vals") + buf = msgp.AppendNil(buf) + buf = msgp.AppendString(buf, "name") + buf = msgp.AppendString(buf, "hello") + buf = msgp.AppendString(buf, "vals") // dup + buf = msgp.AppendNil(buf) + var out NoDupAllownil + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupAllownil_DupNameField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "name") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "name") + buf = msgp.AppendString(buf, "second") + var out NoDupAllownil + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupAllownil_DupMapKey(t *testing.T) { + // The "mapping" field's map has a duplicate key. + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "mapping") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendString(buf, "k") + buf = msgp.AppendString(buf, "v1") + buf = msgp.AppendString(buf, "k") // dup inside map + buf = msgp.AppendString(buf, "v2") + var out NoDupAllownil + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupAllownil_AllNilNoDup(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 4) + buf = msgp.AppendString(buf, "name") + buf = msgp.AppendString(buf, "") + buf = msgp.AppendString(buf, "vals") + buf = msgp.AppendNil(buf) + buf = msgp.AppendString(buf, "items") + buf = msgp.AppendNil(buf) + buf = msgp.AppendString(buf, "mapping") + buf = msgp.AppendNil(buf) + var out NoDupAllownil + mustUnmarshal(t, buf, &out) +} + +// ---------- Per-type directive (noduplicates_specific.go) ---------- + +func TestNoDupTargeted_DuplicateField(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "alpha") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "beta") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "alpha") // dup + buf = msgp.AppendString(buf, "second") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupTargeted + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupTargeted + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupTargeted_RoundTrip(t *testing.T) { + orig := NoDupTargeted{Alpha: "a", Beta: 1, Gamma: true} + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupTargeted + mustUnmarshal(t, buf, &out) + if out != orig { + t.Fatal("mismatch") + } +} + +func TestNoDupTargetedMap_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "x") // dup + buf = msgp.AppendInt(buf, 2) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupTargetedMap + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupTargetedMap + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupUntargeted_DuplicatesAllowed(t *testing.T) { + // NoDupUntargeted is NOT targeted by the directive - duplicates must be accepted. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendString(buf, "alpha") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "beta") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "alpha") // dup - should be silently accepted + buf = msgp.AppendString(buf, "second") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupUntargeted + _, err := out.UnmarshalMsg(mkBuf()) + expectNoDup(t, err) + if out.Alpha != "second" { + t.Fatalf("expected last value, got %q", out.Alpha) + } + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupUntargeted + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectNoDup(t, out.DecodeMsg(r)) + if out.Alpha != "second" { + t.Fatalf("expected last value, got %q", out.Alpha) + } + }) +} + +func TestNoDupUntargetedMap_DuplicatesAllowed(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "x") + buf = msgp.AppendString(buf, "v1") + buf = msgp.AppendString(buf, "x") // dup - should be silently accepted + buf = msgp.AppendString(buf, "v2") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupUntargetedMap + _, err := out.UnmarshalMsg(mkBuf()) + expectNoDup(t, err) + if out["x"] != "v2" { + t.Fatalf("expected last value, got %q", out["x"]) + } + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupUntargetedMap + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectNoDup(t, out.DecodeMsg(r)) + if out["x"] != "v2" { + t.Fatalf("expected last value, got %q", out["x"]) + } + }) +} + +// ---------- Error quality ---------- + +func TestNoDup_ErrorContainsFieldName(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "second") + var out NoDupSimple + _, err := out.UnmarshalMsg(buf) + if err == nil { + t.Fatal("expected error") + } + if !bytes.Contains([]byte(err.Error()), []byte("Foo")) { + t.Fatalf("error should reference field 'Foo', got: %s", err.Error()) + } +} + +func TestNoDup_ErrorIsUnwrappable(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendString(buf, "second") + var out NoDupSimple + _, err := out.UnmarshalMsg(buf) + if err == nil { + t.Fatal("expected error") + } + if cause := msgp.Cause(err); cause != msgp.ErrDuplicateEntry { + t.Fatalf("Cause should be ErrDuplicateEntry, got %v", cause) + } + if !errors.Is(err, msgp.ErrDuplicateEntry) { + t.Fatal("errors.Is should match ErrDuplicateEntry") + } +} + +func TestNoDupMap_ErrorContainsKey(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "mykey") + buf = msgp.AppendString(buf, "v1") + buf = msgp.AppendString(buf, "mykey") + buf = msgp.AppendString(buf, "v2") + var out NoDupMap + _, err := out.UnmarshalMsg(buf) + if err == nil { + t.Fatal("expected error") + } + if !bytes.Contains([]byte(err.Error()), []byte("mykey")) { + t.Fatalf("error should reference key 'mykey', got: %s", err.Error()) + } +} + +func TestNoDupNested_MapErrorContainsContext(t *testing.T) { + // A duplicate inside the Items map should reference the field path. + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "Items") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendString(buf, "dupkey") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "dupkey") + buf = msgp.AppendInt(buf, 2) + var out NoDupNested + _, err := out.UnmarshalMsg(buf) + if err == nil { + t.Fatal("expected error") + } + errStr := err.Error() + if !bytes.Contains([]byte(errStr), []byte("Items")) { + t.Fatalf("error should reference 'Items', got: %s", errStr) + } + if !bytes.Contains([]byte(errStr), []byte("dupkey")) { + t.Fatalf("error should reference 'dupkey', got: %s", errStr) + } +} + +// ---------- Binary key maps (noduplicates_binkey.go) ---------- + +func TestNoDupBinKeyInt_RoundTrip(t *testing.T) { + orig := NoDupBinKeyInt{1: "a", 2: "b", 3: "c"} + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupBinKeyInt + mustUnmarshal(t, buf, &out) + if len(out) != 3 { + t.Fatalf("expected 3 entries, got %d", len(out)) + } +} + +func TestNoDupBinKeyInt_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 3) + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "a") + buf = msgp.AppendInt(buf, 2) + buf = msgp.AppendString(buf, "b") + buf = msgp.AppendInt(buf, 1) // dup + buf = msgp.AppendString(buf, "c") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupBinKeyInt + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupBinKeyInt + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupBinKeyUint32_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendUint32(buf, 42) + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendUint32(buf, 42) // dup + buf = msgp.AppendInt(buf, 2) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupBinKeyUint32 + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupBinKeyUint32 + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupBinKeyArray_DuplicateKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendBytes(buf, []byte{0x01, 0x02}) + buf = msgp.AppendInt(buf, 10) + buf = msgp.AppendBytes(buf, []byte{0x01, 0x02}) // dup + buf = msgp.AppendInt(buf, 20) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupBinKeyArray + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupBinKeyArray + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupBinKeyArray_UniqueKeys(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendBytes(buf, []byte{0x01, 0x02}) + buf = msgp.AppendInt(buf, 10) + buf = msgp.AppendBytes(buf, []byte{0x03, 0x04}) + buf = msgp.AppendInt(buf, 20) + var out NoDupBinKeyArray + mustUnmarshal(t, buf, &out) + if out[[2]byte{0x01, 0x02}] != 10 || out[[2]byte{0x03, 0x04}] != 20 { + t.Fatalf("mismatch: %+v", out) + } +} + +func TestNoDupBinKeyStruct_DupStructField(t *testing.T) { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "name") + buf = msgp.AppendString(buf, "first") + buf = msgp.AppendString(buf, "name") // dup + buf = msgp.AppendString(buf, "second") + var out NoDupBinKeyStruct + _, err := out.UnmarshalMsg(buf) + expectDup(t, err) +} + +func TestNoDupBinKeyStruct_DupIntMapKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "int_map") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendInt(buf, 5) + buf = msgp.AppendString(buf, "five") + buf = msgp.AppendInt(buf, 5) // dup + buf = msgp.AppendString(buf, "cinq") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupBinKeyStruct + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupBinKeyStruct + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupBinKeyStruct_DupStrMapKey(t *testing.T) { + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "str_map") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendString(buf, "k") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "k") // dup + buf = msgp.AppendInt(buf, 2) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupBinKeyStruct + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupBinKeyStruct + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +// ---------- Shimmed key maps (noduplicates_shim.go) ---------- + +func TestNoDupShimMap_RoundTrip(t *testing.T) { + orig := NoDupShimMap{"foo": 1, "bar": 2} + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupShimMap + mustUnmarshal(t, buf, &out) + if len(out) != 2 { + t.Fatalf("expected 2 entries, got %d", len(out)) + } +} + +func TestNoDupShimMap_DuplicateWireKey(t *testing.T) { + // Same wire string sent twice. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "foo") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "foo") // dup + buf = msgp.AppendInt(buf, 2) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupShimMap + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupShimMap + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupShimMap_DifferentWireKeysSameShimmedValue(t *testing.T) { + // "Foo" and "foo" are different wire strings but noDupShimDec lowercases both, + // so they resolve to the same map key. Should be detected as duplicate. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 2) + buf = msgp.AppendString(buf, "Foo") + buf = msgp.AppendInt(buf, 1) + buf = msgp.AppendString(buf, "foo") // different wire key, same after shim + buf = msgp.AppendInt(buf, 2) + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupShimMap + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupShimMap + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupShimStruct_DupShimMapKey(t *testing.T) { + // Shimmed map field inside a struct with keys that collapse. + mkBuf := func() []byte { + buf := msgp.AppendMapHeader(nil, 1) + buf = msgp.AppendString(buf, "data") + buf = msgp.AppendMapHeader(buf, 2) + buf = msgp.AppendString(buf, "Hello") + buf = msgp.AppendString(buf, "world") + buf = msgp.AppendString(buf, "hello") // collapses to same key + buf = msgp.AppendString(buf, "again") + return buf + } + t.Run("Unmarshal", func(t *testing.T) { + var out NoDupShimStruct + _, err := out.UnmarshalMsg(mkBuf()) + expectDup(t, err) + }) + t.Run("Decode", func(t *testing.T) { + var out NoDupShimStruct + r := msgp.NewReader(bytes.NewReader(mkBuf())) + expectDup(t, out.DecodeMsg(r)) + }) +} + +func TestNoDupShimStruct_RoundTrip(t *testing.T) { + orig := NoDupShimStruct{ + Name: "test", + Data: map[NoDupShimKey]string{"foo": "bar", "baz": "qux"}, + } + buf, err := orig.MarshalMsg(nil) + expectNoDup(t, err) + var out NoDupShimStruct + mustUnmarshal(t, buf, &out) + if out.Name != "test" || len(out.Data) != 2 { + t.Fatalf("mismatch: %+v", out) + } +} diff --git a/gen/decode.go b/gen/decode.go index cf3f05f7..8712774a 100644 --- a/gen/decode.go +++ b/gen/decode.go @@ -255,6 +255,12 @@ func (d *decodeGen) structAsMap(s *Struct) { // Index to field idx of each emitted oeEmittedIdx := []int{} + dupVar := sz + "Dup" + if d.ctx.noDuplicates && len(s.Fields) > 0 { + nBytes := (len(s.Fields) + 7) / 8 + d.p.printf("\nvar %s [%d]byte", dupVar, nBytes) + } + d.p.printf("\nfor %s > 0 {\n%s--", sz, sz) d.assignAndCheck("field", mapKey) d.p.print("\nswitch msgp.UnsafeString(field) {") @@ -264,6 +270,16 @@ func (d *decodeGen) structAsMap(s *Struct) { fieldElem := s.Fields[i].FieldElem anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() + if d.ctx.noDuplicates { + byteIdx := i / 8 + bitIdx := uint(i % 8) + d.p.printf("\nif %s[%d] & (1<<%d) != 0 {", dupVar, byteIdx, bitIdx) + d.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %q)", s.Fields[i].FieldTag) + d.p.printf("\nreturn") + d.p.printf("\n}") + d.p.printf("\n%s[%d] |= 1<<%d", dupVar, byteIdx, bitIdx) + } + // Set field-specific limits in context based on struct field's FieldLimit if s.Fields[i].FieldLimit > 0 { // Apply same limit to both arrays and maps for this field @@ -453,12 +469,36 @@ func (d *decodeGen) gMap(m *Map) { d.needsField() d.p.printf("\nfor %s > 0 {\n%s--", sz, sz) m.readKey(d.ctx, d.p, d, d.assignAndCheck) + + // Duplicate check for non-shimmed maps (early, before reading the value). + // Shimmed maps are checked later inside mapAssign after the key is resolved. + if d.ctx.noDuplicates && (m.Key == nil || m.AllowBinMaps) { + okVar := randIdent() + d.p.printf("\nif _, %s := %s[%s]; %s {", okVar, m.Varname(), m.Keyidx, okVar) + if args := d.ctx.ArgsStr(); args != "" { + d.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s, %s)", args, m.Keyidx) + } else { + d.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s)", m.Keyidx) + } + d.p.printf("\nreturn") + d.p.printf("\n}") + } + + var dupCtx string + if d.ctx.noDuplicates { + if args := d.ctx.ArgsStr(); args != "" { + dupCtx = args + ", " + m.Keyidx + } else { + dupCtx = m.Keyidx + } + } + d.p.declare(m.Validx, m.Value.TypeName()) d.ctx.PushVar(m.Keyidx) m.Value.SetIsAllowNil(false) setTypeParams(m.Value, m.typeParams) next(d, m.Value) - d.p.mapAssign(m) + d.p.mapAssign(m, d.ctx.noDuplicates, dupCtx) d.ctx.Pop() d.p.closeblock() } diff --git a/gen/spec.go b/gen/spec.go index b28f5420..f17e1b69 100644 --- a/gen/spec.go +++ b/gen/spec.go @@ -87,6 +87,8 @@ type Printer struct { MapLimit uint32 MarshalLimits bool LimitPrefix string + NoDuplicates bool + NoDupTypes map[string]struct{} } func NewPrinter(m Method, out io.Writer, tests io.Writer) *Printer { @@ -163,6 +165,8 @@ func (p *Printer) ApplyDirective(pass Method, t TransformPass) { // Print prints an Elem. func (p *Printer) Print(e Elem) error { e.SetIsAllowNil(false) + _, noDup := p.NoDupTypes[e.BaseTypeName()] + noDup = noDup || p.NoDuplicates for _, g := range p.gens { // Elem.SetVarname() is called before the Print() step in parse.FileSet.PrintTo(). // Elem.SetVarname() generates identifiers as it walks the Elem. This can cause @@ -180,6 +184,7 @@ func (p *Printer) Print(e Elem) error { limitPrefix: p.LimitPrefix, currentFieldArrayLimit: math.MaxUint32, // Initialize to "no field limit" currentFieldMapLimit: math.MaxUint32, // Initialize to "no field limit" + noDuplicates: noDup, }) resetIdent("za") @@ -218,6 +223,7 @@ type Context struct { limitPrefix string currentFieldArrayLimit uint32 // Current field's array limit (0 = no field-level limit) currentFieldMapLimit uint32 // Current field's map limit (0 = no field-level limit) + noDuplicates bool // Reject duplicate keys in maps/struct fields } func (c *Context) PushString(s string) { @@ -403,8 +409,24 @@ func (p *printer) resizeMap(size string, m *Map) { // CanAutoShim contains the primitives that can be auto-shimmed. var CanAutoShim = map[Primitive]bool{Uint: true, Uint8: true, Uint16: true, Uint32: true, Uint64: true, Int: true, Int8: true, Int16: true, Int32: true, Int64: true, Bool: true, Float32: true, Float64: true, Byte: true} -// assign key to value based on varnames -func (p *printer) mapAssign(m *Map) { +// mapAssignDupCheck generates a duplicate key check before map assignment. +// resolvedKey is the Go expression for the map key after any shimming. +func (p *printer) mapAssignDupCheck(m *Map, resolvedKey string, dupCtx string) { + okVar := randIdent() + p.printf("\nif _, %s := %s[%s]; %s {", okVar, m.Varname(), resolvedKey, okVar) + if dupCtx != "" { + p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s)", dupCtx) + } else { + p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry)") + } + p.printf("\nreturn") + p.printf("\n}") +} + +// assign key to value based on varnames. +// dupCheck enables duplicate key detection for shimmed maps (non-shimmed maps +// are checked earlier in gMap before reading the value). +func (p *printer) mapAssign(m *Map, dupCheck bool, dupCtx string) { if !p.ok() { return } @@ -425,24 +447,51 @@ func (p *printer) mapAssign(m *Map) { } else { p.printf("\n%sTmp = %s(%s)", m.Keyidx, fromBase, m.Keyidx) } + if dupCheck { + p.mapAssignDupCheck(m, m.Keyidx+"Tmp", dupCtx) + } p.printf("\n%s[%sTmp] = %s", m.Varname(), m.Keyidx, m.Validx) return } else if key.Value == IDENT { - p.printf("\n%s[%s(%s)] = %s", m.Varname(), fromBase, m.Keyidx, m.Validx) + if dupCheck { + // Evaluate once into a temp to avoid double call. + p.printf("\nvar %sResolved %s", m.Keyidx, key.TypeName()) + p.printf("\n%sResolved = %s(%s)", m.Keyidx, fromBase, m.Keyidx) + p.mapAssignDupCheck(m, m.Keyidx+"Resolved", dupCtx) + p.printf("\n%s[%sResolved] = %s", m.Varname(), m.Keyidx, m.Validx) + } else { + p.printf("\n%s[%s(%s)] = %s", m.Varname(), fromBase, m.Keyidx, m.Validx) + } return } else { if shimErr { p.printf("\nvar %sTmp %s", m.Keyidx, strings.ToLower(key.Value.String())) p.printf("\n%sTmp, err = %s(%s)", m.Keyidx, fromBase, m.Keyidx) p.wrapErrCheck("\"shim: " + m.Varname() + "\"") - p.printf("\n%s[%s(%sTmp)] = %s", m.Varname(), key.FromBase(), m.Keyidx, m.Validx) + resolvedExpr := key.FromBase() + "(" + m.Keyidx + "Tmp)" + if dupCheck { + p.printf("\nvar %sResolved %s", m.Keyidx, key.TypeName()) + p.printf("\n%sResolved = %s", m.Keyidx, resolvedExpr) + p.mapAssignDupCheck(m, m.Keyidx+"Resolved", dupCtx) + p.printf("\n%s[%sResolved] = %s", m.Varname(), m.Keyidx, m.Validx) + } else { + p.printf("\n%s[%s] = %s", m.Varname(), resolvedExpr, m.Validx) + } } else { - p.printf("\n%s[%s(%s)] = %s", m.Varname(), fromBase, m.Keyidx, m.Validx) + if dupCheck { + p.printf("\nvar %sResolved %s", m.Keyidx, key.TypeName()) + p.printf("\n%sResolved = %s(%s)", m.Keyidx, fromBase, m.Keyidx) + p.mapAssignDupCheck(m, m.Keyidx+"Resolved", dupCtx) + p.printf("\n%s[%sResolved] = %s", m.Varname(), m.Keyidx, m.Validx) + } else { + p.printf("\n%s[%s(%s)] = %s", m.Varname(), fromBase, m.Keyidx, m.Validx) + } } return } } } + // Non-shimmed: dup check already done in gMap before reading value. p.printf("\n%s[%s] = %s", m.Varname(), m.Keyidx, m.Validx) } diff --git a/gen/unmarshal.go b/gen/unmarshal.go index 21f30b54..18588529 100644 --- a/gen/unmarshal.go +++ b/gen/unmarshal.go @@ -312,6 +312,12 @@ func (u *unmarshalGen) mapstruct(s *Struct) { // Index to field idx of each emitted oeEmittedIdx := []int{} + dupVar := sz + "Dup" + if u.ctx.noDuplicates && len(s.Fields) > 0 { + nBytes := (len(s.Fields) + 7) / 8 + u.p.printf("\nvar %s [%d]byte", dupVar, nBytes) + } + u.p.printf("\nfor %s > 0 {", sz) u.p.printf("\n%s--; field, bts, err = msgp.ReadMapKeyZC(bts)", sz) u.p.wrapErrCheck(u.ctx.ArgsStr()) @@ -326,6 +332,16 @@ func (u *unmarshalGen) mapstruct(s *Struct) { fieldElem := s.Fields[i].FieldElem anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() + if u.ctx.noDuplicates { + byteIdx := i / 8 + bitIdx := uint(i % 8) + u.p.printf("\nif %s[%d] & (1<<%d) != 0 {", dupVar, byteIdx, bitIdx) + u.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %q)", s.Fields[i].FieldTag) + u.p.printf("\nreturn") + u.p.printf("\n}") + u.p.printf("\n%s[%d] |= 1<<%d", dupVar, byteIdx, bitIdx) + } + // Set field-specific limits in context based on struct field's FieldLimit if s.Fields[i].FieldLimit > 0 { // Apply same limit to both arrays and maps for this field @@ -532,12 +548,36 @@ func (u *unmarshalGen) gMap(m *Map) { u.p.printf("\nfor %s > 0 {", sz) u.p.printf("\nvar %s %s; %s--", m.Validx, m.Value.TypeName(), sz) m.readKey(u.ctx, u.p, u, u.assignAndCheck) + + // Duplicate check for non-shimmed maps (early, before reading the value). + // Shimmed maps are checked later inside mapAssign after the key is resolved. + if u.ctx.noDuplicates && (m.Key == nil || m.AllowBinMaps) { + okVar := randIdent() + u.p.printf("\nif _, %s := %s[%s]; %s {", okVar, m.Varname(), m.Keyidx, okVar) + if args := u.ctx.ArgsStr(); args != "" { + u.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s, %s)", args, m.Keyidx) + } else { + u.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s)", m.Keyidx) + } + u.p.printf("\nreturn") + u.p.printf("\n}") + } + + var dupCtx string + if u.ctx.noDuplicates { + if args := u.ctx.ArgsStr(); args != "" { + dupCtx = args + ", " + m.Keyidx + } else { + dupCtx = m.Keyidx + } + } + u.ctx.PushVar(m.Keyidx) m.Value.SetIsAllowNil(false) setTypeParams(m.Value, m.typeParams) next(u, m.Value) u.ctx.Pop() - u.p.mapAssign(m) + u.p.mapAssign(m, u.ctx.noDuplicates, dupCtx) u.p.closeblock() } diff --git a/msgp/errors.go b/msgp/errors.go index ce422073..fd9c1a5a 100644 --- a/msgp/errors.go +++ b/msgp/errors.go @@ -21,6 +21,10 @@ var ( // Limits can be set on the Reader to prevent excessive memory usage by adversarial data. ErrLimitExceeded error = errLimitExceeded{} + // ErrDuplicateEntry is returned when a duplicate map key or struct field + // is encountered during decoding and the noduplicates directive is active. + ErrDuplicateEntry error = errDuplicateEntry{} + // this error is only returned // if we reach code that should // be unreachable @@ -152,6 +156,11 @@ type errLimitExceeded struct{} func (e errLimitExceeded) Error() string { return "msgp: configured reader limit exceeded" } func (e errLimitExceeded) Resumable() bool { return false } +type errDuplicateEntry struct{} + +func (e errDuplicateEntry) Error() string { return "msgp: duplicate map key or field" } +func (e errDuplicateEntry) Resumable() bool { return false } + // ArrayError is an error returned // when decoding a fix-sized array // of the wrong size diff --git a/parse/directives.go b/parse/directives.go index 1460321d..b03eeb86 100644 --- a/parse/directives.go +++ b/parse/directives.go @@ -38,6 +38,7 @@ var directives = map[string]directive{ "binappend": binappend, "textmarshal": textmarshal, "textappend": textappend, + "noduplicates": noduplicates, } // map of all recognized directives which will be applied @@ -524,3 +525,21 @@ func textappend(text []string, f *FileSet) error { return nil } + +//msgp:noduplicates [TypeA TypeB ...] +func noduplicates(text []string, f *FileSet) error { + if len(text) < 2 { + f.NoDuplicates = true + infof("rejecting duplicate keys for all types\n") + return nil + } + if f.NoDupTypes == nil { + f.NoDupTypes = make(map[string]struct{}) + } + for _, item := range text[1:] { + name := strings.TrimSpace(item) + f.NoDupTypes[name] = struct{}{} + infof("rejecting duplicate keys for %s\n", name) + } + return nil +} diff --git a/parse/getast.go b/parse/getast.go index 15f4cccc..4e69ecd0 100644 --- a/parse/getast.go +++ b/parse/getast.go @@ -38,10 +38,12 @@ type FileSet struct { AllowMapShims bool // Allow map keys to be shimmed (default true) AllowBinMaps bool // Allow maps with binary keys to be used (default false) AutoMapShims bool // Automatically shim map keys of builtin types(default false) - ArrayLimit uint32 // Maximum array/slice size allowed during deserialization - MapLimit uint32 // Maximum map size allowed during deserialization - MarshalLimits bool // Whether to enforce limits during marshaling - LimitPrefix string // Unique prefix for limit constants to avoid collisions + ArrayLimit uint32 // Maximum array/slice size allowed during deserialization + MapLimit uint32 // Maximum map size allowed during deserialization + MarshalLimits bool // Whether to enforce limits during marshaling + LimitPrefix string // Unique prefix for limit constants to avoid collisions + NoDuplicates bool // Reject duplicate keys for all types + NoDupTypes map[string]struct{} // Reject duplicate keys for specific types only tagName string // tag to read field names from pointerRcv bool // generate with pointer receivers. @@ -422,6 +424,8 @@ loop: p.MapLimit = fs.MapLimit p.MarshalLimits = fs.MarshalLimits p.LimitPrefix = fs.LimitPrefix + p.NoDuplicates = fs.NoDuplicates + p.NoDupTypes = fs.NoDupTypes } func (fs *FileSet) PrintTo(p *gen.Printer) error { From 7dc7c56bbf2f196175ca791f5e9e4e2df2ab114c Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 15 Apr 2026 15:47:38 +0200 Subject: [PATCH 2/2] Apply suggestion. go fmt the _generated folder. --- _generated/def.go | 2 +- _generated/field_limits.go | 4 ++-- _generated/limits_test.go | 20 ++++++++++---------- _generated/marshal_limits.go | 12 ++++++------ _generated/noduplicates.go | 14 +++++++------- _generated/noduplicates_binkey.go | 6 +++--- _generated/noduplicates_shim.go | 2 +- _generated/noduplicates_test.go | 8 ++++++++ gen/decode.go | 2 +- gen/unmarshal.go | 2 +- 10 files changed, 40 insertions(+), 32 deletions(-) diff --git a/_generated/def.go b/_generated/def.go index d8d9af32..29714198 100644 --- a/_generated/def.go +++ b/_generated/def.go @@ -65,7 +65,7 @@ type X struct { type TestType struct { F *float64 `msg:"float"` Els map[string]string `msg:"elements"` - Obj struct { // test anonymous struct + Obj struct { // test anonymous struct ValueA string `msg:"value_a"` ValueB []byte `msg:"value_b"` } `msg:"object"` diff --git a/_generated/field_limits.go b/_generated/field_limits.go index d27542a2..90baf75e 100644 --- a/_generated/field_limits.go +++ b/_generated/field_limits.go @@ -13,7 +13,7 @@ type FieldLimitTestData struct { LargeSlice []string `msg:"large_slice,limit=100"` SmallMap map[string]int `msg:"small_map,limit=3"` LargeMap map[int]string `msg:"large_map,limit=20"` - NoLimit []byte `msg:"no_limit"` // Uses file-level limits if any + NoLimit []byte `msg:"no_limit"` // Uses file-level limits if any FixedArray [10]int `msg:"fixed_array,limit=2"` // Should be ignored } @@ -25,4 +25,4 @@ type AliasedFieldLimitTestData struct { LargeAliasedMap AliasedMap `msg:"large_aliased_map,limit=25"` IntSliceAlias AliasedIntSlice `msg:"int_slice_alias,limit=10"` NoLimitAlias AliasedSlice `msg:"no_limit_alias"` // Uses file-level limits -} \ No newline at end of file +} diff --git a/_generated/limits_test.go b/_generated/limits_test.go index 9ee25872..f8ed033e 100644 --- a/_generated/limits_test.go +++ b/_generated/limits_test.go @@ -1027,7 +1027,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send bytes that exceed file limit (100) buf := msgp.AppendMapHeader(nil, 1) - buf = msgp.AppendString(buf, "nil_slice") // Uses file limit (100) + buf = msgp.AppendString(buf, "nil_slice") // Uses file limit (100) buf = msgp.AppendBytes(buf, make([]byte, 150)) // Exceeds limit _, err := data.UnmarshalMsg(buf) @@ -1041,7 +1041,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send bytes that exceed file limit (100) buf := msgp.AppendMapHeader(nil, 1) - buf = msgp.AppendString(buf, "nil_slice") // Uses file limit (100) + buf = msgp.AppendString(buf, "nil_slice") // Uses file limit (100) buf = msgp.AppendBytes(buf, make([]byte, 150)) // Exceeds limit reader := msgp.NewReader(bytes.NewReader(buf)) @@ -1057,7 +1057,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send slice that exceeds field limit buf := msgp.AppendMapHeader(nil, 1) buf = msgp.AppendString(buf, "nil_tight_slice") // Field limit = 5 - buf = msgp.AppendArrayHeader(buf, 8) // Exceeds field limit + buf = msgp.AppendArrayHeader(buf, 8) // Exceeds field limit for i := 0; i < 8; i++ { buf = msgp.AppendInt(buf, i) } @@ -1074,7 +1074,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send slice that exceeds field limit buf := msgp.AppendMapHeader(nil, 1) buf = msgp.AppendString(buf, "nil_tight_slice") // Field limit = 5 - buf = msgp.AppendArrayHeader(buf, 8) // Exceeds field limit + buf = msgp.AppendArrayHeader(buf, 8) // Exceeds field limit for i := 0; i < 8; i++ { buf = msgp.AppendInt(buf, i) } @@ -1092,7 +1092,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send map that exceeds field limit buf := msgp.AppendMapHeader(nil, 1) buf = msgp.AppendString(buf, "nil_tight_map") // Field limit = 3 - buf = msgp.AppendMapHeader(buf, 5) // Exceeds field limit + buf = msgp.AppendMapHeader(buf, 5) // Exceeds field limit for i := 0; i < 5; i++ { buf = msgp.AppendString(buf, fmt.Sprintf("key%d", i)) buf = msgp.AppendInt(buf, i) @@ -1110,7 +1110,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Try to send map that exceeds field limit buf := msgp.AppendMapHeader(nil, 1) buf = msgp.AppendString(buf, "nil_tight_map") // Field limit = 3 - buf = msgp.AppendMapHeader(buf, 5) // Exceeds field limit + buf = msgp.AppendMapHeader(buf, 5) // Exceeds field limit for i := 0; i < 5; i++ { buf = msgp.AppendString(buf, fmt.Sprintf("key%d", i)) buf = msgp.AppendInt(buf, i) @@ -1134,7 +1134,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Manually craft a message with a huge size header but don't include the data // This should fail at the limit check, not during data reading - buf = append(buf, 0xc6) // bin32 format + buf = append(buf, 0xc6) // bin32 format buf = append(buf, 0x00, 0x00, 0x27, 0x10) // size = 10000 (exceeds limit of 100) // Don't append actual data - should fail before trying to read it @@ -1155,7 +1155,7 @@ func TestAllowNilSecurityLimits(t *testing.T) { // Manually craft a message with a huge size header but don't include the data // This should fail at the limit check, not during data reading - buf = append(buf, 0xc6) // bin32 format + buf = append(buf, 0xc6) // bin32 format buf = append(buf, 0x00, 0x00, 0x27, 0x10) // size = 10000 (exceeds limit of 100) // Don't append actual data - should fail before trying to read it @@ -1267,7 +1267,7 @@ func TestAllowNilZeroCopy(t *testing.T) { buf = msgp.AppendString(buf, "nil_zc_slice") // Manually craft message with huge header but no data - buf = append(buf, 0xc6) // bin32 format + buf = append(buf, 0xc6) // bin32 format buf = append(buf, 0x00, 0x00, 0x27, 0x10) // size = 10000 (exceeds limit) _, err := data.UnmarshalMsg(buf) @@ -1375,7 +1375,7 @@ func TestAllowNilZeroCopy(t *testing.T) { buf = msgp.AppendString(buf, "nil_zc_slice") // Manually craft message with huge header but no data - buf = append(buf, 0xc6) // bin32 format + buf = append(buf, 0xc6) // bin32 format buf = append(buf, 0x00, 0x00, 0x27, 0x10) // size = 10000 (exceeds limit) reader := msgp.NewReader(bytes.NewReader(buf)) diff --git a/_generated/marshal_limits.go b/_generated/marshal_limits.go index bf2fea03..af4f74d3 100644 --- a/_generated/marshal_limits.go +++ b/_generated/marshal_limits.go @@ -14,10 +14,10 @@ type MarshalLimitTestData struct { // Test field limits vs file limits precedence with marshal:true // File limits: arrays:30 maps:20 marshal:true type MarshalFieldOverrideTestData struct { - TightSlice []int `msg:"tight_slice,limit=10"` // Field limit (10) < file limit (30) - LooseSlice []string `msg:"loose_slice,limit=50"` // Field limit (50) > file limit (30) - TightMap map[string]int `msg:"tight_map,limit=5"` // Field limit (5) < file limit (20) - LooseMap map[int]string `msg:"loose_map,limit=40"` // Field limit (40) > file limit (20) - DefaultSlice []byte `msg:"default_slice"` // No field limit, uses file limit (30) - DefaultMap map[string]byte `msg:"default_map"` // No field limit, uses file limit (20) + TightSlice []int `msg:"tight_slice,limit=10"` // Field limit (10) < file limit (30) + LooseSlice []string `msg:"loose_slice,limit=50"` // Field limit (50) > file limit (30) + TightMap map[string]int `msg:"tight_map,limit=5"` // Field limit (5) < file limit (20) + LooseMap map[int]string `msg:"loose_map,limit=40"` // Field limit (40) > file limit (20) + DefaultSlice []byte `msg:"default_slice"` // No field limit, uses file limit (30) + DefaultMap map[string]byte `msg:"default_map"` // No field limit, uses file limit (20) } diff --git a/_generated/noduplicates.go b/_generated/noduplicates.go index b5a4ca2d..f871d5bf 100644 --- a/_generated/noduplicates.go +++ b/_generated/noduplicates.go @@ -40,13 +40,13 @@ type NoDupMapInt map[string]int // NoDupRichTypes covers pointer, slice, bytes, float, and nested struct fields. type NoDupRichTypes struct { - Str string `msg:"str"` - Num float64 `msg:"num"` - Data []byte `msg:"data"` - PtrStr *string `msg:"ptr_str"` - Strings []string `msg:"strings"` - Inner NoDupInner `msg:"inner"` - PtrInner *NoDupInner `msg:"ptr_inner"` + Str string `msg:"str"` + Num float64 `msg:"num"` + Data []byte `msg:"data"` + PtrStr *string `msg:"ptr_str"` + Strings []string `msg:"strings"` + Inner NoDupInner `msg:"inner"` + PtrInner *NoDupInner `msg:"ptr_inner"` MapSlice map[string][]int `msg:"map_slice"` } diff --git a/_generated/noduplicates_binkey.go b/_generated/noduplicates_binkey.go index fbca67c8..bcfad98c 100644 --- a/_generated/noduplicates_binkey.go +++ b/_generated/noduplicates_binkey.go @@ -13,9 +13,9 @@ type NoDupBinKeyUint32 map[uint32]int // NoDupBinKeyStruct has both string-keyed and binary-keyed maps. type NoDupBinKeyStruct struct { - Name string `msg:"name"` - IntMap map[int]string `msg:"int_map"` - StrMap map[string]int `msg:"str_map"` + Name string `msg:"name"` + IntMap map[int]string `msg:"int_map"` + StrMap map[string]int `msg:"str_map"` } // NoDupBinKeyArray is a map with array keys. diff --git a/_generated/noduplicates_shim.go b/_generated/noduplicates_shim.go index c04c894c..484cec93 100644 --- a/_generated/noduplicates_shim.go +++ b/_generated/noduplicates_shim.go @@ -13,7 +13,7 @@ import "strings" type NoDupShimKey string func noDupShimEnc(k NoDupShimKey) string { return strings.ToLower(string(k)) } -func noDupShimDec(s string) NoDupShimKey { return NoDupShimKey(strings.ToLower(s)) } +func noDupShimDec(s string) NoDupShimKey { return NoDupShimKey(strings.ToLower(s)) } // NoDupShimMap is a map with shimmed keys that normalize to lowercase. // Two different wire strings (e.g. "Foo" and "foo") shim to the same key. diff --git a/_generated/noduplicates_test.go b/_generated/noduplicates_test.go index 919fb5d3..614d8527 100644 --- a/_generated/noduplicates_test.go +++ b/_generated/noduplicates_test.go @@ -600,6 +600,14 @@ func TestNoDupRichTypes_DupInsideNestedStruct(t *testing.T) { var out NoDupRichTypes _, err := out.UnmarshalMsg(buf) expectDup(t, err) + // Error should contain the full context path "Inner/X" + errStr := err.Error() + if !bytes.Contains([]byte(errStr), []byte("Inner")) { + t.Fatalf("error should reference 'Inner', got: %s", errStr) + } + if !bytes.Contains([]byte(errStr), []byte("X")) { + t.Fatalf("error should reference 'X', got: %s", errStr) + } } func TestNoDupRichTypes_DupPtrStructField(t *testing.T) { diff --git a/gen/decode.go b/gen/decode.go index 8712774a..246b5b4f 100644 --- a/gen/decode.go +++ b/gen/decode.go @@ -274,7 +274,7 @@ func (d *decodeGen) structAsMap(s *Struct) { byteIdx := i / 8 bitIdx := uint(i % 8) d.p.printf("\nif %s[%d] & (1<<%d) != 0 {", dupVar, byteIdx, bitIdx) - d.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %q)", s.Fields[i].FieldTag) + d.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s)", d.ctx.ArgsStr()) d.p.printf("\nreturn") d.p.printf("\n}") d.p.printf("\n%s[%d] |= 1<<%d", dupVar, byteIdx, bitIdx) diff --git a/gen/unmarshal.go b/gen/unmarshal.go index 18588529..8f30edb6 100644 --- a/gen/unmarshal.go +++ b/gen/unmarshal.go @@ -336,7 +336,7 @@ func (u *unmarshalGen) mapstruct(s *Struct) { byteIdx := i / 8 bitIdx := uint(i % 8) u.p.printf("\nif %s[%d] & (1<<%d) != 0 {", dupVar, byteIdx, bitIdx) - u.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %q)", s.Fields[i].FieldTag) + u.p.printf("\nerr = msgp.WrapError(msgp.ErrDuplicateEntry, %s)", u.ctx.ArgsStr()) u.p.printf("\nreturn") u.p.printf("\n}") u.p.printf("\n%s[%d] |= 1<<%d", dupVar, byteIdx, bitIdx)