You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
157 lines
4.8 KiB
157 lines
4.8 KiB
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)
|
|
}
|
|
})
|
|
}
|