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.
898 lines
27 KiB
898 lines
27 KiB
package codec_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"math/rand/v2"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
)
|
|
|
|
func mustHex(t testing.TB, s string) []byte {
|
|
t.Helper()
|
|
b, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func isNonCanonical(err error) bool { return errors.Is(err, datekeys.ErrNonCanonicalCBOR) }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Encoder
|
|
|
|
func TestEncoderShortestForm(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
v uint64
|
|
want string
|
|
}{
|
|
{0, "00"}, {23, "17"}, {24, "1818"}, {255, "18ff"}, {256, "190100"},
|
|
{65535, "19ffff"}, {65536, "1a00010000"}, {1<<32 - 1, "1affffffff"},
|
|
{1 << 32, "1b0000000100000000"}, {math.MaxUint64, "1bffffffffffffffff"},
|
|
} {
|
|
var e codec.Encoder
|
|
e.Uint(tc.v)
|
|
if b, err := e.Out(); err != nil || hex.EncodeToString(b) != tc.want {
|
|
t.Errorf("Uint(%d) = %x %v, want %s", tc.v, b, err, tc.want)
|
|
}
|
|
}
|
|
// {0: "x", 1: 23, 2: h'01', 10: [1, 500], 11: h'', 12: ""}
|
|
var e codec.Encoder
|
|
e.Map(6)
|
|
e.Uint(0)
|
|
e.Text("x")
|
|
e.Uint(1)
|
|
e.Uint(23)
|
|
e.Uint(2)
|
|
e.Bstr([]byte{1})
|
|
e.Uint(10)
|
|
e.Array(2)
|
|
e.Uint(1)
|
|
e.Uint(500)
|
|
e.Uint(11)
|
|
e.Bstr(nil)
|
|
e.Uint(12)
|
|
e.Text("")
|
|
b, err := e.Out()
|
|
if want := "a600617801170241010a82011901f40b400c60"; err != nil || hex.EncodeToString(b) != want {
|
|
t.Fatalf("got %x %v, want %s", b, err, want)
|
|
}
|
|
long := strings.Repeat("a", 24)
|
|
var l codec.Encoder
|
|
l.Text(long)
|
|
l.Bstr([]byte(long))
|
|
l.Map(24)
|
|
l.Array(256)
|
|
s := hex.EncodeToString([]byte(long))
|
|
if b, _ := l.Out(); hex.EncodeToString(b) != "7818"+s+"5818"+s+"b818"+"990100" {
|
|
t.Fatalf("long heads %x", b)
|
|
}
|
|
}
|
|
|
|
func TestEncoderErrorsAreSticky(t *testing.T) {
|
|
for name, fail := range map[string]func(e *codec.Encoder){
|
|
"negative map": func(e *codec.Encoder) { e.Map(-1) },
|
|
"negative array": func(e *codec.Encoder) { e.Array(-1) },
|
|
"invalid UTF-8": func(e *codec.Encoder) { e.Text("\xff") },
|
|
"surrogate": func(e *codec.Encoder) { e.Text("\xed\xa0\x80") },
|
|
"overlong": func(e *codec.Encoder) { e.Text("\xc0\x80") },
|
|
"Fail": func(e *codec.Encoder) { e.Fail(fmt.Errorf("schema rule: %w", datekeys.ErrNonCanonicalCBOR)) },
|
|
"Fail twice": func(e *codec.Encoder) {
|
|
e.Fail(fmt.Errorf("first: %w", datekeys.ErrNonCanonicalCBOR))
|
|
e.Fail(errors.New("second"))
|
|
},
|
|
} {
|
|
var e codec.Encoder
|
|
e.Bstr([]byte("secret"))
|
|
fail(&e)
|
|
e.Uint(1)
|
|
e.Bstr([]byte{1})
|
|
e.Text("ok")
|
|
e.Map(1)
|
|
e.Array(1)
|
|
if b, err := e.Out(); b != nil || !isNonCanonical(err) {
|
|
t.Errorf("%s: %x %v", name, b, err)
|
|
}
|
|
}
|
|
// A nil error records nothing.
|
|
var e codec.Encoder
|
|
e.Fail(nil)
|
|
e.Uint(1)
|
|
if b, err := e.Out(); err != nil || !bytes.Equal(b, []byte{0x01}) {
|
|
t.Errorf("Fail(nil): %x %v", b, err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Decoder
|
|
|
|
// run interprets prog, a space-separated list of Decoder calls, on in and
|
|
// returns the first error, after checking that it is sticky:
|
|
//
|
|
// m<max> Map k Key e EndMap a<max> Array u[<max>] Uint
|
|
// b<min>,<max> Bstr t<max> Text d Done
|
|
func run(in []byte, prog string) error {
|
|
d := codec.NewDecoder(in)
|
|
num := func(s string) int { n, _ := strconv.Atoi(s); return n }
|
|
var first error
|
|
for _, op := range strings.Fields(prog) {
|
|
arg := op[1:]
|
|
var err error
|
|
switch op[0] {
|
|
case 'm':
|
|
_, err = d.Map(num(arg))
|
|
case 'k':
|
|
_, err = d.Key()
|
|
case 'e':
|
|
err = d.EndMap()
|
|
case 'a':
|
|
_, err = d.Array(num(arg))
|
|
case 'u':
|
|
max := uint64(math.MaxUint64)
|
|
if arg != "" {
|
|
max, _ = strconv.ParseUint(arg, 10, 64)
|
|
}
|
|
_, err = d.Uint(max)
|
|
case 'b':
|
|
lo, hi, _ := strings.Cut(arg, ",")
|
|
_, err = d.Bstr(num(lo), num(hi))
|
|
case 't':
|
|
_, err = d.Text(num(arg))
|
|
case 'd':
|
|
err = d.Done()
|
|
default:
|
|
panic("bad op " + op)
|
|
}
|
|
if first == nil {
|
|
first = err
|
|
} else if err != first {
|
|
return fmt.Errorf("error not sticky: %v then %v", first, err)
|
|
}
|
|
}
|
|
return first
|
|
}
|
|
|
|
func TestDecoderAccepts(t *testing.T) {
|
|
for _, tc := range []struct{ name, hex, prog string }{
|
|
{"uint 23 inline", "17", "u23 d"},
|
|
{"uint 24 one byte", "1818", "u24 d"},
|
|
{"uint 256 two bytes", "190100", "u d"},
|
|
{"uint 65536 four bytes", "1a00010000", "u d"},
|
|
{"uint 2^32 eight bytes", "1b0000000100000000", "u d"},
|
|
{"uint 2^64-1", "1bffffffffffffffff", "u d"},
|
|
{"empty bstr", "40", "b0,0 d"},
|
|
{"bstr at its bounds", "420102", "b2,2 d"},
|
|
{"bstr 24 bytes", "5818" + strings.Repeat("00", 24), "b0,24 d"},
|
|
{"empty text", "60", "t0 d"},
|
|
{"text with leading BOM", "64efbbbf61", "t4 d"},
|
|
{"text U+10FFFF", "64f48fbfbf", "t4 d"},
|
|
{"empty map", "a0", "m0 e d"},
|
|
{"map two sorted keys", "a200010101", "m2 k u k u e d"},
|
|
{"map keys 0 and 2^64-1", "a200001bffffffffffffffff00", "m2 k u k u e d"},
|
|
{"empty array", "80", "a0 d"},
|
|
{"array of maps", "82a10000a10101", "a2 m1 k u e m1 k u e d"},
|
|
{"nested maps", "a100a10000", "m1 k m1 k u e e d"},
|
|
} {
|
|
if err := run(mustHex(t, tc.hex), tc.prog); err != nil {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecoderRejects(t *testing.T) {
|
|
for _, tc := range []struct{ name, hex, prog string }{
|
|
// Outside the profile of spec §58.
|
|
{"negative int", "20", "u"},
|
|
{"tag", "c101", "u"},
|
|
{"tag on a byte string", "c24101", "b0,9"},
|
|
{"half float", "f97e00", "u"},
|
|
{"single float", "fa3f800000", "u"},
|
|
{"double float", "fb3ff0000000000000", "u"},
|
|
{"false", "f4", "u"},
|
|
{"true", "f5", "u"},
|
|
{"null", "f6", "b0,9"},
|
|
{"undefined", "f7", "t9"},
|
|
{"break", "ff", "u"},
|
|
{"indefinite array", "9f01ff", "a9"},
|
|
{"indefinite map", "bf0001ff", "m9"},
|
|
{"indefinite byte string", "5f4101ff", "b0,9"},
|
|
{"indefinite text", "7f6161ff", "t9"},
|
|
{"reserved 28", "1c", "u"},
|
|
{"reserved 29", "1d", "u"},
|
|
{"reserved 30", "1e", "u"},
|
|
// Shortest form.
|
|
{"uint 23 with one extra byte", "1817", "u"},
|
|
{"uint 255 in two bytes", "1900ff", "u"},
|
|
{"uint 65535 in four bytes", "1a0000ffff", "u"},
|
|
{"uint 2^32-1 in eight bytes", "1b00000000ffffffff", "u"},
|
|
{"bstr length not shortest", "5800", "b0,9"},
|
|
{"map length not shortest", "b800", "m9"},
|
|
{"key not shortest", "a1180000", "m1 k"},
|
|
// Truncation.
|
|
{"empty input", "", "u"},
|
|
{"truncated uint", "1901", "u"},
|
|
{"truncated uint 8", "1b00000000000000", "u"},
|
|
{"length beyond input", "5affffffff", "b0,9"},
|
|
{"bstr shorter than its length", "4300", "b0,9"},
|
|
{"text shorter than its length", "6361", "t9"},
|
|
{"map beyond input", "a300", "m9"},
|
|
{"huge map", "bbffffffffffffffff", "m100"},
|
|
{"array beyond input", "8300", "a9"},
|
|
{"huge array", "9b7fffffffffffffff", "a100"},
|
|
{"truncated inside a map", "a200", "m2 k u"},
|
|
// Types and bounds.
|
|
{"bstr where uint", "4100", "u"},
|
|
{"uint where bstr", "00", "b0,9"},
|
|
{"text where bstr", "6161", "b0,9"},
|
|
{"bstr where text", "4161", "t9"},
|
|
{"array where map", "80", "m9"},
|
|
{"map where array", "a0", "a9"},
|
|
{"uint above max", "1818", "u23"},
|
|
{"bstr below min", "4101", "b2,9"},
|
|
{"bstr above max", "420102", "b0,1"},
|
|
{"text above max", "626161", "t1"},
|
|
{"map above max", "a200010101", "m1"},
|
|
{"negative map max", "a0", "m-1"},
|
|
{"array above max", "820101", "a1"},
|
|
{"negative array max", "80", "a-1"},
|
|
// Keys.
|
|
{"keys out of order", "a201000001", "m2 k u k"},
|
|
{"duplicate key", "a200000001", "m2 k u k"},
|
|
{"text key", "a1616100", "m1 k"},
|
|
{"bstr key", "a1416100", "m1 k"},
|
|
{"negative key", "a12000", "m1 k"},
|
|
{"key outside a map", "00", "k"},
|
|
{"key after the last entry", "a10000", "m1 k u k"},
|
|
{"key after the map closed", "a0", "m0 e k"},
|
|
{"end outside a map", "00", "e"},
|
|
{"end with entries left", "a10000", "m1 e"},
|
|
{"done with a map open", "a0", "m0 d"},
|
|
// Trailing bytes and UTF-8.
|
|
{"trailing byte", "0100", "u d"},
|
|
{"invalid UTF-8", "61ff", "t9"},
|
|
{"overlong UTF-8", "62c080", "t9"},
|
|
{"UTF-8 surrogate", "63eda080", "t9"},
|
|
{"above U+10FFFF", "64f4908080", "t9"},
|
|
{"truncated UTF-8", "62e282", "t9"},
|
|
// The first error stays.
|
|
{"sticky after a failed read", "f600", "u u d"},
|
|
} {
|
|
if err := run(mustHex(t, tc.hex), tc.prog); !isNonCanonical(err) {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecoderCopiesByteStrings(t *testing.T) {
|
|
in := mustHex(t, "43010203")
|
|
d := codec.NewDecoder(in)
|
|
b, err := d.Bstr(3, 3)
|
|
if err != nil || d.Done() != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in[1] = 9
|
|
if !bytes.Equal(b, []byte{1, 2, 3}) {
|
|
t.Fatal("Bstr returned a view of its input")
|
|
}
|
|
d = codec.NewDecoder([]byte{0x40})
|
|
if b, err := d.Bstr(0, 0); err != nil || b == nil || len(b) != 0 {
|
|
t.Fatalf("empty byte string: %v %v", b, err)
|
|
}
|
|
}
|
|
|
|
func TestDecoderErrorsNameTheOffset(t *testing.T) {
|
|
err := run(mustHex(t, "a2 00 01 00 02"), "m2 k u k")
|
|
if err == nil || !strings.Contains(err.Error(), "offset 3") || !strings.Contains(err.Error(), "key 0 after key 0") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Unmarshal
|
|
|
|
// sample is a schema of this test: {0: tstr, 1: uint, 2: bstr, ? 10: [* uint]}.
|
|
// Its decoder accepts an empty list at key 10 and its encoder omits an empty
|
|
// list, so an empty list that is present is left to the re-encoding check.
|
|
type sample struct {
|
|
Type string
|
|
N uint64
|
|
Bytes []byte
|
|
List []uint64
|
|
}
|
|
|
|
func (s *sample) decode(d *codec.Decoder) error {
|
|
pairs, err := d.Map(4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for range pairs {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch k {
|
|
case 0:
|
|
s.Type, err = d.Text(16)
|
|
case 1:
|
|
s.N, err = d.Uint(math.MaxUint64)
|
|
case 2:
|
|
s.Bytes, err = d.Bstr(0, 64)
|
|
case 10:
|
|
var n int
|
|
if n, err = d.Array(8); err == nil {
|
|
s.List = make([]uint64, n)
|
|
for i := range s.List {
|
|
if s.List[i], err = d.Uint(math.MaxUint64); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown key %d: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if s.Type == "" || s.Bytes == nil {
|
|
return fmt.Errorf("missing key: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
return d.EndMap()
|
|
}
|
|
|
|
func (s *sample) encode(e *codec.Encoder) {
|
|
pairs := 3
|
|
if len(s.List) > 0 {
|
|
pairs++
|
|
}
|
|
e.Map(pairs)
|
|
e.Uint(0)
|
|
e.Text(s.Type)
|
|
e.Uint(1)
|
|
e.Uint(s.N)
|
|
e.Uint(2)
|
|
e.Bstr(s.Bytes)
|
|
if len(s.List) > 0 {
|
|
e.Uint(10)
|
|
e.Array(len(s.List))
|
|
for _, v := range s.List {
|
|
e.Uint(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *sample) marshal() ([]byte, error) {
|
|
var e codec.Encoder
|
|
s.encode(&e)
|
|
return e.Out()
|
|
}
|
|
|
|
func unmarshal(in []byte) (*sample, error) {
|
|
s := &sample{}
|
|
return s, codec.Unmarshal(in, s.decode, s.encode)
|
|
}
|
|
|
|
func TestUnmarshal(t *testing.T) {
|
|
s, err := unmarshal(mustHex(t, "a400617801170241010a82011901f4"))
|
|
if err != nil || s.Type != "x" || s.N != 23 || !bytes.Equal(s.Bytes, []byte{1}) || !slices.Equal(s.List, []uint64{1, 500}) {
|
|
t.Fatalf("%+v %v", s, 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) {
|
|
if _, err := unmarshal(mustHex(t, tc.hex)); !isNonCanonical(err) {
|
|
t.Fatalf("got %v, want ErrNonCanonicalCBOR", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalChecksTheReEncoding(t *testing.T) {
|
|
in := mustHex(t, "a30061780117024101")
|
|
// An error of the schema is returned as it is.
|
|
own := errors.New("schema error")
|
|
if err := codec.Unmarshal(in, func(*codec.Decoder) error { return own }, func(*codec.Encoder) {}); err != own {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
// A decoder that stops early leaves bytes behind.
|
|
stop := func(d *codec.Decoder) error { _, err := d.Map(3); return err }
|
|
if err := codec.Unmarshal(in, stop, func(*codec.Encoder) {}); !isNonCanonical(err) {
|
|
t.Fatalf("partial decode: %v", err)
|
|
}
|
|
// A decoder that ignores an error of the Decoder fails anyway.
|
|
ignore := func(d *codec.Decoder) error { _, _ = d.Uint(0); return nil }
|
|
if err := codec.Unmarshal(in, ignore, func(*codec.Encoder) {}); !isNonCanonical(err) {
|
|
t.Fatalf("ignored error: %v", err)
|
|
}
|
|
// An encoder that fails, and one that writes something else.
|
|
s := &sample{}
|
|
if err := codec.Unmarshal(in, s.decode, func(e *codec.Encoder) { e.Map(-1) }); !isNonCanonical(err) {
|
|
t.Fatalf("encode error: %v", err)
|
|
}
|
|
if err := codec.Unmarshal(in, s.decode, func(e *codec.Encoder) { s.encode(e); e.Uint(0) }); !isNonCanonical(err) {
|
|
t.Fatalf("different re-encoding: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Peek and CheckSchema
|
|
|
|
func TestPeek(t *testing.T) {
|
|
// Nothing after key 1 is read: a future version may use anything there.
|
|
future, _ := cbortest.Marshal(map[uint64]any{0: "datekeycap", 1: uint64(2), 99: "new", 100: cbortest.Raw{0xf9, 0x7e, 0x00}})
|
|
tag, v, err := codec.Peek(future)
|
|
if err != nil || tag != "datekeycap" || v != 2 {
|
|
t.Fatalf("%q %d %v", tag, v, err)
|
|
}
|
|
long := strings.Repeat("a", codec.MaxTypeTagLen)
|
|
if tag, _, err := codec.Peek(mustHex(t, "a200"+"7840"+hex.EncodeToString([]byte(long))+"0101")); err != nil || tag != long {
|
|
t.Fatalf("type tag of MaxTypeTagLen bytes: %q %v", tag, err)
|
|
}
|
|
for _, tc := range []struct{ name, hex string }{
|
|
{"empty", ""},
|
|
{"not a map", "8200"},
|
|
{"one entry", "a1006161"},
|
|
{"first key not 0", "a2016161" + "0201"},
|
|
{"second key not 1", "a2006161" + "0201"},
|
|
{"text key", "a2616100" + "0101"},
|
|
{"type tag not text", "a2004161" + "0101"},
|
|
{"version not uint", "a2006161" + "0120"},
|
|
{"version above 2^53-1", "a2006161" + "011b0020000000000000"},
|
|
{"keys swapped", "a2010100" + "6161"},
|
|
{"truncated type tag", "a2006361"},
|
|
{"map beyond input", "a5006161" + "0101"},
|
|
{"type tag above MaxTypeTagLen", "a200" + "7841" + strings.Repeat("61", codec.MaxTypeTagLen+1) + "0101"},
|
|
} {
|
|
if _, _, err := codec.Peek(mustHex(t, tc.hex)); !isNonCanonical(err) {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckSchema(t *testing.T) {
|
|
b, _ := (&sample{Type: "datekeycap", N: 1, Bytes: []byte{}}).marshal()
|
|
if err := codec.CheckSchema(b, "datekeycap", 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := codec.CheckSchema(b, "datekeys-control", 1); !isNonCanonical(err) {
|
|
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, _ := cbortest.Marshal(map[uint64]any{0: "datekeycap", 1: uint64(2), 99: int64(-7)})
|
|
if err := codec.CheckSchema(future, "datekeycap", 1); !errors.Is(err, datekeys.ErrUnsupportedVersion) {
|
|
t.Fatalf("future version: %v", err)
|
|
}
|
|
for _, in := range [][]byte{nil, {0xff}, {0x83, 0x01}, mustHex(t, "a10061")} {
|
|
if err := codec.CheckSchema(in, "datekeycap", 1); !isNonCanonical(err) {
|
|
t.Fatalf("%x: %v", in, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spec §70: a version is read only as the second key, after a type tag, both
|
|
// in the profile; every other form of the version is ErrNonCanonicalCBOR,
|
|
// whatever its value.
|
|
func TestCheckSchemaVersionForms(t *testing.T) {
|
|
text := hex.EncodeToString([]byte("datekeycap"))
|
|
tag := "6a" + text
|
|
for _, tc := range []struct {
|
|
name, hex string
|
|
want error
|
|
}{
|
|
{"version 2", "a200" + tag + "0102", datekeys.ErrUnsupportedVersion},
|
|
{"version 2, rest malformed", "a300" + tag + "0102" + "02ff", datekeys.ErrUnsupportedVersion},
|
|
{"version 2^53-1", "a200" + tag + "011b001fffffffffffff", datekeys.ErrUnsupportedVersion},
|
|
{"version 2 not in shortest form", "a200" + tag + "011802", datekeys.ErrNonCanonicalCBOR},
|
|
{"version 2^53", "a200" + tag + "011b0020000000000000", datekeys.ErrNonCanonicalCBOR},
|
|
{"version 2^64-1", "a200" + tag + "011bffffffffffffffff", datekeys.ErrNonCanonicalCBOR},
|
|
{"version before key 0", "a2" + "0102" + "00" + tag, datekeys.ErrNonCanonicalCBOR},
|
|
{"version after key 2", "a3" + "00" + tag + "0200" + "0102", datekeys.ErrNonCanonicalCBOR},
|
|
{"map head not in shortest form", "b802" + "00" + tag + "0102", datekeys.ErrNonCanonicalCBOR},
|
|
{"type tag head not in shortest form", "a200" + "780a" + text + "0102", datekeys.ErrNonCanonicalCBOR},
|
|
{"version missing", "a100" + tag, datekeys.ErrNonCanonicalCBOR},
|
|
{"version null", "a200" + tag + "01f6", datekeys.ErrNonCanonicalCBOR},
|
|
{"version undefined", "a200" + tag + "01f7", datekeys.ErrNonCanonicalCBOR},
|
|
{"version true", "a200" + tag + "01f5", datekeys.ErrNonCanonicalCBOR},
|
|
{"version false", "a200" + tag + "01f4", datekeys.ErrNonCanonicalCBOR},
|
|
} {
|
|
if err := codec.CheckSchema(mustHex(t, tc.hex), "datekeycap", 1); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Walk
|
|
|
|
func TestWalk(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, hex string
|
|
maxDepth, max int
|
|
ok bool
|
|
}{
|
|
{"uint", "17", 0, 0, true},
|
|
{"empty bstr", "40", 0, 0, true},
|
|
{"text", "626161", 0, 2, true},
|
|
{"text above max", "626161", 0, 1, false},
|
|
{"bstr above max", "420000", 0, 1, false},
|
|
{"map two sorted keys", "a200010101", 1, 2, true},
|
|
{"map at depth 0", "a0", 0, 0, false},
|
|
{"map above max", "a200010101", 1, 1, false},
|
|
{"array above max", "83010203", 1, 2, false},
|
|
{"four levels", "a1008181a10040", 4, 1, true},
|
|
{"four levels, depth 3", "a1008181a10040", 3, 1, false},
|
|
{"empty containers", "82a080", 2, 2, true},
|
|
{"keys out of order", "a201000001", 1, 2, false},
|
|
{"text key", "a1616100", 1, 1, false},
|
|
{"float inside", "8201f97e00", 1, 2, false},
|
|
{"truncated map", "a20001", 1, 2, false},
|
|
{"truncated array", "8201", 1, 2, false},
|
|
{"trailing byte", "8000", 1, 0, false},
|
|
{"empty input", "", 1, 1, false},
|
|
{"invalid UTF-8 inside", "a10061ff", 1, 1, false},
|
|
{"null", "f6", 1, 1, false},
|
|
} {
|
|
err := codec.Walk(mustHex(t, tc.hex), tc.maxDepth, tc.max)
|
|
if (err == nil) != tc.ok || (err != nil && !isNonCanonical(err)) {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWalkDeepInputDoesNotExhaustTheStack(t *testing.T) {
|
|
const n = 1 << 20
|
|
in := append(bytes.Repeat([]byte{0x81}, n), 0x00)
|
|
if err := codec.Walk(in, n, 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := codec.Walk(in, n-1, 1); !isNonCanonical(err) {
|
|
t.Fatalf("depth limit: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Properties
|
|
|
|
// randomValue returns a value of the profile, in the types of cbortest.
|
|
func randomValue(r *rand.Rand, depth int) any {
|
|
switch k := r.IntN(6); {
|
|
case k == 0 && depth < 4:
|
|
v := make([]any, r.IntN(4))
|
|
for i := range v {
|
|
v[i] = randomValue(r, depth+1)
|
|
}
|
|
return v
|
|
case k == 1 && depth < 4:
|
|
m := map[uint64]any{}
|
|
for range r.IntN(4) {
|
|
m[r.Uint64()>>r.IntN(64)] = randomValue(r, depth+1)
|
|
}
|
|
return m
|
|
case k == 2:
|
|
return make([]byte, r.IntN(30))
|
|
case k == 3:
|
|
return strings.Repeat("é", r.IntN(20))
|
|
}
|
|
return r.Uint64() >> r.IntN(64)
|
|
}
|
|
|
|
// shape returns the nesting depth of v and the length of its longest string
|
|
// or container.
|
|
func shape(v any) (depth, length int) {
|
|
var items []any
|
|
switch v := v.(type) {
|
|
case []byte:
|
|
return 0, len(v)
|
|
case string:
|
|
return 0, len(v)
|
|
case []any:
|
|
items = v
|
|
case map[uint64]any:
|
|
items = slices.Collect(maps.Values(v))
|
|
default:
|
|
return 0, 0
|
|
}
|
|
length = len(items)
|
|
for _, x := range items {
|
|
d, l := shape(x)
|
|
depth, length = max(depth, d), max(length, l)
|
|
}
|
|
return depth + 1, length
|
|
}
|
|
|
|
// encode writes v, a value of the profile, with the Encoder.
|
|
func encode(e *codec.Encoder, v any) {
|
|
switch v := v.(type) {
|
|
case uint64:
|
|
e.Uint(v)
|
|
case []byte:
|
|
e.Bstr(v)
|
|
case string:
|
|
e.Text(v)
|
|
case []any:
|
|
e.Array(len(v))
|
|
for _, x := range v {
|
|
encode(e, x)
|
|
}
|
|
case map[uint64]any:
|
|
e.Map(len(v))
|
|
for _, k := range slices.Sorted(maps.Keys(v)) {
|
|
e.Uint(k)
|
|
encode(e, v[k])
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("%T", v))
|
|
}
|
|
}
|
|
|
|
// The Encoder writes what the independent encoder of cbortest writes, and
|
|
// Walk accepts it within its exact shape and rejects it one level or one
|
|
// byte tighter.
|
|
func TestEncoderAndWalkAgreeWithAReference(t *testing.T) {
|
|
r := rand.New(rand.NewPCG(1, 2))
|
|
for range 3000 {
|
|
v := randomValue(r, 0)
|
|
ref, err := cbortest.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var e codec.Encoder
|
|
encode(&e, v)
|
|
b, err := e.Out()
|
|
if err != nil || !bytes.Equal(b, ref) {
|
|
t.Fatalf("Encoder %x, reference %x: %v", b, ref, err)
|
|
}
|
|
depth, length := shape(v)
|
|
if err := codec.Walk(b, depth, length); err != nil {
|
|
t.Fatalf("%x: %v", b, err)
|
|
}
|
|
if depth > 0 && codec.Walk(b, depth-1, length) == nil {
|
|
t.Fatalf("%x accepted at depth %d", b, depth-1)
|
|
}
|
|
if length > 0 && codec.Walk(b, depth, length-1) == nil {
|
|
t.Fatalf("%x accepted with length %d", b, length-1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalRoundTripProperty(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 := s.marshal()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := unmarshal(b)
|
|
if err != nil {
|
|
t.Fatalf("%x: %v", b, err)
|
|
}
|
|
if b2, _ := got.marshal(); !bytes.Equal(b, b2) {
|
|
t.Fatal("encoding is not stable")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fuzzing
|
|
|
|
func addSeeds(f *testing.F) {
|
|
for _, h := range []string{
|
|
"a400617801170241010a82011901f4", "a30061780117024101", "a3006178011702f6", "9f01ff",
|
|
"a200010101", "a1008181a10040", "64efbbbf61", "1bffffffffffffffff",
|
|
"a2006a646174656b657963617001" + "01",
|
|
} {
|
|
f.Add(mustHex(f, h))
|
|
}
|
|
}
|
|
|
|
// FuzzUnmarshal: whatever Unmarshal accepts is the deterministic encoding of
|
|
// the decoded value and an item of the profile, and every error carries
|
|
// ErrNonCanonicalCBOR.
|
|
func FuzzUnmarshal(f *testing.F) {
|
|
addSeeds(f)
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
s, err := unmarshal(b)
|
|
if err != nil {
|
|
if !isNonCanonical(err) {
|
|
t.Fatalf("error without ErrNonCanonicalCBOR: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if re, err := s.marshal(); err != nil || !bytes.Equal(re, b) {
|
|
t.Fatalf("accepted a non-canonical item %x", b)
|
|
}
|
|
if err := codec.Walk(b, 2, 64); err != nil {
|
|
t.Fatalf("Walk rejects %x: %v", b, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzDecoder drives the Decoder primitives with a program, the first input,
|
|
// over the second input: no call panics, every error carries
|
|
// ErrNonCanonicalCBOR and is sticky, and every value is within the bounds
|
|
// asked for.
|
|
func FuzzDecoder(f *testing.F) {
|
|
f.Add([]byte{0, 2, 1, 4, 9, 1, 4, 9, 2, 7}, mustHex(f, "a200010101"))
|
|
f.Add([]byte{3, 4, 0, 1, 1, 5, 4, 2, 7}, mustHex(f, "81a10040"))
|
|
f.Add([]byte{6, 9, 7}, mustHex(f, "6461626364"))
|
|
f.Fuzz(func(t *testing.T, prog, in []byte) {
|
|
d := codec.NewDecoder(in)
|
|
var first error
|
|
for i := 0; i < len(prog); i++ {
|
|
bound := 0
|
|
if i+1 < len(prog) {
|
|
bound = int(prog[i+1])
|
|
}
|
|
var err error
|
|
switch prog[i] % 8 {
|
|
case 0:
|
|
var n int
|
|
if n, err = d.Map(bound); err == nil && n > bound {
|
|
t.Fatalf("Map(%d) = %d", bound, n)
|
|
}
|
|
i++
|
|
case 1:
|
|
_, err = d.Key()
|
|
case 2:
|
|
err = d.EndMap()
|
|
case 3:
|
|
var n int
|
|
if n, err = d.Array(bound); err == nil && n > bound {
|
|
t.Fatalf("Array(%d) = %d", bound, n)
|
|
}
|
|
i++
|
|
case 4:
|
|
var v uint64
|
|
if v, err = d.Uint(uint64(bound)); err == nil && v > uint64(bound) {
|
|
t.Fatalf("Uint(%d) = %d", bound, v)
|
|
}
|
|
i++
|
|
case 5:
|
|
var b []byte
|
|
lo := bound % 16
|
|
if b, err = d.Bstr(lo, bound); err == nil && (len(b) < lo || len(b) > bound) {
|
|
t.Fatalf("Bstr(%d, %d) = %d bytes", lo, bound, len(b))
|
|
}
|
|
i++
|
|
case 6:
|
|
var s string
|
|
if s, err = d.Text(bound); err == nil && len(s) > bound {
|
|
t.Fatalf("Text(%d) = %d bytes", bound, len(s))
|
|
}
|
|
i++
|
|
case 7:
|
|
err = d.Done()
|
|
}
|
|
if err != nil && !isNonCanonical(err) {
|
|
t.Fatalf("error without ErrNonCanonicalCBOR: %v", err)
|
|
}
|
|
if first == nil {
|
|
first = err
|
|
} else if err != first {
|
|
t.Fatalf("error not sticky: %v then %v", first, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzWalk compares Walk with the independent decoder of cbortest: Walk
|
|
// accepts exactly the items of the profile whose shape fits its bounds.
|
|
func FuzzWalk(f *testing.F) {
|
|
addSeeds(f)
|
|
f.Fuzz(func(t *testing.T, in []byte) {
|
|
err := codec.Walk(in, 8, 64)
|
|
if err != nil && !isNonCanonical(err) {
|
|
t.Fatalf("error without ErrNonCanonicalCBOR: %v", err)
|
|
}
|
|
v, refErr := cbortest.Unmarshal(in)
|
|
if refErr != nil {
|
|
if err == nil {
|
|
t.Fatalf("Walk accepts %x, the reference rejects it: %v", in, refErr)
|
|
}
|
|
return
|
|
}
|
|
depth, length := shape(v)
|
|
if fits := depth <= 8 && length <= 64; fits != (err == nil) {
|
|
t.Fatalf("%x of depth %d and length %d: Walk %v", in, depth, length, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzPeek: Peek never panics, its errors carry ErrNonCanonicalCBOR, and on
|
|
// an item of the profile it reads what the reference decoder reads.
|
|
func FuzzPeek(f *testing.F) {
|
|
addSeeds(f)
|
|
f.Fuzz(func(t *testing.T, in []byte) {
|
|
tag, version, err := codec.Peek(in)
|
|
if err != nil && !isNonCanonical(err) {
|
|
t.Fatalf("error without ErrNonCanonicalCBOR: %v", err)
|
|
}
|
|
m, refErr := cbortest.UnmarshalMap(in)
|
|
if refErr != nil {
|
|
return
|
|
}
|
|
// Keys ascend, so keys 0 and 1, when present, come first.
|
|
refTag, okTag := m[0].(string)
|
|
refVersion, okVersion := m[1].(uint64)
|
|
want := okTag && len(refTag) <= codec.MaxTypeTagLen && okVersion && refVersion <= codec.MaxSafeUint
|
|
if want != (err == nil) || (err == nil && (tag != refTag || version != refVersion)) {
|
|
t.Fatalf("%x: Peek %q %d %v", in, tag, version, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzEncodeImpliesWalk writes a value of the profile built from the inputs
|
|
// with the Encoder, and requires that Walk and the reference decoder accept
|
|
// it and that the reference reads the same value back.
|
|
func FuzzEncodeImpliesWalk(f *testing.F) {
|
|
f.Add(uint64(1), []byte{1, 2, 3})
|
|
f.Add(uint64(99), []byte("datekeys"))
|
|
f.Fuzz(func(t *testing.T, seed uint64, data []byte) {
|
|
r := rand.New(rand.NewPCG(seed, uint64(len(data))))
|
|
v := randomValue(r, 0)
|
|
if len(data) > 0 {
|
|
v = []any{v, data, string(bytes.ToValidUTF8(data, []byte("?")))}
|
|
}
|
|
var e codec.Encoder
|
|
encode(&e, v)
|
|
b, err := e.Out()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
depth, length := shape(v)
|
|
if err := codec.Walk(b, depth, length); err != nil {
|
|
t.Fatalf("Walk rejects what the Encoder wrote: %x: %v", b, err)
|
|
}
|
|
back, err := cbortest.Unmarshal(b)
|
|
if err != nil {
|
|
t.Fatalf("the reference rejects what the Encoder wrote: %x: %v", b, err)
|
|
}
|
|
if re, _ := cbortest.Marshal(back); !bytes.Equal(re, b) {
|
|
t.Fatalf("%x read back as %x", b, re)
|
|
}
|
|
})
|
|
}
|