- lib/src/errors.dart, the port of errors.go: ErrorCode, the 19 codes in
the order of section 69, and DateKeysException, which carries one of
them with the message of the reference, the context and then the code.
wrap and withContext are fmt.Errorf("prefix: %w"), errorCode is
datekeys.Code. test/errors_spec_test.dart reads section 69 from
../datekeys-go at the tag spec-v0.11 and compares its ERR_ lines with
the catalogue; it is skipped when that repository is missing.
- lib/src/bytes.dart, as bytes.ts of datekeys-ts: hexadecimal,
comparison and concatenation; strict UTF-8 that keeps a leading
U+FEFF, which the Utf8Decoder of dart:convert drops on the VM and on
the web; Go's utf8.DecodeRune; and Go's %q, with the table of
strconv.IsPrint of Go 1.26 copied from datekeys-ts and the SHA-256 of
the whole rune set pinned. A lone surrogate, which a Dart String may
hold, is written in generalized UTF-8, which is not UTF-8.
- lib/datekeys.dart exports the errors and the byte functions that a
user needs.
- The tests that read files are marked @TestOn('vm'), those of stage 0
included, so that the others also run compiled to JavaScript with
dart test -p node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
parent
5aa5eedcc1
commit
da63bc394c
@ -0,0 +1,379 @@
|
|||||||
|
/// Byte helpers of the codec and of the schemas: hexadecimal, comparison and
|
||||||
|
/// concatenation, UTF-8 as strict as Go's, and Go's `%q`, which the error
|
||||||
|
/// texts of the reference use. As bytes.ts of datekeys-ts.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
const _hexDigits = '0123456789abcdef';
|
||||||
|
|
||||||
|
/// The lowercase hexadecimal of [bytes], as Go's hex.EncodeToString.
|
||||||
|
String toHex(List<int> bytes) {
|
||||||
|
final out = StringBuffer();
|
||||||
|
for (final b in bytes) {
|
||||||
|
out
|
||||||
|
..write(_hexDigits[b >> 4])
|
||||||
|
..write(_hexDigits[b & 15]);
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bytes of the hexadecimal [s], in lower or upper case. Throws a
|
||||||
|
/// [FormatException] unless [s] is an even number of hexadecimal digits.
|
||||||
|
Uint8List fromHex(String s) {
|
||||||
|
if (s.length.isOdd) {
|
||||||
|
throw FormatException('hexadecimal of odd length ${s.length}', s);
|
||||||
|
}
|
||||||
|
final out = Uint8List(s.length ~/ 2);
|
||||||
|
for (var i = 0; i < out.length; i++) {
|
||||||
|
out[i] = _nibble(s, 2 * i) << 4 | _nibble(s, 2 * i + 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _nibble(String s, int i) {
|
||||||
|
final c = s.codeUnitAt(i);
|
||||||
|
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
||||||
|
final lower = c | 0x20;
|
||||||
|
if (lower >= 0x61 && lower <= 0x66) return lower - 0x61 + 10;
|
||||||
|
throw FormatException('not a hexadecimal digit', s, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [a] and [b] hold the same bytes.
|
||||||
|
bool equalBytes(List<int> a, List<int> b) {
|
||||||
|
if (a.length != b.length) return false;
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Orders byte strings as Go's bytes.Compare: -1, 0 or 1, byte by byte and
|
||||||
|
/// then by length.
|
||||||
|
int compareBytes(List<int> a, List<int> b) {
|
||||||
|
final n = a.length < b.length ? a.length : b.length;
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
if (a[i] != b[i]) return a[i] < b[i] ? -1 : 1;
|
||||||
|
}
|
||||||
|
return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The concatenation of [parts].
|
||||||
|
Uint8List concatBytes(Iterable<List<int>> parts) {
|
||||||
|
var n = 0;
|
||||||
|
for (final p in parts) {
|
||||||
|
n += p.length;
|
||||||
|
}
|
||||||
|
final out = Uint8List(n);
|
||||||
|
var o = 0;
|
||||||
|
for (final p in parts) {
|
||||||
|
out.setRange(o, o + p.length, p);
|
||||||
|
o += p.length;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UTF-8
|
||||||
|
|
||||||
|
/// Go's utf8.RuneError, U+FFFD.
|
||||||
|
const _runeError = 0xfffd;
|
||||||
|
|
||||||
|
/// Go's utf8.DecodeRune of [bytes] from [offset]: the rune and the number of
|
||||||
|
/// bytes it takes. An invalid or truncated sequence is `(0xFFFD, 1)`, and the
|
||||||
|
/// end of the input `(0xFFFD, 0)`. Surrogates, overlong forms and code points
|
||||||
|
/// above U+10FFFF are invalid.
|
||||||
|
(int, int) decodeRune(List<int> bytes, int offset) {
|
||||||
|
final n = bytes.length - offset;
|
||||||
|
if (n < 1) return (_runeError, 0);
|
||||||
|
final b0 = bytes[offset];
|
||||||
|
if (b0 < 0x80) return (b0, 1);
|
||||||
|
// The size of the sequence and the range of its second byte, as the
|
||||||
|
// tables first and acceptRanges of Go's unicode/utf8.
|
||||||
|
final int size;
|
||||||
|
var lo = 0x80;
|
||||||
|
var hi = 0xbf;
|
||||||
|
if (b0 < 0xc2) {
|
||||||
|
return (_runeError, 1);
|
||||||
|
} else if (b0 < 0xe0) {
|
||||||
|
size = 2;
|
||||||
|
} else if (b0 < 0xf0) {
|
||||||
|
size = 3;
|
||||||
|
if (b0 == 0xe0) lo = 0xa0;
|
||||||
|
if (b0 == 0xed) hi = 0x9f;
|
||||||
|
} else if (b0 < 0xf5) {
|
||||||
|
size = 4;
|
||||||
|
if (b0 == 0xf0) lo = 0x90;
|
||||||
|
if (b0 == 0xf4) hi = 0x8f;
|
||||||
|
} else {
|
||||||
|
return (_runeError, 1);
|
||||||
|
}
|
||||||
|
if (n < size) return (_runeError, 1);
|
||||||
|
final b1 = bytes[offset + 1];
|
||||||
|
if (b1 < lo || b1 > hi) return (_runeError, 1);
|
||||||
|
if (size == 2) return ((b0 & 0x1f) << 6 | b1 & 0x3f, 2);
|
||||||
|
final b2 = bytes[offset + 2];
|
||||||
|
if (b2 < 0x80 || b2 > 0xbf) return (_runeError, 1);
|
||||||
|
if (size == 3) return ((b0 & 0x0f) << 12 | (b1 & 0x3f) << 6 | b2 & 0x3f, 3);
|
||||||
|
final b3 = bytes[offset + 3];
|
||||||
|
if (b3 < 0x80 || b3 > 0xbf) return (_runeError, 1);
|
||||||
|
return (
|
||||||
|
(b0 & 0x07) << 18 | (b1 & 0x3f) << 12 | (b2 & 0x3f) << 6 | b3 & 0x3f,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [bytes] are valid UTF-8, as Go's utf8.Valid.
|
||||||
|
bool isValidUtf8(List<int> bytes) {
|
||||||
|
for (var i = 0; i < bytes.length;) {
|
||||||
|
if (bytes[i] < 0x80) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final (_, size) = decodeRune(bytes, i);
|
||||||
|
// A sequence that starts above U+007F is one byte only when invalid.
|
||||||
|
if (size == 1) return false;
|
||||||
|
i += size;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes [bytes] when they are valid UTF-8, as Go's utf8.Valid has it, and
|
||||||
|
/// returns null otherwise: no byte is replaced. A leading U+FEFF is kept as a
|
||||||
|
/// character. dart:convert's Utf8Decoder is as strict about everything else,
|
||||||
|
/// but drops a leading U+FEFF, on the VM and on the web alike, and two texts
|
||||||
|
/// that differ in it would decode the same.
|
||||||
|
String? decodeUtf8(List<int> bytes) {
|
||||||
|
final runes = <int>[];
|
||||||
|
for (var i = 0; i < bytes.length;) {
|
||||||
|
final b = bytes[i];
|
||||||
|
if (b < 0x80) {
|
||||||
|
runes.add(b);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final (r, size) = decodeRune(bytes, i);
|
||||||
|
if (size == 1) return null;
|
||||||
|
runes.add(r);
|
||||||
|
i += size;
|
||||||
|
}
|
||||||
|
return String.fromCharCodes(runes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The UTF-8 bytes of [s]. A lone surrogate, which a Dart String may hold and
|
||||||
|
/// UTF-8 cannot, takes the three bytes of its code point, as the generalized
|
||||||
|
/// UTF-8 of WTF-8 writes it (U+D800 is ED A0 80): bytes that are not valid
|
||||||
|
/// UTF-8, so that whoever checks them rejects the string as Go rejects those
|
||||||
|
/// bytes. dart:convert writes U+FFFD in its place instead.
|
||||||
|
Uint8List utf8Bytes(String s) {
|
||||||
|
var n = 0;
|
||||||
|
for (var i = 0; i < s.length; i++) {
|
||||||
|
final c = s.codeUnitAt(i);
|
||||||
|
if (c < 0x80) {
|
||||||
|
n += 1;
|
||||||
|
} else if (c < 0x800) {
|
||||||
|
n += 2;
|
||||||
|
} else if (_surrogatePairAt(s, i)) {
|
||||||
|
n += 4;
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
n += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final out = Uint8List(n);
|
||||||
|
var o = 0;
|
||||||
|
for (var i = 0; i < s.length; i++) {
|
||||||
|
final c = s.codeUnitAt(i);
|
||||||
|
if (c < 0x80) {
|
||||||
|
out[o++] = c;
|
||||||
|
} else if (c < 0x800) {
|
||||||
|
out[o++] = 0xc0 | c >> 6;
|
||||||
|
out[o++] = 0x80 | c & 0x3f;
|
||||||
|
} else if (_surrogatePairAt(s, i)) {
|
||||||
|
final r = 0x10000 + ((c & 0x3ff) << 10 | s.codeUnitAt(++i) & 0x3ff);
|
||||||
|
out[o++] = 0xf0 | r >> 18;
|
||||||
|
out[o++] = 0x80 | r >> 12 & 0x3f;
|
||||||
|
out[o++] = 0x80 | r >> 6 & 0x3f;
|
||||||
|
out[o++] = 0x80 | r & 0x3f;
|
||||||
|
} else {
|
||||||
|
out[o++] = 0xe0 | c >> 12;
|
||||||
|
out[o++] = 0x80 | c >> 6 & 0x3f;
|
||||||
|
out[o++] = 0x80 | c & 0x3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [s] holds no lone surrogate: whether it has a UTF-8 encoding.
|
||||||
|
bool isWellFormedUtf16(String s) {
|
||||||
|
for (var i = 0; i < s.length; i++) {
|
||||||
|
final c = s.codeUnitAt(i);
|
||||||
|
if (c & 0xf800 != 0xd800) continue;
|
||||||
|
if (!_surrogatePairAt(s, i)) return false;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether a high surrogate at i is followed by a low surrogate.
|
||||||
|
bool _surrogatePairAt(String s, int i) {
|
||||||
|
final c = s.codeUnitAt(i);
|
||||||
|
return c >= 0xd800 &&
|
||||||
|
c <= 0xdbff &&
|
||||||
|
i + 1 < s.length &&
|
||||||
|
s.codeUnitAt(i + 1) & 0xfc00 == 0xdc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Go's %q
|
||||||
|
|
||||||
|
// Go's strconv.IsPrint above U+007F (letters, marks, numbers, punctuation and
|
||||||
|
// symbols), generated from strconv.IsPrint of Go 1.26, the toolchain of the
|
||||||
|
// reference implementation (Unicode 15.0.0: 710 ranges, 148 903 runes). A
|
||||||
|
// fixed table, the same as GO_PRINTABLE_RANGES of datekeys-ts, rather than
|
||||||
|
// the Unicode data of the platform, which follows a version of its own. Pairs
|
||||||
|
// of (gap from the end of the previous range, length) in base 36, from
|
||||||
|
// U+0080, printed by `go run scripts/go-isprint-table.go` of datekeys-ts;
|
||||||
|
// bytes_test.dart pins the SHA-256 of the whole rune set.
|
||||||
|
const _goPrintableRanges =
|
||||||
|
'x,c,1,ju,2,6,4,7,1,1,1,k,1,b1,1,12,2,1e,2,3,1,1j,8,r,4,6,h,m,1,5c,1,1c,'
|
||||||
|
'2,1n,2,2t,e,1n,2,1d,2,f,1,s,2,1,1,b,5,v,9,22,1,4h,1,8,2,2,2,m,1,7,1,1,3,'
|
||||||
|
'4,2,9,2,2,2,4,8,1,4,2,1,5,2,p,2,3,1,6,4,2,2,m,1,7,1,2,1,2,1,2,2,1,1,5,4,'
|
||||||
|
'2,2,3,3,1,7,4,1,1,7,h,a,3,1,9,1,3,1,m,1,7,1,2,1,5,2,a,1,3,1,3,2,1,f,4,2,'
|
||||||
|
'c,7,7,1,3,1,8,2,2,2,m,1,7,1,2,1,5,2,9,2,2,2,3,7,3,4,2,1,5,2,i,a,2,1,6,3,'
|
||||||
|
'3,1,4,3,2,1,1,1,2,3,2,3,3,3,c,4,5,3,3,1,4,2,1,6,1,e,l,5,d,1,3,1,n,1,g,2,'
|
||||||
|
'9,1,3,1,4,7,2,1,3,2,1,2,4,2,a,7,m,1,3,1,n,1,a,1,5,2,9,1,3,1,4,7,2,6,2,1,'
|
||||||
|
'4,2,a,1,3,c,d,1,3,1,1f,1,3,1,6,4,g,2,q,1,3,1,i,3,o,1,9,1,1,2,7,3,1,4,6,'
|
||||||
|
'1,1,1,8,6,a,2,3,c,1m,4,t,11,2,1,1,1,5,1,o,1,1,1,n,2,5,1,1,1,7,1,a,2,4,w,'
|
||||||
|
'20,1,10,4,13,1,10,1,f,1,d,11,5i,1,1,5,1,2,ah,1,4,2,7,1,1,1,4,2,15,1,4,2,'
|
||||||
|
'x,1,4,2,7,1,1,1,4,2,f,1,1l,1,4,2,1v,2,w,3,q,6,2e,2,6,2,hs,1,s,3,2h,7,m,'
|
||||||
|
'9,o,9,k,c,d,1,3,1,2,c,2m,2,a,6,a,6,e,1,b,6,2h,7,17,5,1y,a,v,1,c,4,c,4,1,'
|
||||||
|
'3,16,2,5,b,18,4,q,6,b,3,1q,2,1t,1,t,2,b,6,a,6,e,2,v,1d,25,3,1b,1,38,8,'
|
||||||
|
'1o,3,f,3,1o,7,17,2,b,8,17,5,eu,2,6,2,12,2,6,2,8,1,1,1,1,1,1,1,v,2,1h,1,'
|
||||||
|
'f,1,e,2,6,1,j,2,3,1,9,h,o,8,1b,h,2,2,r,1,d,3,x,f,x,f,3w,4,if,p,b,l,1ec,'
|
||||||
|
'2,w,1,9p,5,19,1,1,5,1,2,1k,7,2,e,o,9,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,3i,'
|
||||||
|
'y,q,1,2h,c,5y,q,c,5,1r,1,2e,2,2v,5,17,1,2m,1,2c,c,1b,1,mlp,3,1j,9,9o,k,'
|
||||||
|
'54,8,5n,5,2,1,1,1,5,o,1n,3,a,6,1k,8,1y,8,c,6,38,b,u,3,26,1,b,4,x,1,1j,9,'
|
||||||
|
'e,2,a,2,2v,o,s,a,6,2,6,2,6,9,7,1,7,1,1o,4,3i,2,a,6,8mc,c,n,4,1d,6is,a6,'
|
||||||
|
'2,2y,12,7,c,5,5,q,1,5,1,1,1,2,1,2,1,3h,g,cd,2,1i,7,1,w,16,6,1f,1,j,1,4,'
|
||||||
|
'4,5,1,3r,4,5a,3,6,2,6,2,6,2,3,3,7,1,7,d,2,2,c,1,q,1,j,1,2,1,f,2,e,y,3f,'
|
||||||
|
'5,3,4,19,3,2g,1,d,3,1,1b,1a,3m,t,3,1d,f,s,4,10,9,u,5,17,5,u,1,11,4,e,16,'
|
||||||
|
'4e,2,a,6,10,4,10,4,14,8,1g,b,c,1,f,1,7,1,2,1,b,1,f,1,7,1,2,1v,8n,9,m,a,'
|
||||||
|
'8,o,6,1,16,1,9,1x,6,2,1,1,18,1,2,3,1,2,n,1,20,8,9,1c,j,1,2,5,x,3,r,5,1,'
|
||||||
|
'1s,1k,4,k,2,1e,1,2,5,8,1,3,1,t,2,3,4,a,7,9,7,1s,w,13,4,c,9,1i,3,t,2,r,5,'
|
||||||
|
'q,7,4,c,7,28,21,1j,1f,d,1f,7,1a,8,a,86,v,1,16,1,3,2,2,23,17,8,16,m,q,12,'
|
||||||
|
's,k,n,9,26,4,10,9,1q,1,5,d,p,7,a,6,1h,1,i,8,13,9,2o,1,k,b,i,1,1b,1q,7,1,'
|
||||||
|
'1,1,4,1,f,1,b,6,1n,5,a,6,4,1,8,2,2,2,m,1,7,1,2,1,5,1,a,2,2,2,3,2,1,6,1,'
|
||||||
|
'5,7,2,7,3,5,3v,2k,1,5,u,20,8,a,4m,1i,2,12,y,1x,b,a,6,d,j,1m,6,a,1i,r,2,'
|
||||||
|
'f,4,n,55,1o,2s,2b,c,8,2,1,2,8,1,2,1,u,1,2,2,c,9,a,1y,8,2,1a,2,b,r,20,8,'
|
||||||
|
'2b,d,21,7,a,6u,9,1,19,1,e,a,t,3,w,2,m,1,e,21,7,1,2,1,18,3,1,1,2,1,9,8,a,'
|
||||||
|
'6,6,1,2,1,11,1,2,1,6,7,a,8m,p,7,h,1,15,3,s,2e,1,f,1e,d,pn,2u,33,1,5,b,'
|
||||||
|
'5g,218,2r,d,ts,g,m,33e,g7,6nt,ft,7,v,1,a,4,29,1,a,6,u,2,6,a,1y,a,a,1,7,'
|
||||||
|
'1,l,5,j,j4,2j,2t,23,4,1l,7,h,1s,5,b,2,e,4qg,8,ye,16,9,6w7,4,1,7,1,2,1,'
|
||||||
|
'83,f,1,t,3,2,1,e,4,8,b0,1s4,2z,5,d,3,9,7,a,2,4,3mo,1a,2,n,9,38,1o,6u,a,'
|
||||||
|
'13,2,22,8,34,l,1y,3e,k,c,k,c,2f,9,p,3r,2d,1,1z,1,2,2,1,2,2,2,4,1,c,1,1,'
|
||||||
|
'1,7,1,1t,1,4,2,8,1,7,1,s,1,4,1,5,1,1,3,7,1,9g,2,84,2,ji,f,5,1,f,uo,v,6,'
|
||||||
|
'6,5x,7,1,h,2,7,1,2,1,5,5,1q,x,1,34,19,3,e,2,a,4,2,8w,v,h,1m,5,1,cw,16,'
|
||||||
|
'km,7,1,4,1,2,1,f,1,5h,2,g,15,24,4,a,4,2,lt,1w,24,1p,5e,4,1,r,1,2,1,1,2,'
|
||||||
|
'1,1,a,1,4,1,1,1,1,6,1,4,1,1,1,1,1,1,3,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,'
|
||||||
|
'1,2,4,1,7,1,4,1,4,1,1,1,a,1,h,5,3,1,5,1,h,1g,2,7i,18,4,2s,c,f,2,f,1,f,1,'
|
||||||
|
'11,a,4u,1k,t,d,18,4,9,7,2,e,6,4a,rc,4,h,3,d,3,3b,4,2n,6,c,4,1,f,c,4,1k,'
|
||||||
|
'8,a,6,14,8,u,2,2,26,9g,c,e,2,d,3,9,7,1a,1,7,8,e,4,9,7,9,7,43,1,1j,11,a,'
|
||||||
|
'sm,wyo,w,37e,6,66,2,4g2,e,5rl,2e7,f2,15u,3t7,5,38g,f9e8,6o';
|
||||||
|
|
||||||
|
// The ranges of _goPrintableRanges, decoded on first use: the first and the
|
||||||
|
// last rune of range k at 2k and 2k + 1.
|
||||||
|
final Int32List _printable = () {
|
||||||
|
final nums = _goPrintableRanges.split(',');
|
||||||
|
final ranges = Int32List(nums.length);
|
||||||
|
var next = 0x80;
|
||||||
|
for (var i = 0; i < nums.length; i += 2) {
|
||||||
|
final first = next + int.parse(nums[i], radix: 36);
|
||||||
|
final last = first + int.parse(nums[i + 1], radix: 36) - 1;
|
||||||
|
ranges[i] = first;
|
||||||
|
ranges[i + 1] = last;
|
||||||
|
next = last + 1;
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}();
|
||||||
|
|
||||||
|
/// Go's strconv.IsPrint of the rune [r], with the Unicode tables of Go 1.26:
|
||||||
|
/// the ASCII space, letters, marks, numbers, punctuation and symbols.
|
||||||
|
bool isPrint(int r) {
|
||||||
|
if (r < 0x80) return r >= 0x20 && r < 0x7f;
|
||||||
|
// The last range whose first rune is at most r.
|
||||||
|
var lo = 0;
|
||||||
|
var hi = _printable.length ~/ 2;
|
||||||
|
while (lo < hi) {
|
||||||
|
final mid = (lo + hi) >> 1;
|
||||||
|
if (_printable[2 * mid] <= r) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lo > 0 && r <= _printable[2 * lo - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go's `%q` (strconv.Quote) of [bytes] taken as a Go string, so that error
|
||||||
|
/// texts match the reference: printable runes as they are, other runes
|
||||||
|
/// escaped, and each byte that is not valid UTF-8 as `\xHH`. A Dart String is
|
||||||
|
/// quoted as its [utf8Bytes].
|
||||||
|
String goQuote(List<int> bytes) {
|
||||||
|
final out = StringBuffer('"');
|
||||||
|
for (var i = 0; i < bytes.length;) {
|
||||||
|
final (r, size) = decodeRune(bytes, i);
|
||||||
|
if (r == _runeError && size == 1) {
|
||||||
|
out
|
||||||
|
..write('\\x')
|
||||||
|
..write(_hex(bytes[i], 2));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += size;
|
||||||
|
if (r == 0x22 || r == 0x5c) {
|
||||||
|
out
|
||||||
|
..write('\\')
|
||||||
|
..writeCharCode(r);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isPrint(r)) {
|
||||||
|
out.writeCharCode(r);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (r) {
|
||||||
|
case 0x07:
|
||||||
|
out.write('\\a');
|
||||||
|
case 0x08:
|
||||||
|
out.write('\\b');
|
||||||
|
case 0x0c:
|
||||||
|
out.write('\\f');
|
||||||
|
case 0x0a:
|
||||||
|
out.write('\\n');
|
||||||
|
case 0x0d:
|
||||||
|
out.write('\\r');
|
||||||
|
case 0x09:
|
||||||
|
out.write('\\t');
|
||||||
|
case 0x0b:
|
||||||
|
out.write('\\v');
|
||||||
|
default:
|
||||||
|
if (r < 0x20 || r == 0x7f) {
|
||||||
|
out
|
||||||
|
..write('\\x')
|
||||||
|
..write(_hex(r, 2));
|
||||||
|
} else if (r < 0x10000) {
|
||||||
|
out
|
||||||
|
..write('\\u')
|
||||||
|
..write(_hex(r, 4));
|
||||||
|
} else {
|
||||||
|
out
|
||||||
|
..write('\\U')
|
||||||
|
..write(_hex(r, 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write('"');
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _hex(int v, int digits) => v.toRadixString(16).padLeft(digits, '0');
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
/// The normative errors of spec §69, as errors.go of datekeys-go.
|
||||||
|
///
|
||||||
|
/// Every failure of the protocol that this library reports is a
|
||||||
|
/// [DateKeysException] carrying exactly one [ErrorCode]. Its message follows
|
||||||
|
/// the convention of the Go reference: the context, then `: ` and the code,
|
||||||
|
/// such as `capsule: truncated prelude: ERR_INTEGRITY`. An exception, not an
|
||||||
|
/// [Error]: in Dart an Error is a bug of the program, and these are failures
|
||||||
|
/// of the input.
|
||||||
|
library;
|
||||||
|
|
||||||
|
/// The normative error codes, in the order of spec §69.
|
||||||
|
enum ErrorCode {
|
||||||
|
/// The object does not start with DKC1 or DKK1 (spec §22, §40).
|
||||||
|
invalidMagic('ERR_INVALID_MAGIC'),
|
||||||
|
|
||||||
|
/// An unknown framing or schema version (spec §22, §70).
|
||||||
|
unsupportedVersion('ERR_UNSUPPORTED_VERSION'),
|
||||||
|
|
||||||
|
/// FLAGS or RESERVED are not zero (spec §22, §40).
|
||||||
|
invalidFlags('ERR_INVALID_FLAGS'),
|
||||||
|
|
||||||
|
/// The bytes are not the unique deterministic CBOR encoding of a valid
|
||||||
|
/// instance of the normative schema (spec §58, §58.1).
|
||||||
|
nonCanonicalCbor('ERR_NON_CANONICAL_CBOR'),
|
||||||
|
|
||||||
|
/// The DateKey names a profile that is not pinned locally (spec §13).
|
||||||
|
unknownProfile('ERR_UNKNOWN_PROFILE'),
|
||||||
|
|
||||||
|
/// A chain hash or profile does not match the pinned profile (spec §35,
|
||||||
|
/// §63).
|
||||||
|
profileMismatch('ERR_PROFILE_MISMATCH'),
|
||||||
|
|
||||||
|
/// A DateKey that cannot be decoded or validated (spec §18, §19).
|
||||||
|
dateKeyInvalid('ERR_DATEKEY_INVALID'),
|
||||||
|
|
||||||
|
/// A valid DateKey in a non-canonical encoding (spec §19).
|
||||||
|
dateKeyNonCanonical('ERR_DATEKEY_NON_CANONICAL'),
|
||||||
|
|
||||||
|
/// A round that differs from the locally resolved one (spec §17, §63).
|
||||||
|
roundMismatch('ERR_ROUND_MISMATCH'),
|
||||||
|
|
||||||
|
/// The release is not published yet or no source delivered it (spec §45 to
|
||||||
|
/// §50).
|
||||||
|
releaseUnavailable('ERR_RELEASE_UNAVAILABLE'),
|
||||||
|
|
||||||
|
/// A release that fails local verification (spec §51).
|
||||||
|
releaseInvalid('ERR_RELEASE_INVALID'),
|
||||||
|
|
||||||
|
/// The policy requires an access credential and none was supplied (spec
|
||||||
|
/// §33).
|
||||||
|
accessRequired('ERR_ACCESS_REQUIRED'),
|
||||||
|
|
||||||
|
/// The supplied credentials do not open this capsule (spec §33, §38).
|
||||||
|
accessInvalid('ERR_ACCESS_INVALID'),
|
||||||
|
|
||||||
|
/// The cryptographic structure does not match the declared access policy
|
||||||
|
/// or the stanza rules of V1 (spec §25, §29, §32, §33, §36).
|
||||||
|
policyStructureMismatch('ERR_POLICY_STRUCTURE_MISMATCH'),
|
||||||
|
|
||||||
|
/// header_binding does not match PRELUDE || PUBLIC_HEADER (spec §26).
|
||||||
|
headerBinding('ERR_HEADER_BINDING'),
|
||||||
|
|
||||||
|
/// Truncation, corruption or failed authentication of framing or age data
|
||||||
|
/// (spec §4, §55).
|
||||||
|
integrity('ERR_INTEGRITY'),
|
||||||
|
|
||||||
|
/// A critical extension this implementation does not know (spec §54).
|
||||||
|
extensionCriticalUnknown('ERR_EXTENSION_CRITICAL_UNKNOWN'),
|
||||||
|
|
||||||
|
/// A known extension whose data does not follow its registered schema. It
|
||||||
|
/// rejects the object only for a critical extension; a noncritical one is
|
||||||
|
/// reported as unusable (spec §54, §72).
|
||||||
|
extensionDataInvalid('ERR_EXTENSION_DATA_INVALID'),
|
||||||
|
|
||||||
|
/// The head of a format 3 capsule is well encoded but one of its fields
|
||||||
|
/// breaks its rules: a path, the comment, the declared author or the layout
|
||||||
|
/// of the files (spec §29.4 to §29.6).
|
||||||
|
headInvalid('ERR_HEAD_INVALID');
|
||||||
|
|
||||||
|
const ErrorCode(this.code);
|
||||||
|
|
||||||
|
/// The code as spec §69 writes it, such as `ERR_INVALID_MAGIC`.
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A normative DateKeys error (spec §69): a failure of the protocol, with
|
||||||
|
/// exactly one [code].
|
||||||
|
final class DateKeysException implements Exception {
|
||||||
|
/// An exception of [code] whose message is `context: CODE`, or the bare
|
||||||
|
/// code when [context] is empty.
|
||||||
|
DateKeysException(this.code, [String context = ''])
|
||||||
|
: message = context.isEmpty ? code.code : '$context: ${code.code}';
|
||||||
|
|
||||||
|
const DateKeysException._(this.code, this.message);
|
||||||
|
|
||||||
|
/// The normative code.
|
||||||
|
final ErrorCode code;
|
||||||
|
|
||||||
|
/// The text of the error, as Go's err.Error() of the reference: it always
|
||||||
|
/// ends with the code.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Returns this error with `prefix: ` before its message and the same code,
|
||||||
|
/// as Go's `fmt.Errorf("prefix: %w", err)`.
|
||||||
|
DateKeysException wrap(String prefix) =>
|
||||||
|
DateKeysException._(code, '$prefix: $message');
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The normative code of [error], such as `ERR_INTEGRITY`, or `''` when it is
|
||||||
|
/// not a [DateKeysException]: Go's datekeys.Code.
|
||||||
|
String errorCode(Object? error) =>
|
||||||
|
error is DateKeysException ? error.code.code : '';
|
||||||
|
|
||||||
|
/// Runs [body] and puts `prefix: ` before the message of a
|
||||||
|
/// [DateKeysException] that it throws, keeping its stack trace: Go's
|
||||||
|
/// `fmt.Errorf("prefix: %w", err)`. Other exceptions and errors pass through
|
||||||
|
/// unchanged.
|
||||||
|
T withContext<T>(String prefix, T Function() body) {
|
||||||
|
try {
|
||||||
|
return body();
|
||||||
|
} on DateKeysException catch (e, stack) {
|
||||||
|
Error.throwWithStackTrace(e.wrap(prefix), stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
// The byte helpers: hexadecimal, comparison, UTF-8 as strict as Go's, Go's
|
||||||
|
// utf8.DecodeRune and Go's %q, as bytes.test.ts of datekeys-ts. The texts of
|
||||||
|
// %q are those printed by Go 1.26.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:datekeys/src/bytes.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// One backslash, for the texts of Go's %q.
|
||||||
|
const bs = '\\';
|
||||||
|
|
||||||
|
String cp(List<int> runes) => String.fromCharCodes(runes);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('converts hexadecimal', () {
|
||||||
|
expect(toHex([0, 1, 0xab, 0xff]), '0001abff');
|
||||||
|
expect(toHex([]), '');
|
||||||
|
expect(fromHex('0001ABff'), [0, 1, 0xab, 0xff]);
|
||||||
|
expect(fromHex(''), isEmpty);
|
||||||
|
expect(() => fromHex('abc'), throwsFormatException);
|
||||||
|
expect(() => fromHex('zz'), throwsFormatException);
|
||||||
|
expect(() => fromHex('0g'), throwsFormatException);
|
||||||
|
expect(() => fromHex('${cp([0xff10])}0'), throwsFormatException);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('compares and joins byte strings', () {
|
||||||
|
final a = [1, 2];
|
||||||
|
expect(equalBytes(a, [1, 2]), isTrue);
|
||||||
|
expect(equalBytes(a, [1, 3]), isFalse);
|
||||||
|
expect(equalBytes(a, [1]), isFalse);
|
||||||
|
expect(compareBytes(a, [1, 2]), 0);
|
||||||
|
expect(compareBytes(a, [1, 3]), -1);
|
||||||
|
expect(compareBytes([2], a), 1);
|
||||||
|
expect(compareBytes([1], a), -1);
|
||||||
|
expect(compareBytes(a, [1]), 1);
|
||||||
|
expect(compareBytes([0xff], [0x00, 0x00]), 1);
|
||||||
|
expect(
|
||||||
|
concatBytes([
|
||||||
|
a,
|
||||||
|
<int>[],
|
||||||
|
Uint8List.fromList([3]),
|
||||||
|
]),
|
||||||
|
[1, 2, 3],
|
||||||
|
);
|
||||||
|
expect(concatBytes([]), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('UTF-8', () {
|
||||||
|
// Each byte string that UTF-8 forbids, and what Go's utf8.Valid refuses.
|
||||||
|
final invalid = <String, List<int>>{
|
||||||
|
'an invalid byte': [0x61, 0xff],
|
||||||
|
'a byte above F4': [0xf5, 0x80, 0x80, 0x80],
|
||||||
|
'an overlong NUL': [0xc0, 0x80],
|
||||||
|
'an overlong three-byte form': [0xe0, 0x80, 0x80],
|
||||||
|
'an overlong four-byte form': [0xf0, 0x80, 0x80, 0x80],
|
||||||
|
'an encoded high surrogate': [0xed, 0xa0, 0x80],
|
||||||
|
'an encoded low surrogate': [0xed, 0xbf, 0xbf],
|
||||||
|
'a code point above U+10FFFF': [0xf4, 0x90, 0x80, 0x80],
|
||||||
|
'a truncated sequence': [0xe2, 0x82],
|
||||||
|
'a lone continuation byte': [0x80],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('dart:convert refuses what Go refuses, and so does decodeUtf8', () {
|
||||||
|
for (final MapEntry(key: name, value: b) in invalid.entries) {
|
||||||
|
// Short inputs and long ones: dart2js decodes the long ones with the
|
||||||
|
// TextDecoder of the platform.
|
||||||
|
for (final input in [
|
||||||
|
b,
|
||||||
|
[...b, ...List.filled(40, 0x61)],
|
||||||
|
]) {
|
||||||
|
expect(
|
||||||
|
() => const Utf8Decoder().convert(input),
|
||||||
|
throwsFormatException,
|
||||||
|
reason: name,
|
||||||
|
);
|
||||||
|
expect(decodeUtf8(input), isNull, reason: name);
|
||||||
|
expect(isValidUtf8(input), isFalse, reason: name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dart:convert drops a leading U+FEFF, and decodeUtf8 keeps it', () {
|
||||||
|
// The reason decodeUtf8 does not use Utf8Decoder: a text that starts
|
||||||
|
// with U+FEFF would decode as one that does not, on the VM and on the
|
||||||
|
// web alike.
|
||||||
|
for (final rest in [
|
||||||
|
[0x61],
|
||||||
|
List.filled(40, 0x61),
|
||||||
|
]) {
|
||||||
|
final input = [0xef, 0xbb, 0xbf, ...rest];
|
||||||
|
expect(const Utf8Decoder().convert(input), cp(rest));
|
||||||
|
expect(decodeUtf8(input), cp([0xfeff, ...rest]));
|
||||||
|
}
|
||||||
|
// Further ones are characters for both.
|
||||||
|
expect(
|
||||||
|
decodeUtf8([0x61, 0xef, 0xbb, 0xbf, 0xef, 0xbb, 0xbf]),
|
||||||
|
cp([0x61, 0xfeff, 0xfeff]),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
decodeUtf8([0xef, 0xbb, 0xbf, 0xef, 0xbb, 0xbf]),
|
||||||
|
cp([0xfeff, 0xfeff]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodes every valid sequence, U+FFFD and U+10FFFF included', () {
|
||||||
|
final valid = <List<int>, String>{
|
||||||
|
[]: '',
|
||||||
|
[0x00]: cp([0]),
|
||||||
|
[0x7f]: cp([0x7f]),
|
||||||
|
[0xc2, 0x80]: cp([0x80]),
|
||||||
|
[0xdf, 0xbf]: cp([0x7ff]),
|
||||||
|
[0xe0, 0xa0, 0x80]: cp([0x800]),
|
||||||
|
[0xed, 0x9f, 0xbf]: cp([0xd7ff]),
|
||||||
|
[0xee, 0x80, 0x80]: cp([0xe000]),
|
||||||
|
[0xef, 0xbf, 0xbd]: cp([0xfffd]),
|
||||||
|
[0xf0, 0x90, 0x80, 0x80]: cp([0x10000]),
|
||||||
|
[0xf4, 0x8f, 0xbf, 0xbf]: cp([0x10ffff]),
|
||||||
|
};
|
||||||
|
for (final MapEntry(key: b, value: s) in valid.entries) {
|
||||||
|
expect(decodeUtf8(b), s, reason: toHex(b));
|
||||||
|
expect(isValidUtf8(b), isTrue, reason: toHex(b));
|
||||||
|
expect(utf8Bytes(s), b, reason: toHex(b));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes a well-formed string as dart:convert does', () {
|
||||||
|
final s = cp([0xfeff, 0x61, 0xe9, 0x20ac, 0x10000, 0x1f600, 0x10ffff]);
|
||||||
|
expect(utf8Bytes(s), utf8.encode(s));
|
||||||
|
expect(isWellFormedUtf16(s), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'writes a lone surrogate in generalized UTF-8, which is not UTF-8',
|
||||||
|
() {
|
||||||
|
// dart:convert writes U+FFFD in its place: the string would encode as
|
||||||
|
// another.
|
||||||
|
expect(utf8.encode(cp([0xd800])), [0xef, 0xbf, 0xbd]);
|
||||||
|
final cases = <List<int>, String>{
|
||||||
|
[0xd800]: 'eda080',
|
||||||
|
[0xdfff]: 'edbfbf',
|
||||||
|
[0x61, 0xdc00, 0xd800]: '61edb080eda080',
|
||||||
|
[0xd800, 0xd800, 0xdc00]: 'eda080f0908080',
|
||||||
|
[0xdbff, 0x61]: 'edafbf61',
|
||||||
|
};
|
||||||
|
for (final MapEntry(key: units, value: hex) in cases.entries) {
|
||||||
|
final s = cp(units);
|
||||||
|
expect(toHex(utf8Bytes(s)), hex);
|
||||||
|
expect(isWellFormedUtf16(s), isFalse);
|
||||||
|
expect(isValidUtf8(utf8Bytes(s)), isFalse);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('decodes runes as Go utf8.DecodeRune', () {
|
||||||
|
final b = [
|
||||||
|
0x41, 0xc3, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, //
|
||||||
|
0x98, 0x80, 0xc0, 0x80, 0xe0, 0x80, 0xf5,
|
||||||
|
];
|
||||||
|
expect(decodeRune(b, 0), (0x41, 1));
|
||||||
|
expect(decodeRune(b, 1), (0xe9, 2));
|
||||||
|
expect(decodeRune(b, 3), (0x20ac, 3));
|
||||||
|
expect(decodeRune(b, 6), (0x1f600, 4));
|
||||||
|
for (final i in [7, 10, 11, 12, 13, 14]) {
|
||||||
|
expect(decodeRune(b, i), (0xfffd, 1), reason: '$i');
|
||||||
|
}
|
||||||
|
expect(decodeRune(b, 15), (0xfffd, 0));
|
||||||
|
expect(decodeRune([0xf0, 0x9f, 0x98], 0), (0xfffd, 1));
|
||||||
|
expect(decodeRune([0xc3], 0), (0xfffd, 1));
|
||||||
|
expect(decodeRune([0xef, 0xbf, 0xbd], 0), (0xfffd, 3));
|
||||||
|
expect(decodeRune([], 0), (0xfffd, 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Go's %q", () {
|
||||||
|
test('quotes as strconv.Quote', () {
|
||||||
|
expect(
|
||||||
|
goQuote([0x61, 0x00, 0x62, 0xff, 0xc3, 0xa9, 0xe2, 0x80, 0xa8]),
|
||||||
|
'"a${bs}x00b${bs}xff${cp([0xe9])}${bs}u2028"',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
goQuote(utf8Bytes(cp([7, 8, 12, 10, 13, 9, 11]))),
|
||||||
|
'"${bs}a${bs}b${bs}f${bs}n${bs}r${bs}t${bs}v"',
|
||||||
|
);
|
||||||
|
expect(goQuote(utf8Bytes('"$bs')), '"$bs"$bs$bs"');
|
||||||
|
expect(
|
||||||
|
goQuote(utf8Bytes(cp([0x10000, 0x1f600, 0x301]))),
|
||||||
|
'"${cp([0x10000, 0x1f600, 0x301])}"',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
goQuote(utf8Bytes(cp([0xa0, 0xfeff, 0xe000, 0x10ffff, 0x7f]))),
|
||||||
|
'"${bs}u00a0${bs}ufeff${bs}ue000${bs}U0010ffff${bs}x7f"',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
goQuote([0xed, 0xa0, 0x80, 0xf4, 0x90, 0x80, 0x80, 0xe2, 0x82]),
|
||||||
|
'"${bs}xed${bs}xa0${bs}x80${bs}xf4${bs}x90${bs}x80${bs}x80${bs}xe2'
|
||||||
|
'${bs}x82"',
|
||||||
|
);
|
||||||
|
expect(goQuote(utf8Bytes(cp([0xfffd]))), '"${cp([0xfffd])}"');
|
||||||
|
expect(goQuote(utf8Bytes(cp([0x378]))), '"${bs}u0378"');
|
||||||
|
expect(goQuote([]), '""');
|
||||||
|
// New in Unicode 16, which the platform may know and Go 1.26 does not.
|
||||||
|
expect(
|
||||||
|
goQuote(utf8Bytes(cp([0x31e4, 0x31e3, 0x1fbcb, 0x1fbca]))),
|
||||||
|
'"${bs}u31e4${cp([0x31e3])}${bs}U0001fbcb${cp([0x1fbca])}"',
|
||||||
|
);
|
||||||
|
// A lone surrogate, as its generalized UTF-8.
|
||||||
|
expect(
|
||||||
|
goQuote(utf8Bytes(cp([0x61, 0xd800]))),
|
||||||
|
'"a${bs}xed${bs}xa0${bs}x80"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("classifies every rune as Go 1.26's strconv.IsPrint", () {
|
||||||
|
// The IsPrint bit set of all runes (bit r at byte r >> 3, mask
|
||||||
|
// 1 << (r & 7)) and its SHA-256, computed by Go 1.26.8 (Unicode
|
||||||
|
// 15.0.0), as bytes.test.ts pins it.
|
||||||
|
final bits = Uint8List(0x110000 ~/ 8);
|
||||||
|
var runes = 0;
|
||||||
|
var ranges = 0;
|
||||||
|
var previous = false;
|
||||||
|
for (var r = 0; r < 0x110000; r++) {
|
||||||
|
final p = isPrint(r);
|
||||||
|
if (p) {
|
||||||
|
bits[r >> 3] |= 1 << (r & 7);
|
||||||
|
if (r >= 0x80) {
|
||||||
|
runes++;
|
||||||
|
if (!previous) ranges++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous = p && r >= 0x80;
|
||||||
|
}
|
||||||
|
expect((runes, ranges), (148903, 710));
|
||||||
|
expect(
|
||||||
|
sha256.convert(bits).toString(),
|
||||||
|
'dbe4beecc0027b38c16c32d304958e611e27baa40412efea4cf616346eb631a2',
|
||||||
|
);
|
||||||
|
for (final r in [
|
||||||
|
0x80,
|
||||||
|
0x9f,
|
||||||
|
0xa0,
|
||||||
|
0xad,
|
||||||
|
0x31e4,
|
||||||
|
0xe000,
|
||||||
|
0xfeff,
|
||||||
|
0x10ffff,
|
||||||
|
]) {
|
||||||
|
expect(isPrint(r), isFalse, reason: r.toRadixString(16));
|
||||||
|
}
|
||||||
|
for (final r in [
|
||||||
|
0x20,
|
||||||
|
0x7e,
|
||||||
|
0xa1,
|
||||||
|
0xe9,
|
||||||
|
0x31e3,
|
||||||
|
0xfffd,
|
||||||
|
0x1f600,
|
||||||
|
0xe0100,
|
||||||
|
]) {
|
||||||
|
expect(isPrint(r), isTrue, reason: r.toRadixString(16));
|
||||||
|
}
|
||||||
|
for (final r in [0x00, 0x1f, 0x7f]) {
|
||||||
|
expect(isPrint(r), isFalse, reason: r.toRadixString(16));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
// The catalogue matches spec §69 exactly, in order, as TestCatalogueMatchesSpec
|
||||||
|
// of datekeys-go. The spec is read from the Go repository next to this one, at
|
||||||
|
// the tag of the version of the spec that this package implements, so that
|
||||||
|
// uncommitted changes there never count.
|
||||||
|
@TestOn('vm')
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:datekeys/datekeys.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final repo = Directory('../datekeys-go');
|
||||||
|
test(
|
||||||
|
'the catalogue is the list of ERR_ lines of spec §69, in order',
|
||||||
|
() {
|
||||||
|
final path =
|
||||||
|
'spec-v$specVersion:spec/DateKeys_Protocol_Specification_v$specVersion.md';
|
||||||
|
final r = Process.runSync(
|
||||||
|
'git',
|
||||||
|
['-C', repo.path, 'show', path],
|
||||||
|
stdoutEncoding: utf8,
|
||||||
|
stderrEncoding: utf8,
|
||||||
|
);
|
||||||
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||||
|
final spec = r.stdout as String;
|
||||||
|
final start = spec.indexOf('## 69. Errores normativos');
|
||||||
|
final end = spec.indexOf('## 70.');
|
||||||
|
expect(start, isNonNegative, reason: 'section 69 not found');
|
||||||
|
expect(end, greaterThan(start), reason: 'section 70 not found');
|
||||||
|
final want = [
|
||||||
|
for (final line in spec.substring(start, end).split('\n'))
|
||||||
|
if (line.trim().startsWith('ERR_')) line.trim(),
|
||||||
|
];
|
||||||
|
expect(want, isNotEmpty);
|
||||||
|
expect([for (final c in ErrorCode.values) c.code], want);
|
||||||
|
},
|
||||||
|
skip: repo.existsSync()
|
||||||
|
? false
|
||||||
|
: '../datekeys-go is missing: the spec is read from it',
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
// The normative errors of spec §69: the message convention of the Go
|
||||||
|
// reference, wrapping with a prefix and the code of an error, as
|
||||||
|
// errors_test.go of datekeys-go and the errors tests of bytes.test.ts of
|
||||||
|
// datekeys-ts. errors_spec_test.dart checks the catalogue against the spec.
|
||||||
|
|
||||||
|
import 'package:datekeys/datekeys.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('every code is written as the spec writes it, and printed so', () {
|
||||||
|
for (final c in ErrorCode.values) {
|
||||||
|
expect(c.code, matches(RegExp(r'^ERR_[A-Z_]+$')));
|
||||||
|
expect('$c', c.code);
|
||||||
|
}
|
||||||
|
expect(ErrorCode.values.map((c) => c.code).toSet(), hasLength(19));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a message is the context and the code, or the bare code', () {
|
||||||
|
final e = DateKeysException(
|
||||||
|
ErrorCode.integrity,
|
||||||
|
'capsule: truncated prelude',
|
||||||
|
);
|
||||||
|
expect(e.message, 'capsule: truncated prelude: ERR_INTEGRITY');
|
||||||
|
expect('$e', 'capsule: truncated prelude: ERR_INTEGRITY');
|
||||||
|
expect(e.code, ErrorCode.integrity);
|
||||||
|
expect(DateKeysException(ErrorCode.integrity).message, 'ERR_INTEGRITY');
|
||||||
|
expect(
|
||||||
|
DateKeysException(ErrorCode.invalidMagic, '').message,
|
||||||
|
'ERR_INVALID_MAGIC',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wrapping puts a prefix before the message and keeps the code', () {
|
||||||
|
// fmt.Errorf("capsule: step 8: %w", fmt.Errorf("agewrap: %w", ErrRoundMismatch))
|
||||||
|
final err = DateKeysException(
|
||||||
|
ErrorCode.roundMismatch,
|
||||||
|
'agewrap',
|
||||||
|
).wrap('capsule: step 8');
|
||||||
|
expect(err.message, 'capsule: step 8: agewrap: ERR_ROUND_MISMATCH');
|
||||||
|
expect(errorCode(err), 'ERR_ROUND_MISMATCH');
|
||||||
|
expect(err.code, isNot(ErrorCode.integrity));
|
||||||
|
final data = DateKeysException(ErrorCode.extensionDataInvalid)
|
||||||
|
.wrap('capsule: step 4');
|
||||||
|
expect(data.message, 'capsule: step 4: ERR_EXTENSION_DATA_INVALID');
|
||||||
|
expect(data.code, ErrorCode.extensionDataInvalid);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a failure kept as text adds no code', () {
|
||||||
|
// The failure of a relay kept as text, as provider/drand.Client keeps
|
||||||
|
// it: the error carries exactly one normative code.
|
||||||
|
final relay = DateKeysException(ErrorCode.roundMismatch, 'relay');
|
||||||
|
final err = DateKeysException(
|
||||||
|
ErrorCode.releaseUnavailable,
|
||||||
|
'drand: no relay returned a verified release for round 1000: ${relay.message}',
|
||||||
|
);
|
||||||
|
expect(errorCode(err), 'ERR_RELEASE_UNAVAILABLE');
|
||||||
|
expect(err.message, contains('ERR_ROUND_MISMATCH'));
|
||||||
|
expect(
|
||||||
|
err.message,
|
||||||
|
'drand: no relay returned a verified release for round 1000: relay: '
|
||||||
|
'ERR_ROUND_MISMATCH: ERR_RELEASE_UNAVAILABLE',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('errorCode is empty for anything but a DateKeys error', () {
|
||||||
|
expect(
|
||||||
|
errorCode(DateKeysException(ErrorCode.headInvalid)),
|
||||||
|
'ERR_HEAD_INVALID',
|
||||||
|
);
|
||||||
|
expect(errorCode(const FormatException('plain')), '');
|
||||||
|
expect(errorCode(StateError('plain')), '');
|
||||||
|
expect(errorCode(null), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withContext prefixes DateKeys errors only, and keeps the stack', () {
|
||||||
|
expect(withContext('ctx', () => 1), 1);
|
||||||
|
StackTrace? inner;
|
||||||
|
DateKeysException? caught;
|
||||||
|
StackTrace? outer;
|
||||||
|
void failing() {
|
||||||
|
try {
|
||||||
|
throw DateKeysException(ErrorCode.integrity, 'inner');
|
||||||
|
} on DateKeysException catch (_, stack) {
|
||||||
|
inner = stack;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
withContext('ctx', failing);
|
||||||
|
} on DateKeysException catch (e, stack) {
|
||||||
|
caught = e;
|
||||||
|
outer = stack;
|
||||||
|
}
|
||||||
|
expect(caught?.message, 'ctx: inner: ERR_INTEGRITY');
|
||||||
|
expect(caught?.code, ErrorCode.integrity);
|
||||||
|
expect(inner, isNotNull);
|
||||||
|
expect('$outer', '$inner');
|
||||||
|
final plain = RangeError('plain');
|
||||||
|
expect(() => withContext('ctx', () => throw plain), throwsA(same(plain)));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in new issue