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.
271 lines
8.1 KiB
271 lines
8.1 KiB
// Helpers of the tests of the head, the public note, the inspection and the
|
|
// opening against the vectors that tool/open_go_vectors.go writes: the
|
|
// extension registries of their cases, the capsules of their edits, and the
|
|
// checks of the heads and the notes. They read no file, so that the tests
|
|
// that run on Node.js can use them.
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:datekeys/datekeys.dart';
|
|
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
|
|
import 'package:test/test.dart';
|
|
|
|
import 'tlock_support.dart' show applyEdits;
|
|
|
|
typedef Json = Map<String, Object?>;
|
|
|
|
String str(Json c, String k) => c[k]! as String;
|
|
|
|
/// The extensions an application implements in a case: known at (id,
|
|
/// version), with valid data only when it equals valid_data, with the texts
|
|
/// of testkit.KnownExtensions of the reference.
|
|
final class KnownExtensions extends ExtensionRegistry {
|
|
KnownExtensions(this._known);
|
|
|
|
final List<({String id, int version, Uint8List valid})> _known;
|
|
|
|
({String id, int version, Uint8List valid})? _find(String id, int v) {
|
|
for (final k in _known) {
|
|
if (k.id == id && k.version == v) return k;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
bool known(String id, int version) => _find(id, version) != null;
|
|
|
|
@override
|
|
Object? validateData(Extension e) {
|
|
final k = _find(e.id, e.version);
|
|
if (k == null) return 'extension ${e.id} version ${e.version} is not known';
|
|
final data = e.data ?? Uint8List(0);
|
|
return equalBytes(data, k.valid)
|
|
? null
|
|
: 'data ${toHex(data)} is not ${toHex(k.valid)}';
|
|
}
|
|
}
|
|
|
|
/// A registry that knows every extension and rejects the data of all with
|
|
/// [text].
|
|
final class RejectAll extends ExtensionRegistry {
|
|
const RejectAll(this.text);
|
|
|
|
final String text;
|
|
|
|
@override
|
|
bool known(String id, int version) => true;
|
|
|
|
@override
|
|
Object? validateData(Extension e) => text;
|
|
}
|
|
|
|
/// The registry of the extensions of a case of the vectors.
|
|
ExtensionRegistry? caseExtensions(Json c) {
|
|
final list = c['extensions'] as List<Object?>?;
|
|
if (list != null) {
|
|
return KnownExtensions([
|
|
for (final x in list.cast<Json>())
|
|
(
|
|
id: str(x, 'id'),
|
|
version: x['version']! as int,
|
|
valid: fromHex(str(x, 'valid_data')),
|
|
),
|
|
]);
|
|
}
|
|
final reject = c['reject_all'] as String?;
|
|
return reject == null ? null : RejectAll(reject);
|
|
}
|
|
|
|
/// The capsule of a case: the edits of its dkc on [base], the bytes of the
|
|
/// fixture it names.
|
|
Uint8List capsuleOf(Json c, Uint8List Function(String file) fixture) {
|
|
final dkc = c['dkc']! as Json;
|
|
final base = dkc['base'] as String?;
|
|
return applyEdits(
|
|
base == null ? Uint8List(0) : fixture(base),
|
|
dkc['edits']! as List<Object?>,
|
|
);
|
|
}
|
|
|
|
/// [v] as JSON with the keys of every map sorted, to compare values decoded
|
|
/// from the vectors with values built here.
|
|
String canonical(Object? v) {
|
|
Object? sorted(Object? x) => switch (x) {
|
|
final Map<Object?, Object?> m => {
|
|
for (final e
|
|
in m.entries.toList()
|
|
..sort((a, b) => '${a.key}'.compareTo('${b.key}')))
|
|
'${e.key}': sorted(e.value),
|
|
},
|
|
final List<Object?> l => [for (final e in l) sorted(e)],
|
|
_ => x,
|
|
};
|
|
return jsonEncode(sorted(v));
|
|
}
|
|
|
|
/// The violation of an ERR_HEAD_INVALID without its prefix and its code, as
|
|
/// HeadDetail of the reference.
|
|
String headDetail(DateKeysException e) {
|
|
var s = e.message;
|
|
const prefix = 'capsule: head: ';
|
|
if (s.startsWith(prefix)) s = s.substring(prefix.length);
|
|
final suffix = ': ${e.code.code}';
|
|
return s.endsWith(suffix) ? s.substring(0, s.length - suffix.length) : s;
|
|
}
|
|
|
|
/// The registry of the heads of open_heads.json: the extension a of version
|
|
/// 1 without data, and org.example of version 2 with data 01.
|
|
final headRegistry = KnownExtensions([
|
|
(id: 'a', version: 1, valid: Uint8List(0)),
|
|
(id: 'org.example', version: 2, valid: Uint8List.fromList([1])),
|
|
]);
|
|
|
|
/// The outcome of [body] as the vectors write it: [result, text].
|
|
List<String> outcomeOf(void Function() body) {
|
|
try {
|
|
body();
|
|
return ['ok', ''];
|
|
} on DateKeysException catch (e) {
|
|
return [e.code.code, e.message];
|
|
}
|
|
}
|
|
|
|
List<String> _want(Json c) => [
|
|
c['result']! as String,
|
|
c['text'] as String? ?? '',
|
|
];
|
|
|
|
/// The decode cases of open_heads.json: the result and the text of Go with
|
|
/// no registry and with [headRegistry], and a head that decodes encodes to
|
|
/// its bytes again.
|
|
void checkDecodeCases(List<Json> cases) {
|
|
for (final c in cases) {
|
|
final b = fromHex(c['hex']! as String);
|
|
final reason = '${c['kind']}: ${c['hex']}';
|
|
expect(outcomeOf(() => decodeHead(b)), _want(c), reason: reason);
|
|
final r = (c['with_registry'] as Json?) ?? c;
|
|
expect(
|
|
outcomeOf(() => decodeHead(b, headRegistry)),
|
|
_want(r),
|
|
reason: reason,
|
|
);
|
|
if (c['result'] == 'ok') {
|
|
expect(encodeHead(decodeHead(b)), b, reason: reason);
|
|
}
|
|
}
|
|
}
|
|
|
|
List<Extension> extensionsOf(Object? v) => [
|
|
for (final x in (v! as List).cast<List<Object?>>())
|
|
Extension(
|
|
x[0]! as String,
|
|
x[1]! as int,
|
|
x[2] == null ? null : fromHex(x[2]! as String),
|
|
),
|
|
];
|
|
|
|
/// The encode cases of open_heads.json: the bytes, or the code and the text,
|
|
/// of EncodeHead of Go.
|
|
void checkEncodeCases(List<Json> cases) {
|
|
for (final c in cases) {
|
|
final h = c['head']! as Json;
|
|
final head = Head(
|
|
salt: fromHex(h['salt']! as String),
|
|
comment: h['comment']! as String,
|
|
author: h['author']! as String,
|
|
files: [
|
|
for (final f in (h['files']! as List).cast<List<Object?>>())
|
|
HeadFile(
|
|
path: f[0]! as String,
|
|
size: f[1]! as int,
|
|
start: f[2]! as int,
|
|
end: f[3]! as int,
|
|
sha256: fromHex(f[4]! as String),
|
|
mtime: f[5] as int?,
|
|
),
|
|
],
|
|
critical: extensionsOf(h['critical']),
|
|
noncritical: extensionsOf(h['noncritical']),
|
|
);
|
|
Uint8List? out;
|
|
final got = outcomeOf(() => out = encodeHead(head));
|
|
expect(got, _want(c), reason: canonical(h));
|
|
if (c['result'] == 'ok') {
|
|
expect(toHex(out!), c['hex'], reason: canonical(h));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The note cases of open_notes.json: CheckNote on the bytes, and on the
|
|
/// text when they are UTF-8; Note and UnusableNote of arrays; and
|
|
/// StandardExtensions on a note in PUBLIC_HEADER.
|
|
void checkNoteCases(List<Json> check, List<Json> notes, List<Json> standard) {
|
|
expect(standard, hasLength(check.length));
|
|
for (var i = 0; i < check.length; i++) {
|
|
final c = check[i];
|
|
final data = fromHex(c['data']! as String);
|
|
final reason = '${c['name']}: ${c['data']}';
|
|
String? text;
|
|
expect(
|
|
outcomeOf(() => text = checkNoteData(data)),
|
|
_want(c),
|
|
reason: reason,
|
|
);
|
|
if (c['result'] == 'ok') expect(utf8Bytes(text!), data, reason: reason);
|
|
final decoded = decodeUtf8(data);
|
|
if (decoded != null) {
|
|
expect(outcomeOf(() => checkNote(decoded)), _want(c), reason: reason);
|
|
if (c['result'] == 'ok') {
|
|
final e = newNote(decoded);
|
|
expect(
|
|
[e.id, e.version, toHex(e.data!)],
|
|
[noteExtensionId, 1, c['data']],
|
|
);
|
|
}
|
|
}
|
|
|
|
// StandardExtensions, as Standard of Go, on a note of version 1 with
|
|
// these bytes, absent data when empty.
|
|
final s = standard[i];
|
|
expect(s['name'], c['name']);
|
|
final e = Extension(noteExtensionId, 1, data.isEmpty ? null : data);
|
|
const std = StandardExtensions();
|
|
expect(
|
|
[
|
|
for (final u in checkNoncritical(
|
|
[e],
|
|
std,
|
|
ExtensionObject.publicHeader,
|
|
))
|
|
[u.id, u.version, u.error.message],
|
|
],
|
|
s['unusable'],
|
|
reason: reason,
|
|
);
|
|
expect(
|
|
outcomeOf(
|
|
() => checkWrite(
|
|
std,
|
|
ExtensionObject.publicHeader,
|
|
ExtensionArray.noncritical,
|
|
[e],
|
|
),
|
|
),
|
|
_want(s),
|
|
reason: reason,
|
|
);
|
|
}
|
|
for (final n in notes) {
|
|
final exts = extensionsOf(n['extensions']);
|
|
final reason = n['name'] as String?;
|
|
expect(
|
|
publicNote(exts),
|
|
n['usable'] == true ? n['text'] : null,
|
|
reason: reason,
|
|
);
|
|
expect(unusableNote(exts), n['unusable'], reason: reason);
|
|
}
|
|
}
|