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.
dateKeys-dart/test/cbor_test.dart

1807 lines
53 KiB

// Tests of the CBOR profile of spec §58, ported from the tests of package
// codec of datekeys-go (codec_test.go and internal_test.go, the fuzz targets
// as properties over seeded inputs) and from cbor.test.ts of datekeys-ts.
// Every error text is the one that the Go reference prints for the same
// input (Go 1.26.8, datekeys-go at 601e6d2).
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:test/test.dart';
import 'cbor_reference.dart' as ref;
const nc = 'ERR_NON_CANONICAL_CBOR';
// One backslash, for the texts of Go's %q.
const bs = '\\';
Uint8List h(String hex) => fromHex(hex.replaceAll(' ', ''));
String hexOf(String s) => toHex(utf8.encode(s));
final maxSafe = BigInt.from(maxSafeUint);
final twoPow53 = BigInt.two.pow(53);
final twoPow63 = BigInt.two.pow(63);
/// The DateKeysException that [f] throws.
DateKeysException thrown(void Function() f) {
try {
f();
} on DateKeysException catch (e) {
return e;
}
fail('no DateKeysException');
}
/// The message of the DateKeysException that [f] throws.
String messageOf(void Function() f) => thrown(f).message;
/// The code of what [f] throws, or '' when it returns.
String codeOf(void Function() f) {
try {
f();
} on DateKeysException catch (e) {
return e.code.code;
}
return '';
}
String encoded(void Function(CborEncoder e) write) {
final e = CborEncoder();
write(e);
return toHex(e.out());
}
/// Runs [prog], a space-separated list of decoder calls, on the hexadecimal
/// [input] and returns the first error, after checking that every later call
/// throws the same exception (the run of the Go tests):
///
/// m<max> map k key e endMap a<max> array u[<max>] uint64
/// b<min>,<max> bstr t<max> text d done
DateKeysException? run(String input, String prog) {
final d = CborDecoder(h(input));
DateKeysException? first;
for (final op in prog.split(' ').where((s) => s.isNotEmpty)) {
final arg = op.substring(1);
DateKeysException? err;
try {
switch (op[0]) {
case 'm':
d.map(int.parse(arg));
case 'k':
d.key();
case 'e':
d.endMap();
case 'a':
d.array(int.parse(arg));
case 'u':
d.uint64(arg.isEmpty ? null : BigInt.parse(arg));
case 'b':
final [lo, hi] = arg.split(',').map(int.parse).toList();
d.bstr(lo, hi);
case 't':
d.text(int.parse(arg));
case 'd':
d.done();
default:
throw ArgumentError('bad op $op');
}
} on DateKeysException catch (e) {
err = e;
}
if (first == null) {
first = err;
} else if (!identical(err, first)) {
fail('error not sticky: $first, then $err');
}
}
return first;
}
/// The sample schema of the Go tests: {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.
final class Sample {
String type = '';
BigInt n = BigInt.zero;
Uint8List? bytes;
List<BigInt> list = [];
void decode(CborDecoder d) {
final pairs = d.map(4);
for (var i = 0; i < pairs; i++) {
switch (d.key()) {
case 0:
type = d.text(16);
case 1:
n = d.uint64();
case 2:
bytes = d.bstr(0, 64);
case 10:
list = [for (var j = d.array(8); j > 0; j--) d.uint64()];
case final k:
throw DateKeysException(ErrorCode.nonCanonicalCbor, 'unknown key $k');
}
}
if (type.isEmpty || bytes == null) {
throw DateKeysException(ErrorCode.nonCanonicalCbor, 'missing key');
}
d.endMap();
}
void encode(CborEncoder e) {
e
..map(list.isEmpty ? 3 : 4)
..uint(0)
..text(type)
..uint(1)
..uint64(n)
..uint(2)
..bstr(bytes ?? Uint8List(0));
if (list.isNotEmpty) {
e
..uint(10)
..array(list.length);
for (final v in list) {
e.uint64(v);
}
}
}
Uint8List marshal() {
final e = CborEncoder();
encode(e);
return e.out();
}
}
Sample unmarshalSample(String hex) {
final s = Sample();
unmarshalCbor(h(hex), s.decode, s.encode);
return s;
}
/// A random unsigned integer of 64 bits shifted right by 0 to 63 bits, as
/// `r.Uint64() >> r.IntN(64)` of the Go tests.
BigInt randomUint64(Random r) =>
(BigInt.from(r.nextInt(0x100000000)) << 32 |
BigInt.from(r.nextInt(0x100000000))) >>
r.nextInt(64);
/// A random value of the profile, in the types of cbor_reference.dart, as
/// randomValue of the Go tests.
Object? randomValue(Random r, int depth) {
final k = r.nextInt(6);
if (k == 0 && depth < 4) {
return [for (var i = r.nextInt(4); i > 0; i--) randomValue(r, depth + 1)];
}
if (k == 1 && depth < 4) {
final m = <BigInt, Object?>{};
for (var i = r.nextInt(4); i > 0; i--) {
m[randomUint64(r)] = randomValue(r, depth + 1);
}
return m;
}
if (k == 2) return Uint8List(r.nextInt(30));
if (k == 3) return String.fromCharCode(0xe9) * r.nextInt(20);
return randomUint64(r);
}
/// Writes [v], a value of [randomValue], with the encoder: integers up to
/// 2^53-1 through uint and the larger ones through uint64.
void encodeValue(CborEncoder e, Object? v) {
void writeUint(BigInt i) => i <= maxSafe ? e.uint(i.toInt()) : e.uint64(i);
switch (v) {
case final BigInt i:
writeUint(i);
case final Uint8List b:
e.bstr(b);
case final String s:
e.text(s);
case final List<Object?> l:
e.array(l.length);
for (final x in l) {
encodeValue(e, x);
}
case final Map<BigInt, Object?> m:
e.map(m.length);
for (final k in m.keys.toList()..sort()) {
writeUint(k);
encodeValue(e, m[k]);
}
default:
throw ArgumentError('${v.runtimeType}');
}
}
/// [b] with one to three random edits, as the mutations of a fuzzer.
Uint8List mutate(Random r, List<int> b) {
final out = List<int>.of(b);
for (var n = 1 + r.nextInt(3); n > 0; n--) {
final k = r.nextInt(6);
if (k == 0 && out.isNotEmpty) {
out[r.nextInt(out.length)] ^= 1 << r.nextInt(8);
} else if (k == 1 && out.isNotEmpty) {
out[r.nextInt(out.length)] = r.nextInt(256);
} else if (k == 2 && out.isNotEmpty) {
out.length = r.nextInt(out.length);
} else if (k == 3) {
out.insert(r.nextInt(out.length + 1), r.nextInt(256));
} else if (k == 4 && out.length > 1) {
out.removeAt(r.nextInt(out.length));
} else {
out.add(r.nextInt(256));
}
}
return Uint8List.fromList(out);
}
/// The seeds of the fuzz targets of the Go tests, and encodings of random
/// values.
List<Uint8List> fuzzSeeds(Random r) => [
for (final s in [
'a400617801170241010a82011901f4',
'a30061780117024101',
'a3006178011702f6',
'9f01ff',
'a200010101',
'a1008181a10040',
'64efbbbf61',
'1bffffffffffffffff',
'a2006a646174656b657963617001'
'01',
])
h(s),
for (var i = 0; i < 100; i++) ref.marshal(randomValue(r, 0)),
];
bool containsBytes(List<int> haystack, List<int> needle) {
for (var i = 0; i + needle.length <= haystack.length; i++) {
var j = 0;
while (j < needle.length && haystack[i + j] == needle[j]) {
j++;
}
if (j == needle.length) return true;
}
return false;
}
void main() {
group('CborEncoder', () {
test('writes every integer and length in its shortest form', () {
final cases = <(int, String)>[
(0, '00'),
(23, '17'),
(24, '1818'),
(255, '18ff'),
(256, '190100'),
(65535, '19ffff'),
(65536, '1a00010000'),
(0xffffffff, '1affffffff'),
(0x100000000, '1b0000000100000000'),
(maxSafeUint, '1b001fffffffffffff'),
];
for (final (v, want) in cases) {
expect(encoded((e) => e.uint(v)), want, reason: '$v');
expect(encoded((e) => e.uint64(BigInt.from(v))), want, reason: '$v');
}
expect(encoded((e) => e.uint64(maxUint64)), '1bffffffffffffffff');
// {0: "x", 1: 23, 2: h'01', 10: [1, 500], 11: h'', 12: ""}
final map = encoded(
(e) => e
..map(6)
..uint(0)
..text('x')
..uint(1)
..uint(23)
..uint(2)
..bstr([1])
..uint(10)
..array(2)
..uint(1)
..uint(500)
..uint(11)
..bstr([])
..uint(12)
..text(''),
);
expect(map, 'a600617801170241010a82011901f40b400c60');
final long = 'a' * 24;
final s = hexOf(long);
expect(
encoded(
(e) => e
..text(long)
..bstr(utf8.encode(long))
..map(24)
..array(256),
),
'7818${s}5818${s}b818990100',
);
expect(
encoded(
(e) => e
..map(0x100000000)
..array(0x100000000),
),
'bb00000001000000009b0000000100000000',
);
});
test('writes text as UTF-8, a leading U+FEFF included', () {
expect(
encoded((e) => e.text(String.fromCharCodes([0xfeff, 0x61]))),
'64efbbbf61',
);
expect(
encoded((e) => e.text(String.fromCharCode(0x10000))),
'64f0908080',
);
expect(
encoded((e) => e.text(String.fromCharCode(0x10ffff))),
'64f48fbfbf',
);
});
test('keeps the first error: later calls do nothing and out throws it', () {
final first = DateKeysException(ErrorCode.nonCanonicalCbor, 'first');
final failures = <String, (void Function(CborEncoder), String)>{
'negative map': (
(e) => e.map(-1),
'codec: map of -1 entries: ERR_NON_CANONICAL_CBOR',
),
'negative array': (
(e) => e.array(-2),
'codec: array of -2 items: ERR_NON_CANONICAL_CBOR',
),
// A Dart String cannot hold the invalid UTF-8 of the Go tests, "\xff"
// and the overlong "\xc0\x80", but it can hold a lone surrogate.
'lone surrogate': (
(e) => e.text(String.fromCharCode(0xd800)),
'codec: text string "${bs}xed${bs}xa0${bs}x80" is not valid UTF-8: '
'ERR_NON_CANONICAL_CBOR',
),
'lone surrogates': (
(e) => e.text(String.fromCharCodes([0x61, 0xdc00, 0x62, 0xd800])),
'codec: text string "a${bs}xed${bs}xb0${bs}x80b${bs}xed${bs}xa0${bs}x80"'
' is not valid UTF-8: ERR_NON_CANONICAL_CBOR',
),
// Texts of Dart only: Go's Uint takes a uint64, which is never
// negative nor above 2^64-1, and has no limit of 2^53-1.
'negative uint': (
(e) => e.uint(-1),
'codec: -1 is not an unsigned integer in 0..2^53-1: '
'ERR_NON_CANONICAL_CBOR',
),
'uint above 2^53-1': (
(e) => e.uint(maxSafeUint + 1),
'codec: 9007199254740992 is not an unsigned integer in 0..2^53-1: '
'ERR_NON_CANONICAL_CBOR',
),
'negative uint64': (
(e) => e.uint64(BigInt.from(-1)),
'codec: -1 is not an unsigned integer in 0..2^64-1: '
'ERR_NON_CANONICAL_CBOR',
),
'uint64 above 2^64-1': (
(e) => e.uint64(maxUint64 + BigInt.one),
'codec: 18446744073709551616 is not an unsigned integer in '
'0..2^64-1: ERR_NON_CANONICAL_CBOR',
),
'fail': ((e) => e.fail(first), 'first: ERR_NON_CANONICAL_CBOR'),
'fail twice': (
(e) => e
..fail(first)
..fail(FormatException('second')),
'first: ERR_NON_CANONICAL_CBOR',
),
};
for (final MapEntry(key: name, value: (failure, text))
in failures.entries) {
final e = CborEncoder()..bstr(utf8.encode('secret'));
failure(e);
e
..uint(1)
..bstr([1])
..text('ok')
..map(1)
..array(1);
final err = thrown(e.out);
expect(err.message, text, reason: name);
expect(err.code, ErrorCode.nonCanonicalCbor, reason: name);
expect(identical(thrown(e.out), err), isTrue, reason: name);
}
// Go's Fail(nil), which records nothing, has no Dart counterpart: fail
// takes an exception.
});
test('wipes every buffer it outgrows', () {
// The internal test of the reference, through the view that out
// returns: no stale copy of a secret is left behind.
final secret = Uint8List(32)..fillRange(0, 32, 0xab);
final e = CborEncoder()..bstr(secret);
final old = e.out().buffer.asUint8List();
e.bstr(Uint8List(2 * old.length));
expect(containsBytes(old, secret.sublist(0, 8)), isFalse);
expect(containsBytes(e.out(), secret), isTrue);
// A buffer with room is not replaced.
final f = CborEncoder(capacity: 64)..text('fits');
expect(f.out().buffer.lengthInBytes, 64);
});
test('wipes the partial output when it fails', () {
final e = CborEncoder()..bstr(Uint8List(8)..fillRange(0, 8, 0xab));
final partial = e.out();
e.map(-1);
expect(e.out, throwsA(isA<DateKeysException>()));
expect(partial, everyElement(0));
});
});
group('CborDecoder', () {
test('accepts the items of the profile', () {
final cases = <(String, String, 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${'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'),
];
for (final (name, hex, prog) in cases) {
expect(run(hex, prog), isNull, reason: name);
}
});
test('rejects everything else with the text of the reference', () {
final cases = <(String, String, String, String)>[
// Outside the profile of spec §58.
(
'negative int',
'20',
'u',
'offset 0: a negative integer (initial byte 0x20) is outside the CBOR profile',
),
(
'tag',
'c101',
'u',
'offset 0: a tag (initial byte 0xc1) is outside the CBOR profile',
),
(
'tag on a byte string',
'c24101',
'b0,9',
'offset 0: a tag (initial byte 0xc2) is outside the CBOR profile',
),
(
'half float',
'f97e00',
'u',
'offset 0: a float or simple value (initial byte 0xf9) is outside the CBOR profile',
),
(
'single float',
'fa3f800000',
'u',
'offset 0: a float or simple value (initial byte 0xfa) is outside the CBOR profile',
),
(
'double float',
'fb3ff0000000000000',
'u',
'offset 0: a float or simple value (initial byte 0xfb) is outside the CBOR profile',
),
(
'false',
'f4',
'u',
'offset 0: a float or simple value (initial byte 0xf4) is outside the CBOR profile',
),
(
'true',
'f5',
'u',
'offset 0: a float or simple value (initial byte 0xf5) is outside the CBOR profile',
),
(
'null',
'f6',
'b0,9',
'offset 0: a float or simple value (initial byte 0xf6) is outside the CBOR profile',
),
(
'undefined',
'f7',
't9',
'offset 0: a float or simple value (initial byte 0xf7) is outside the CBOR profile',
),
(
'break',
'ff',
'u',
'offset 0: a float or simple value (initial byte 0xff) is outside the CBOR profile',
),
(
'indefinite array',
'9f01ff',
'a9',
'offset 0: indefinite length (initial byte 0x9f)',
),
(
'indefinite map',
'bf0001ff',
'm9',
'offset 0: indefinite length (initial byte 0xbf)',
),
(
'indefinite byte string',
'5f4101ff',
'b0,9',
'offset 0: indefinite length (initial byte 0x5f)',
),
(
'indefinite text',
'7f6161ff',
't9',
'offset 0: indefinite length (initial byte 0x7f)',
),
(
'reserved 28',
'1c',
'u',
'offset 0: reserved additional information (initial byte 0x1c)',
),
(
'reserved 29',
'1d',
'u',
'offset 0: reserved additional information (initial byte 0x1d)',
),
(
'reserved 30',
'1e',
'u',
'offset 0: reserved additional information (initial byte 0x1e)',
),
// Shortest form.
(
'uint 23 with one extra byte',
'1817',
'u',
'offset 0: 23 is not in its shortest form (initial byte 0x18)',
),
(
'uint 255 in two bytes',
'1900ff',
'u',
'offset 0: 255 is not in its shortest form (initial byte 0x19)',
),
(
'uint 65535 in four bytes',
'1a0000ffff',
'u',
'offset 0: 65535 is not in its shortest form (initial byte 0x1a)',
),
(
'uint 2^32-1 in eight bytes',
'1b00000000ffffffff',
'u',
'offset 0: 4294967295 is not in its shortest form (initial byte 0x1b)',
),
(
'bstr length not shortest',
'5800',
'b0,9',
'offset 0: 0 is not in its shortest form (initial byte 0x58)',
),
(
'map length not shortest',
'b800',
'm9',
'offset 0: 0 is not in its shortest form (initial byte 0xb8)',
),
(
'key not shortest',
'a1180000',
'm1 k',
'offset 1: 0 is not in its shortest form (initial byte 0x18)',
),
// Truncation.
('empty input', '', 'u', 'offset 0: truncated input'),
('truncated uint', '1901', 'u', 'offset 0: truncated input'),
(
'truncated uint 8',
'1b00000000000000',
'u',
'offset 0: truncated input',
),
(
'length beyond input',
'5affffffff',
'b0,9',
'offset 5: truncated input: a byte string of 4294967295 bytes',
),
(
'bstr shorter than its length',
'4300',
'b0,9',
'offset 1: truncated input: a byte string of 3 bytes',
),
(
'text shorter than its length',
'6361',
't9',
'offset 1: truncated input: a text string of 3 bytes',
),
(
'map beyond input',
'a300',
'm9',
'offset 1: truncated input: map of 3 entries',
),
(
'huge map',
'bbffffffffffffffff',
'm100',
'offset 9: map of 18446744073709551615 entries, at most 100',
),
(
'array beyond input',
'8300',
'a9',
'offset 1: truncated input: array of 3 items',
),
(
'huge array',
'9b7fffffffffffffff',
'a100',
'offset 9: array of 9223372036854775807 items, at most 100',
),
(
'truncated inside a map',
'a200',
'm2 k u',
'offset 1: truncated input: map of 2 entries',
),
// Types and bounds.
(
'bstr where uint',
'4100',
'u',
'offset 0: a byte string where an unsigned integer was expected',
),
(
'uint where bstr',
'00',
'b0,9',
'offset 0: an unsigned integer where a byte string was expected',
),
(
'text where bstr',
'6161',
'b0,9',
'offset 0: a text string where a byte string was expected',
),
(
'bstr where text',
'4161',
't9',
'offset 0: a byte string where a text string was expected',
),
(
'array where map',
'80',
'm9',
'offset 0: an array where a map was expected',
),
(
'map where array',
'a0',
'a9',
'offset 0: a map where an array was expected',
),
(
'uint above max',
'1818',
'u23',
'offset 2: unsigned integer 24 above 23',
),
(
'bstr below min',
'4101',
'b2,9',
'offset 1: a byte string of 1 bytes outside 2..9',
),
(
'bstr above max',
'420102',
'b0,1',
'offset 1: a byte string of 2 bytes outside 0..1',
),
(
'text above max',
'626161',
't1',
'offset 1: a text string of 2 bytes outside 0..1',
),
(
'map above max',
'a200010101',
'm1',
'offset 1: map of 2 entries, at most 1',
),
(
'negative map max',
'a0',
'm-1',
'offset 1: map of 0 entries, at most -1',
),
(
'array above max',
'820101',
'a1',
'offset 1: array of 2 items, at most 1',
),
(
'negative array max',
'80',
'a-1',
'offset 1: array of 0 items, at most -1',
),
// Keys.
(
'keys out of order',
'a201000001',
'm2 k u k',
'offset 3: map key 0 after key 1: keys must be strictly ascending',
),
(
'duplicate key',
'a200000001',
'm2 k u k',
'offset 3: map key 0 after key 0: keys must be strictly ascending',
),
(
'text key',
'a1616100',
'm1 k',
'offset 1: a text string where an unsigned integer was expected',
),
(
'bstr key',
'a1416100',
'm1 k',
'offset 1: a byte string where an unsigned integer was expected',
),
(
'negative key',
'a12000',
'm1 k',
'offset 1: a negative integer (initial byte 0x20) is outside the CBOR profile',
),
('key outside a map', '00', 'k', 'offset 0: map key outside a map'),
(
'key after the last entry',
'a10000',
'm1 k u k',
'offset 3: map key after the last entry',
),
(
'key after the map closed',
'a0',
'm0 e k',
'offset 1: map key outside a map',
),
(
'end outside a map',
'00',
'e',
'offset 0: end of a map outside a map',
),
(
'end with entries left',
'a10000',
'm1 e',
'offset 1: 1 map entries not read',
),
('done with a map open', 'a0', 'm0 d', 'offset 1: 1 maps not closed'),
// Trailing bytes and UTF-8.
('trailing byte', '0100', 'u d', 'offset 1: 1 trailing bytes'),
(
'invalid UTF-8',
'61ff',
't9',
'offset 0: text string is not valid UTF-8',
),
(
'overlong UTF-8',
'62c080',
't9',
'offset 0: text string is not valid UTF-8',
),
(
'UTF-8 surrogate',
'63eda080',
't9',
'offset 0: text string is not valid UTF-8',
),
(
'above U+10FFFF',
'64f4908080',
't9',
'offset 0: text string is not valid UTF-8',
),
(
'truncated UTF-8',
'62e282',
't9',
'offset 0: text string is not valid UTF-8',
),
// The first error stays.
(
'sticky after a failed read',
'f600',
'u u d',
'offset 0: a float or simple value (initial byte 0xf6) is outside the CBOR profile',
),
];
for (final (name, hex, prog, text) in cases) {
final err = run(hex, prog);
expect(err?.message, 'codec: $text: $nc', reason: name);
expect(err?.code, ErrorCode.nonCanonicalCbor, reason: name);
}
});
test('names the offset of the item in its errors', () {
expect(
run('a2 00 01 00 02', 'm2 k u k')?.message,
'codec: offset 3: map key 0 after key 0: keys must be strictly '
'ascending: ERR_NON_CANONICAL_CBOR',
);
});
test('copies byte strings', () {
final input = h('43010203');
final d = CborDecoder(input);
final b = d.bstr(3, 3);
d.done();
input[1] = 9;
expect(b, [1, 2, 3]);
expect(CborDecoder(h('40')).bstr(0, 0), isEmpty);
});
test('keeps a leading U+FEFF of a text', () {
expect(CborDecoder(h('64efbbbf61')).text(4).codeUnits, [0xfeff, 0x61]);
expect(CborDecoder(h('66efbbbfefbbbf')).text(6).codeUnits, [
0xfeff,
0xfeff,
]);
expect(CborDecoder(h('63ed9fbf')).text(3).codeUnits, [0xd7ff]);
});
test(
'takes the bounds of uint in 0..2^53-1 and of uint64 in 0..2^64-1',
() {
expect(CborDecoder(h('04')).uint(4), 4);
expect(() => CborDecoder(h('00')).uint(-1), throwsArgumentError);
expect(
() => CborDecoder(h('00')).uint(maxSafeUint + 1),
throwsArgumentError,
);
expect(
() => CborDecoder(h('00')).uint64(BigInt.from(-1)),
throwsArgumentError,
);
expect(
() => CborDecoder(h('00')).uint64(maxUint64 + BigInt.one),
throwsArgumentError,
);
},
);
});
group('integers at 2^53-1, 2^53, 2^63 and 2^64-1', () {
test('are read exactly', () {
final d = CborDecoder(
h(
'1b001fffffffffffff 1b0020000000000000 1b8000000000000000 '
'1bffffffffffffffff',
),
);
expect(d.uint64(), maxSafe);
expect(d.uint64(), twoPow53);
expect(d.uint64(), twoPow63);
expect(d.uint64(), maxUint64);
d.done();
expect('$maxUint64', '18446744073709551615');
expect(CborDecoder(h('1b001fffffffffffff')).uint(), maxSafeUint);
// A map key is an int up to 2^53-1 and a BigInt above.
final k = CborDecoder(
h(
'a4 1b001fffffffffffff 00 1b0020000000000000 00 '
'1b8000000000000000 00 1bffffffffffffffff 00',
),
);
expect(k.map(4), 4);
final keys = <Object>[];
for (var i = 0; i < 4; i++) {
keys.add(k.key());
k.uint(0);
}
k
..endMap()
..done();
expect(keys[0], isA<int>());
expect(keys[0], maxSafeUint);
expect(keys.skip(1), everyElement(isA<BigInt>()));
expect(keys.skip(1), [twoPow53, twoPow63, maxUint64]);
// Keys above 2^53 compare exactly: 2^53 and 2^53+1 ascend.
expect(
run('a2 1b0020000000000000 00 1b0020000000000001 00', 'm2 k u k u e d'),
isNull,
);
});
test('are printed exactly in errors', () {
final cases = <(String, String, String, String)>[
(
'uint 2^53 above 2^53-1',
'1b0020000000000000',
'u9007199254740991',
'offset 9: unsigned integer 9007199254740992 above 9007199254740991',
),
(
'uint 2^63+1 above 2^63',
'1b8000000000000001',
'u9223372036854775808',
'offset 9: unsigned integer 9223372036854775809 above 9223372036854775808',
),
(
'uint 2^64-1 above 2^64-2',
'1bffffffffffffffff',
'u18446744073709551614',
'offset 9: unsigned integer 18446744073709551615 above 18446744073709551614',
),
(
'keys 2^53+1 then 2^53',
'a2 1b0020000000000001 00 1b0020000000000000 00',
'm2 k u k',
'offset 11: map key 9007199254740992 after key 9007199254740993: keys must be strictly ascending',
),
(
'keys 2^64-1 then 2^64-2',
'a2 1bffffffffffffffff 00 1bfffffffffffffffe 00',
'm2 k u k',
'offset 11: map key 18446744073709551614 after key 18446744073709551615: keys must be strictly ascending',
),
(
'keys 2^63 twice',
'a2 1b8000000000000000 00 1b8000000000000000 00',
'm2 k u k',
'offset 11: map key 9223372036854775808 after key 9223372036854775808: keys must be strictly ascending',
),
(
'keys 2^53-1 twice',
'a2 1b001fffffffffffff 00 1b001fffffffffffff 00',
'm2 k u k',
'offset 11: map key 9007199254740991 after key 9007199254740991: keys must be strictly ascending',
),
(
'bstr of 2^53 bytes',
'5b0020000000000000',
'b0,9',
'offset 9: truncated input: a byte string of 9007199254740992 bytes',
),
(
'text of 2^64-1 bytes',
'7bffffffffffffffff',
't9',
'offset 9: truncated input: a text string of 18446744073709551615 bytes',
),
(
'array of 2^63 items',
'9b8000000000000000',
'a100',
'offset 9: array of 9223372036854775808 items, at most 100',
),
(
'map of 2^53-1 entries',
'bb001fffffffffffff',
'm9007199254740991',
'offset 9: truncated input: map of 9007199254740991 entries',
),
(
'array of 2^53 items',
'9b0020000000000000',
'a9007199254740991',
'offset 9: array of 9007199254740992 items, at most 9007199254740991',
),
];
for (final (name, hex, prog, text) in cases) {
expect(run(hex, prog)?.message, 'codec: $text: $nc', reason: name);
}
expect(
messageOf(() => CborDecoder(h('1b0020000000000000')).uint()),
'codec: offset 9: unsigned integer 9007199254740992 above '
'9007199254740991: ERR_NON_CANONICAL_CBOR',
);
});
test('are written exactly', () {
expect(encoded((e) => e.uint(maxSafeUint)), '1b001fffffffffffff');
expect(encoded((e) => e.uint64(twoPow53)), '1b0020000000000000');
expect(encoded((e) => e.uint64(twoPow63)), '1b8000000000000000');
expect(encoded((e) => e.uint64(maxUint64)), '1bffffffffffffffff');
});
test('bound the map that peekSchema reads at 2^63-1, as Go', () {
final cases = <(String, String)>[
(
'bb8000000000000000',
'offset 9: map of 9223372036854775808 entries, at most 9223372036854775807',
),
(
'bbffffffffffffffff',
'offset 9: map of 18446744073709551615 entries, at most 9223372036854775807',
),
(
'bb7fffffffffffffff',
'offset 9: truncated input: map of 9223372036854775807 entries',
),
(
'bb0020000000000005',
'offset 9: truncated input: map of 9007199254740997 entries',
),
(
'a2 1bffffffffffffffff 00 01 01',
'offset 10: map key 18446744073709551615 where key 0 was expected',
),
(
'a2 00 6161 1b0020000000000000 01',
'offset 13: map key 9007199254740992 where key 1 was expected',
),
];
for (final (hex, text) in cases) {
expect(messageOf(() => peekSchema(h(hex))), 'codec: $text: $nc');
}
});
});
group('unmarshalCbor', () {
test('decodes the deterministic encoding of a value', () {
final s = unmarshalSample('a400617801170241010a82011901f4');
expect(s.type, 'x');
expect(s.n, BigInt.from(23));
expect(s.bytes, [1]);
expect(s.list, [BigInt.one, BigInt.from(500)]);
});
test('rejects every non-canonical or invalid encoding', () {
final cases = <(String, 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',
),
];
for (final (name, hex) in cases) {
expect(codeOf(() => unmarshalSample(hex)), nc, reason: name);
}
expect(
messageOf(() => unmarshalSample('a400617801170241010a80')),
'codec: input is not the deterministic encoding of its value: '
'ERR_NON_CANONICAL_CBOR',
);
expect(
messageOf(() => unmarshalSample('a3006178011702410100')),
'codec: offset 9: 1 trailing bytes: ERR_NON_CANONICAL_CBOR',
);
});
test('checks the re-encoding, whatever the decoder and the encoder do', () {
final input = h('a30061780117024101');
// An error of the schema passes as it is.
final own = DateKeysException(ErrorCode.dateKeyInvalid, 'schema');
expect(
() => unmarshalCbor(input, (_) => throw own, (_) {}),
throwsA(same(own)),
);
final other = StateError('not a DateKeys error');
expect(
() => unmarshalCbor(input, (_) => throw other, (_) {}),
throwsA(same(other)),
);
// A decoder that stops early leaves bytes behind.
expect(
messageOf(() => unmarshalCbor(input, (d) => d.map(3), (_) {})),
'codec: offset 1: 1 maps not closed: ERR_NON_CANONICAL_CBOR',
);
// A decoder that ignores an error of the decoder fails anyway.
void ignore(CborDecoder d) {
try {
d.uint(0);
} on DateKeysException {
// Ignored on purpose: done throws it again.
}
}
expect(
messageOf(() => unmarshalCbor(input, ignore, (_) {})),
'codec: offset 0: a map where an unsigned integer was expected: '
'ERR_NON_CANONICAL_CBOR',
);
// An encoder that fails, and one that writes something else.
const notDeterministic =
'codec: input is not the deterministic encoding of its value: '
'ERR_NON_CANONICAL_CBOR';
final s = Sample();
expect(
messageOf(() => unmarshalCbor(input, s.decode, (e) => e.map(-1))),
notDeterministic,
);
expect(
messageOf(
() => unmarshalCbor(input, s.decode, (e) {
s.encode(e);
e.uint(0);
}),
),
notDeterministic,
);
});
test('wipes the re-encoding, on success and on failure', () {
final input = h('a30061780117024101');
late CborEncoder used;
final s = Sample();
unmarshalCbor(input, s.decode, (e) => s.encode(used = e));
expect(used.out(), hasLength(input.length));
expect(used.out(), everyElement(0));
expect(
codeOf(
() => unmarshalCbor(input, s.decode, (e) {
s.encode(used = e);
e.uint(0);
}),
),
nc,
);
expect(used.out(), hasLength(input.length + 1));
expect(used.out(), everyElement(0));
});
test('round-trips random values of the sample schema', () {
final r = Random(2);
for (var i = 0; i < 500; i++) {
final s = Sample()
..type = String.fromCharCode(0x61 + r.nextInt(26))
..n = randomUint64(r)
..bytes = Uint8List(r.nextInt(40))
..list = [for (var j = r.nextInt(4); j > 0; j--) randomUint64(r)];
final b = Uint8List.fromList(s.marshal());
final back = Sample();
unmarshalCbor(b, back.decode, back.encode);
expect(toHex(back.marshal()), toHex(b));
}
});
});
group('peekSchema and checkSchema', () {
final tag = '6a${hexOf('datekeycap')}';
test('read the type tag and the version, and nothing after them', () {
// A future version may use anything after key 1.
final future = ref.marshal({
0: 'datekeycap',
1: 2,
99: 'new',
100: const ref.Raw([0xf9, 0x7e, 0x00]),
});
expect(peekSchema(future), (typeTag: 'datekeycap', version: 2));
final long = 'a' * maxTypeTagLen;
expect(peekSchema(h('a200 7840 ${hexOf(long)} 0101')).typeTag, long);
expect(
peekSchema(h('a200 $tag 01 1b001fffffffffffff')).version,
maxSafeUint,
);
});
test('reject any other start with the text of the reference', () {
final cases = <(String, String, String)>[
('empty', '', 'offset 0: truncated input'),
('not a map', '8200', 'offset 0: an array where a map was expected'),
(
'one entry',
'a1006161',
'offset 1: map without a type tag and a schema version',
),
(
'first key not 0',
'a2016161'
'0201',
'offset 2: map key 1 where key 0 was expected',
),
(
'second key not 1',
'a2006161'
'0201',
'offset 5: map key 2 where key 1 was expected',
),
(
'text key',
'a2616100'
'0101',
'offset 1: a text string where an unsigned integer was expected',
),
(
'type tag not text',
'a2004161'
'0101',
'offset 2: a byte string where a text string was expected',
),
(
'version not uint',
'a2006161'
'0120',
'offset 5: a negative integer (initial byte 0x20) is outside the CBOR profile',
),
(
'version above 2^53-1',
'a2006161'
'011b0020000000000000',
'offset 14: unsigned integer 9007199254740992 above 9007199254740991',
),
(
'keys swapped',
'a2010100'
'6161',
'offset 2: map key 1 where key 0 was expected',
),
(
'truncated type tag',
'a2006361',
'offset 1: truncated input: map of 2 entries',
),
(
'map beyond input',
'a5006161'
'0101',
'offset 1: truncated input: map of 5 entries',
),
(
'type tag above maxTypeTagLen',
'a200 7841 ${'61' * 65} 0101',
'offset 4: a text string of 65 bytes outside 0..64',
),
(
'type tag not valid UTF-8',
'a20061ff0101',
'offset 2: text string is not valid UTF-8',
),
(
'map head not shortest',
'b802 00 $tag 0102',
'offset 0: 2 is not in its shortest form (initial byte 0xb8)',
),
];
for (final (name, hex, text) in cases) {
expect(
messageOf(() => peekSchema(h(hex))),
'codec: $text: $nc',
reason: name,
);
}
});
test('check the type tag first, and only then the version', () {
final b =
(Sample()
..type = 'datekeycap'
..n = BigInt.one
..bytes = Uint8List(0))
.marshal();
checkSchema(b, 'datekeycap', 1);
expect(
messageOf(() => checkSchema(b, 'datekeys-control', 1)),
'codec: type "datekeycap", want "datekeys-control": '
'ERR_NON_CANONICAL_CBOR',
);
expect(
messageOf(
() => checkSchema(h('a200 $tag 0102'), 'datekeys-control', 1),
),
'codec: type "datekeycap", want "datekeys-control": '
'ERR_NON_CANONICAL_CBOR',
);
final version = thrown(() => checkSchema(b, 'datekeycap', 2));
expect(version.code, ErrorCode.unsupportedVersion);
expect(
version.message,
'codec: datekeycap schema version 1, want 2: ERR_UNSUPPORTED_VERSION',
);
expect(
messageOf(
() => checkSchema(
h('a200 $tag 01 1b001fffffffffffff'),
'datekeycap',
1,
),
),
'codec: datekeycap schema version 9007199254740991, want 1: '
'ERR_UNSUPPORTED_VERSION',
);
// A future version with unknown keys still reports the version.
final future = ref.marshal({0: 'datekeycap', 1: 2, 99: -7});
expect(
codeOf(() => checkSchema(future, 'datekeycap', 1)),
'ERR_UNSUPPORTED_VERSION',
);
for (final hex in ['', 'ff', '8301', 'a10061']) {
expect(codeOf(() => checkSchema(h(hex), 'datekeycap', 1)), nc);
}
});
test('read a version only as the second key, after a type tag', () {
// Spec §70: every other form of the version is ERR_NON_CANONICAL_CBOR,
// whatever its value.
const uv = 'ERR_UNSUPPORTED_VERSION';
final text = hexOf('datekeycap');
final cases = <(String, String, String)>[
('version 2', 'a200 $tag 0102', uv),
('version 2, rest malformed', 'a300 $tag 0102 02ff', uv),
('version 2^53-1', 'a200 $tag 011b001fffffffffffff', uv),
('version 2 not in shortest form', 'a200 $tag 011802', nc),
('version 2^53', 'a200 $tag 011b0020000000000000', nc),
('version 2^64-1', 'a200 $tag 011bffffffffffffffff', nc),
('version before key 0', 'a2 0102 00 $tag', nc),
('version after key 2', 'a3 00 $tag 0200 0102', nc),
('map head not in shortest form', 'b802 00 $tag 0102', nc),
('type tag head not in shortest form', 'a200 780a $text 0102', nc),
('version missing', 'a100 $tag', nc),
('version null', 'a200 $tag 01f6', nc),
('version undefined', 'a200 $tag 01f7', nc),
('version true', 'a200 $tag 01f5', nc),
('version false', 'a200 $tag 01f4', nc),
];
for (final (name, hex, want) in cases) {
expect(
codeOf(() => checkSchema(h(hex), 'datekeycap', 1)),
want,
reason: name,
);
}
});
test('quote a type tag as Go %q, with the tables of Go 1.26', () {
final cases = <(String, String)>[
('67 64617465e280a8', '"date${bs}u2028"'),
('65 efbbbf2261', '"${bs}ufeff$bs"a"'),
('62 0a7f', '"${bs}n${bs}x7f"'),
];
for (final (tagHex, quoted) in cases) {
expect(
messageOf(() => checkSchema(h('a200 $tagHex 0101'), 'datekeycap', 1)),
'codec: type $quoted, want "datekeycap": ERR_NON_CANONICAL_CBOR',
);
}
});
});
group('walkCbor', () {
test('bounds depth and length as the reference', () {
final cases = <(String, String, int, int, String?)>[
('uint', '17', 0, 0, null),
('empty bstr', '40', 0, 0, null),
('text', '626161', 0, 2, null),
(
'text above max',
'626161',
0,
1,
'offset 1: a text string of 2 bytes outside 0..1',
),
(
'bstr above max',
'420000',
0,
1,
'offset 1: a byte string of 2 bytes outside 0..1',
),
('map two sorted keys', 'a200010101', 1, 2, null),
(
'map at depth 0',
'a0',
0,
0,
'offset 0: containers nested deeper than 0',
),
(
'map above max',
'a200010101',
1,
1,
'offset 1: map of 2 entries, at most 1',
),
(
'array above max',
'83010203',
1,
2,
'offset 1: array of 3 items, at most 2',
),
('four levels', 'a1008181a10040', 4, 1, null),
(
'four levels, depth 3',
'a1008181a10040',
3,
1,
'offset 4: containers nested deeper than 3',
),
('empty containers', '82a080', 2, 2, null),
(
'keys out of order',
'a201000001',
1,
2,
'offset 3: map key 0 after key 1: keys must be strictly ascending',
),
(
'text key',
'a1616100',
1,
1,
'offset 1: a text string where an unsigned integer was expected',
),
(
'float inside',
'8201f97e00',
1,
2,
'offset 2: a float or simple value (initial byte 0xf9) is outside the CBOR profile',
),
(
'truncated map',
'a20001',
1,
2,
'offset 1: truncated input: map of 2 entries',
),
(
'truncated array',
'8201',
1,
2,
'offset 1: truncated input: array of 2 items',
),
('trailing byte', '8000', 1, 0, 'offset 1: 1 trailing bytes'),
('empty input', '', 1, 1, 'offset 0: truncated input'),
(
'invalid UTF-8 inside',
'a10061ff',
1,
1,
'offset 2: text string is not valid UTF-8',
),
(
'null',
'f6',
1,
1,
'offset 0: a float or simple value (initial byte 0xf6) is outside the CBOR profile',
),
('2^64-1 inside', 'a1001bffffffffffffffff', 1, 1, null),
];
for (final (name, hex, maxDepth, maxLen, text) in cases) {
void walk() => walkCbor(h(hex), maxDepth, maxLen);
if (text == null) {
expect(walk, returnsNormally, reason: name);
} else {
expect(messageOf(walk), 'codec: $text: $nc', reason: name);
}
}
});
test('reads deep input iteratively', () {
const n = 1 << 20;
final deep = Uint8List(n + 1)..fillRange(0, n, 0x81);
walkCbor(deep, n, 1);
expect(
messageOf(() => walkCbor(deep, n - 1, 1)),
'codec: offset 1048575: containers nested deeper than 1048575: '
'ERR_NON_CANONICAL_CBOR',
);
});
});
// The property tests and the fuzz targets of the Go tests, over seeded
// random values and seeded mutations of the seeds of the fuzz targets.
group('properties', () {
test('the encoder writes what the reference writes, and walk accepts it '
'within its exact shape and rejects it one level or one byte '
'tighter', () {
final r = Random(1);
for (var i = 0; i < 1000; i++) {
final v = randomValue(r, 0);
final e = CborEncoder();
encodeValue(e, v);
final b = e.out();
expect(toHex(b), toHex(ref.marshal(v)));
final (depth, length) = ref.shape(v);
walkCbor(b, depth, length);
if (depth > 0) {
expect(codeOf(() => walkCbor(b, depth - 1, length)), nc);
}
if (length > 0) {
expect(codeOf(() => walkCbor(b, depth, length - 1)), nc);
}
}
});
test(
'walk accepts exactly the items of the profile that fit its bounds',
() {
final r = Random(3);
final seeds = fuzzSeeds(r);
for (var i = 0; i < 4000; i++) {
final input = mutate(r, seeds[r.nextInt(seeds.length)]);
final code = codeOf(() => walkCbor(input, 8, 64));
expect(code, anyOf('', nc));
final Object? v;
try {
v = ref.unmarshal(input);
} on FormatException {
expect(
code,
nc,
reason: '${toHex(input)}: the reference rejects it',
);
continue;
}
final (depth, length) = ref.shape(v);
expect(
code == '',
depth <= 8 && length <= 64,
reason: '${toHex(input)} of depth $depth and length $length',
);
}
},
);
test('peekSchema reads what the reference reads', () {
final r = Random(4);
final seeds = fuzzSeeds(r);
for (var i = 0; i < 4000; i++) {
final input = mutate(r, seeds[r.nextInt(seeds.length)]);
({String typeTag, int version})? got;
try {
got = peekSchema(input);
} on DateKeysException catch (e) {
expect(e.code, ErrorCode.nonCanonicalCbor);
}
final Object? m;
try {
m = ref.unmarshal(input);
} on FormatException {
continue;
}
if (m is! Map<BigInt, Object?>) continue;
// Keys ascend, so keys 0 and 1, when present, come first.
final tag = m[BigInt.zero];
final version = m[BigInt.one];
final want =
tag is ref.Text &&
tag.bytes.length <= maxTypeTagLen &&
version is BigInt &&
version <= maxSafe;
expect(got != null, want, reason: toHex(input));
if (got != null && tag is ref.Text && version is BigInt) {
expect(utf8.encode(got.typeTag), tag.bytes);
expect(BigInt.from(got.version), version);
}
}
});
test(
'the decoder keeps its bounds and its first error, of the profile',
() {
final r = Random(5);
final seeds = fuzzSeeds(r);
for (var i = 0; i < 4000; i++) {
final input = mutate(r, seeds[r.nextInt(seeds.length)]);
final prog = [for (var j = r.nextInt(16); j > 0; j--) r.nextInt(256)];
final d = CborDecoder(input);
DateKeysException? first;
for (var p = 0; p < prog.length; p++) {
final bound = p + 1 < prog.length ? prog[p + 1] : 0;
DateKeysException? err;
try {
switch (prog[p] % 8) {
case 0:
expect(d.map(bound), lessThanOrEqualTo(bound));
p++;
case 1:
d.key();
case 2:
d.endMap();
case 3:
expect(d.array(bound), lessThanOrEqualTo(bound));
p++;
case 4:
expect(d.uint(bound), lessThanOrEqualTo(bound));
p++;
case 5:
final lo = bound % 16;
expect(
d.bstr(lo, bound).length,
allOf(greaterThanOrEqualTo(lo), lessThanOrEqualTo(bound)),
);
p++;
case 6:
expect(
utf8.encode(d.text(bound)).length,
lessThanOrEqualTo(bound),
);
p++;
case 7:
d.done();
}
} on DateKeysException catch (e) {
err = e;
expect(e.code, ErrorCode.nonCanonicalCbor);
}
if (first == null) {
first = err;
} else {
expect(identical(err, first), isTrue, reason: 'not sticky');
}
}
}
},
);
test(
'unmarshalCbor accepts only deterministic encodings of the profile',
() {
final r = Random(6);
final seeds = [
...fuzzSeeds(r),
for (var i = 0; i < 50; i++)
(Sample()
..type = 'x'
..n = randomUint64(r)
..bytes = Uint8List(r.nextInt(8))
..list = [
for (var j = r.nextInt(3); j > 0; j--) randomUint64(r),
])
.marshal(),
];
var accepted = 0;
for (var i = 0; i < 4000; i++) {
final input = mutate(r, seeds[r.nextInt(seeds.length)]);
final s = Sample();
try {
unmarshalCbor(input, s.decode, s.encode);
} on DateKeysException catch (e) {
expect(e.code, ErrorCode.nonCanonicalCbor);
continue;
}
accepted++;
expect(toHex(s.marshal()), toHex(input));
walkCbor(input, 2, 64);
}
expect(accepted, greaterThan(0));
},
);
});
}

Powered by TurnKey Linux.