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.
224 lines
6.5 KiB
224 lines
6.5 KiB
|
2 days ago
|
// An encoder and a decoder of generic CBOR values for the tests, as
|
||
|
|
// internal/cbortest of datekeys-go. They are written independently of
|
||
|
|
// lib/src/cbor.dart on purpose, with BigInt arithmetic and dart:convert:
|
||
|
|
// tests use them to build inputs that the codec cannot write, such as null,
|
||
|
|
// negative integers or a float inside an otherwise valid object, and as a
|
||
|
|
// second reading of the CBOR profile of spec §58 to check the codec against.
|
||
|
|
|
||
|
|
import 'dart:convert';
|
||
|
|
import 'dart:typed_data';
|
||
|
|
|
||
|
|
/// An encoded data item that [marshal] writes verbatim.
|
||
|
|
final class Raw {
|
||
|
|
const Raw(this.bytes);
|
||
|
|
|
||
|
|
final List<int> bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A text string held as its bytes, as [unmarshal] returns it: dart:convert
|
||
|
|
/// would drop a leading U+FEFF.
|
||
|
|
final class Text {
|
||
|
|
const Text(this.bytes);
|
||
|
|
|
||
|
|
final Uint8List bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The deterministic encoding of [v], which is one of: an int (a negative one
|
||
|
|
/// as major type 1) or a BigInt (unsigned), a String or a [Text], a
|
||
|
|
/// Uint8List, a bool, null, a List of values, a Map whose keys are ints or
|
||
|
|
/// BigInts (written in ascending order), or a [Raw].
|
||
|
|
Uint8List marshal(Object? v) {
|
||
|
|
final out = <int>[];
|
||
|
|
_append(out, v);
|
||
|
|
return Uint8List.fromList(out);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _append(List<int> out, Object? v) {
|
||
|
|
switch (v) {
|
||
|
|
case null:
|
||
|
|
out.add(0xf6);
|
||
|
|
case final bool b:
|
||
|
|
out.add(b ? 0xf5 : 0xf4);
|
||
|
|
case final int i when i < 0:
|
||
|
|
_head(out, 1, BigInt.from(-(i + 1)));
|
||
|
|
case final int i:
|
||
|
|
_head(out, 0, BigInt.from(i));
|
||
|
|
case final BigInt i:
|
||
|
|
_head(out, 0, i);
|
||
|
|
case final Uint8List b:
|
||
|
|
_head(out, 2, BigInt.from(b.length));
|
||
|
|
out.addAll(b);
|
||
|
|
case final String s:
|
||
|
|
final b = utf8.encode(s);
|
||
|
|
_head(out, 3, BigInt.from(b.length));
|
||
|
|
out.addAll(b);
|
||
|
|
case final Text t:
|
||
|
|
_head(out, 3, BigInt.from(t.bytes.length));
|
||
|
|
out.addAll(t.bytes);
|
||
|
|
case final Raw r:
|
||
|
|
out.addAll(r.bytes);
|
||
|
|
case final List<Object?> l:
|
||
|
|
_head(out, 4, BigInt.from(l.length));
|
||
|
|
for (final x in l) {
|
||
|
|
_append(out, x);
|
||
|
|
}
|
||
|
|
case final Map<Object, Object?> m:
|
||
|
|
final keys = m.keys.toList()..sort((a, b) => _big(a).compareTo(_big(b)));
|
||
|
|
_head(out, 5, BigInt.from(m.length));
|
||
|
|
for (final k in keys) {
|
||
|
|
_head(out, 0, _big(k));
|
||
|
|
_append(out, m[k]);
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
throw ArgumentError('cannot encode ${v.runtimeType}');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
BigInt _big(Object k) => k is BigInt ? k : BigInt.from(k as int);
|
||
|
|
|
||
|
|
void _head(List<int> out, int major, BigInt arg) {
|
||
|
|
final m = major << 5;
|
||
|
|
if (arg < BigInt.from(24)) {
|
||
|
|
out.add(m | arg.toInt());
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
final int n;
|
||
|
|
if (arg < BigInt.from(0x100)) {
|
||
|
|
out.add(m | 24);
|
||
|
|
n = 1;
|
||
|
|
} else if (arg < BigInt.from(0x10000)) {
|
||
|
|
out.add(m | 25);
|
||
|
|
n = 2;
|
||
|
|
} else if (arg < BigInt.one << 32) {
|
||
|
|
out.add(m | 26);
|
||
|
|
n = 4;
|
||
|
|
} else {
|
||
|
|
out.add(m | 27);
|
||
|
|
n = 8;
|
||
|
|
}
|
||
|
|
for (var i = n - 1; i >= 0; i--) {
|
||
|
|
out.add(((arg >> (8 * i)) & BigInt.from(0xff)).toInt());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The nesting that [unmarshal] follows at most.
|
||
|
|
const maxDepth = 1000;
|
||
|
|
|
||
|
|
/// Decodes exactly one data item of the CBOR profile of spec §58 into BigInt,
|
||
|
|
/// Uint8List, [Text], List and Map with BigInt keys in their order. It throws
|
||
|
|
/// a FormatException for every other major type, an indefinite length, a
|
||
|
|
/// head not in its shortest form, a map key that is not an unsigned integer
|
||
|
|
/// greater than the previous one, invalid UTF-8, truncation, trailing bytes
|
||
|
|
/// and nesting deeper than [maxDepth].
|
||
|
|
Object? unmarshal(List<int> b) {
|
||
|
|
final r = _Reader(b);
|
||
|
|
final v = r.value(0);
|
||
|
|
if (r.off != b.length) {
|
||
|
|
throw FormatException('${b.length - r.off} trailing bytes');
|
||
|
|
}
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
|
||
|
|
final class _Reader {
|
||
|
|
_Reader(this.b);
|
||
|
|
|
||
|
|
final List<int> b;
|
||
|
|
int off = 0;
|
||
|
|
|
||
|
|
(int, BigInt) head() {
|
||
|
|
if (off >= b.length) throw const FormatException('truncated');
|
||
|
|
final ib = b[off++];
|
||
|
|
final major = ib >> 5;
|
||
|
|
final info = ib & 0x1f;
|
||
|
|
if (major == 1 || major >= 6) throw FormatException('major type $major');
|
||
|
|
if (info < 24) return (major, BigInt.from(info));
|
||
|
|
if (info > 27) throw FormatException('additional information $info');
|
||
|
|
final n = 1 << (info - 24);
|
||
|
|
if (b.length - off < n) throw const FormatException('truncated');
|
||
|
|
var arg = BigInt.zero;
|
||
|
|
for (var i = 0; i < n; i++) {
|
||
|
|
arg = arg << 8 | BigInt.from(b[off + i]);
|
||
|
|
}
|
||
|
|
off += n;
|
||
|
|
if ((n == 1 && arg < BigInt.from(24)) ||
|
||
|
|
(n > 1 && arg >> (4 * n) == BigInt.zero)) {
|
||
|
|
throw FormatException('$arg not in its shortest form');
|
||
|
|
}
|
||
|
|
return (major, arg);
|
||
|
|
}
|
||
|
|
|
||
|
|
Uint8List bytes(BigInt n) {
|
||
|
|
if (n > BigInt.from(b.length - off)) {
|
||
|
|
throw const FormatException('truncated');
|
||
|
|
}
|
||
|
|
final s = Uint8List.fromList(b.sublist(off, off + n.toInt()));
|
||
|
|
off += n.toInt();
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
Object? value(int depth) {
|
||
|
|
final (major, arg) = head();
|
||
|
|
switch (major) {
|
||
|
|
case 0:
|
||
|
|
return arg;
|
||
|
|
case 2:
|
||
|
|
return bytes(arg);
|
||
|
|
case 3:
|
||
|
|
final s = bytes(arg);
|
||
|
|
// dart:convert is strict about everything but a leading U+FEFF,
|
||
|
|
// which it drops: the text keeps its bytes.
|
||
|
|
const Utf8Decoder().convert(s);
|
||
|
|
return Text(s);
|
||
|
|
}
|
||
|
|
if (depth >= maxDepth) throw const FormatException('nested too deep');
|
||
|
|
if (arg > BigInt.from(b.length - off)) {
|
||
|
|
throw const FormatException('truncated');
|
||
|
|
}
|
||
|
|
final count = arg.toInt();
|
||
|
|
if (major == 4) {
|
||
|
|
return [for (var i = 0; i < count; i++) value(depth + 1)];
|
||
|
|
}
|
||
|
|
final out = <BigInt, Object?>{};
|
||
|
|
BigInt? last;
|
||
|
|
for (var i = 0; i < count; i++) {
|
||
|
|
final (km, k) = head();
|
||
|
|
if (km != 0) throw FormatException('map key of major type $km');
|
||
|
|
if (last != null && k <= last) {
|
||
|
|
throw FormatException('map key $k after $last');
|
||
|
|
}
|
||
|
|
last = k;
|
||
|
|
out[k] = value(depth + 1);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The nesting depth of [v], a value of [marshal] or [unmarshal], and the
|
||
|
|
/// length of its longest string or container: bytes of a string, items of an
|
||
|
|
/// array, entries of a map.
|
||
|
|
(int, int) shape(Object? v) {
|
||
|
|
final Iterable<Object?> items;
|
||
|
|
switch (v) {
|
||
|
|
case final Uint8List b:
|
||
|
|
return (0, b.length);
|
||
|
|
case final String s:
|
||
|
|
return (0, utf8.encode(s).length);
|
||
|
|
case final Text t:
|
||
|
|
return (0, t.bytes.length);
|
||
|
|
case final List<Object?> l:
|
||
|
|
items = l;
|
||
|
|
case final Map<Object, Object?> m:
|
||
|
|
items = m.values;
|
||
|
|
default:
|
||
|
|
return (0, 0);
|
||
|
|
}
|
||
|
|
var depth = 0;
|
||
|
|
var length = items.length;
|
||
|
|
for (final x in items) {
|
||
|
|
final (d, l) = shape(x);
|
||
|
|
if (d > depth) depth = d;
|
||
|
|
if (l > length) length = l;
|
||
|
|
}
|
||
|
|
return (depth + 1, length);
|
||
|
|
}
|