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.
175 lines
5.3 KiB
175 lines
5.3 KiB
package codec_test
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"math/rand/v2"
|
|
"strings"
|
|
"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", ""},
|
|
} {
|
|
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)
|
|
}
|
|
})
|
|
}
|