package codec_test import ( "encoding/hex" "errors" "math/rand/v2" "strings" "testing" datekeys "github.com/datekeys/datekeys-go" "github.com/datekeys/datekeys-go/codec" ) type sample struct { Type string `cbor:"0,keyasint"` N uint64 `cbor:"1,keyasint"` Bytes []byte `cbor:"2,keyasint"` List []uint64 `cbor:"10,keyasint,omitempty"` } func mustHex(t *testing.T, s string) []byte { t.Helper() b, err := hex.DecodeString(s) if err != nil { t.Fatal(err) } return b } func TestMarshalIsCoreDeterministic(t *testing.T) { b, err := codec.Marshal(sample{Type: "x", N: 23, Bytes: []byte{1}, List: []uint64{1, 500}}) if err != nil { t.Fatal(err) } // {0: "x", 1: 23, 2: h'01', 10: [1, 500]} with keys sorted and shortest integers. if got, want := hex.EncodeToString(b), "a400617801170241010a82011901f4"; got != want { t.Fatalf("got %s, want %s", got, want) } var s sample if err := codec.Unmarshal(b, &s); err != nil { t.Fatal(err) } } func TestUnmarshalRejectsNonCanonical(t *testing.T) { for _, tc := range []struct{ name, hex string }{ {"integer not in shortest form", "a300617801181702410" + "1"}, {"keys out of order", "a301170061780241" + "01"}, {"duplicate key", "a4006178006179011702" + "4101"}, {"indefinite-length map", "bf00617801170241" + "01ff"}, {"indefinite-length byte string", "a3006178011702" + "5f4101ff"}, {"tag", "a3006178011702" + "c24101"}, {"unknown key", "a4006178011702410103" + "00"}, {"missing key", "a2006178011" + "7"}, {"trailing byte", "a30061780117024101" + "00"}, {"invalid UTF-8", "a30061ff0117024101"}, {"empty optional array present", "a400617801170241010a" + "80"}, {"wrong type", "a300617801617a024101"}, {"not a map", "83006178" + "01"}, {"empty input", ""}, } { t.Run(tc.name, func(t *testing.T) { var s sample err := codec.Unmarshal(mustHex(t, tc.hex), &s) if !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("got %v, want ErrNonCanonicalCBOR", err) } }) } } func TestValid(t *testing.T) { for _, h := range []string{ "00", "17", "1818", "20", "3bffffffffffffffff", "40", "60", "80", "a0", "f4", "f5", "f6", "f97e00", "f93c00", "fa47c35000", "a2016161026162", "a1416101", "8201820203", } { if err := codec.Valid(mustHex(t, h)); err != nil { t.Errorf("%s rejected: %v", h, err) } } for _, h := range []string{ "1817", // 23 encoded in two bytes "f7", // undefined "fb3ff0000000000000", // 1.0 as float64 instead of float16 "fa7fc00000", // NaN not in the canonical f97e00 form "a2026162016161", // keys out of order "c101", // tag "9f01ff", // indefinite-length array "a1810101", // array as map key "0000", // two items strings.Repeat("81", codec.MaxNestedLevels+4) + "00", // nesting beyond the limit } { if err := codec.Valid(mustHex(t, h)); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("%s accepted or wrong error: %v", h, err) } } } func TestCheckSchema(t *testing.T) { b, _ := codec.Marshal(sample{Type: "datekeycap", N: 1, Bytes: []byte{}}) if err := codec.CheckSchema(b, "datekeycap", 1); err != nil { t.Fatal(err) } if err := codec.CheckSchema(b, "datekeys-control", 1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("type confusion: %v", err) } if err := codec.CheckSchema(b, "datekeycap", 2); !errors.Is(err, datekeys.ErrUnsupportedVersion) { t.Fatalf("version: %v", err) } // A future version with unknown keys still reports the version. future, _ := codec.Marshal(map[uint64]any{0: "datekeycap", 1: uint64(2), 99: "new"}) if err := codec.CheckSchema(future, "datekeycap", 1); !errors.Is(err, datekeys.ErrUnsupportedVersion) { t.Fatalf("future version: %v", err) } } func TestRoundTripProperty(t *testing.T) { r := rand.New(rand.NewPCG(1, 2)) for range 2000 { s := sample{Type: string(rune('a' + r.IntN(26))), N: r.Uint64() >> r.IntN(64), Bytes: make([]byte, r.IntN(40))} for range r.IntN(4) { s.List = append(s.List, r.Uint64()>>r.IntN(64)) } b, err := codec.Marshal(s) if err != nil { t.Fatal(err) } var got sample if err := codec.Unmarshal(b, &got); err != nil { t.Fatalf("%x: %v", b, err) } b2, _ := codec.Marshal(got) if string(b) != string(b2) { t.Fatal("encoding is not stable") } } } func TestErrorsCarryTheNormativeCode(t *testing.T) { if _, err := codec.Marshal(make(chan int)); err == nil { t.Fatal("encoded a channel") } for _, in := range [][]byte{nil, {0xff}, {0x83, 0x01}, mustHex(t, "a10061")} { if err := codec.CheckSchema(in, "datekeycap", 1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("%x: %v", in, err) } var v struct { A uint64 `cbor:"0,keyasint"` } if err := codec.Peek(in, &v); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("peek %x: %v", in, err) } } } func FuzzValid(f *testing.F) { for _, h := range []string{"a400617801170241010a82011901f4", "f97e00", "a2016161026162", "9f01ff"} { b, _ := hex.DecodeString(h) f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { if codec.Valid(b) != nil { return } var v any if err := codec.Unmarshal(b, &v); err != nil { t.Fatalf("Valid accepted what Unmarshal rejects: %v", err) } re, err := codec.Marshal(v) if err != nil || string(re) != string(b) { t.Fatalf("accepted a non-canonical item %x", b) } }) }