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 Map k Key e EndMap a Array u[] Uint // b, Bstr t 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) } }) }