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.
1807 lines
53 KiB
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));
|
|
},
|
|
);
|
|
});
|
|
}
|