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 new file mode 100644 index 00000000..f871d5bf --- /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..bcfad98c --- /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..484cec93 --- /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..614d8527 --- /dev/null +++ b/_generated/noduplicates_test.go @@ -0,0 +1,1196 @@ +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) + // 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) { + 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..246b5b4f 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, %s)", d.ctx.ArgsStr()) + 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..8f30edb6 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, %s)", u.ctx.ArgsStr()) + 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 {