package codec_test import ( "encoding/hex" "errors" "math/rand/v2" "testing" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/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", ""}, // Outside the CBOR profile of spec ยง58. {"negative integer", "a3006178012002" + "4101"}, {"float", "a300617801f9400002" + "4101"}, {"true", "a300617801f502" + "4101"}, {"null byte string", "a30061780117" + "02f6"}, {"undefined byte string", "a30061780117" + "02f7"}, {"null text", "a300f60117" + "024101"}, {"text map key", "a300617801176162" + "4101"}, } { 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 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) } } } // FuzzUnmarshal: whatever Unmarshal accepts is the deterministic encoding of // the decoded value. func FuzzUnmarshal(f *testing.F) { for _, h := range []string{"a400617801170241010a82011901f4", "a30061780117024101", "a3006178011702f6", "9f01ff"} { b, _ := hex.DecodeString(h) f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { var s sample if err := codec.Unmarshal(b, &s); err != nil { if !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("error without ErrNonCanonicalCBOR: %v", err) } return } re, err := codec.Marshal(s) if err != nil || string(re) != string(b) { t.Fatalf("accepted a non-canonical item %x", b) } }) }