You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/test/open_vectors_support.dart

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);
}
}

Powered by TurnKey Linux.