// 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 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?; if (list != null) { return KnownExtensions([ for (final x in list.cast()) ( 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, ); } /// [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 m => { for (final e in m.entries.toList() ..sort((a, b) => '${a.key}'.compareTo('${b.key}'))) '${e.key}': sorted(e.value), }, final List 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 outcomeOf(void Function() body) { try { body(); return ['ok', '']; } on DateKeysException catch (e) { return [e.code.code, e.message]; } } List _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 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 extensionsOf(Object? v) => [ for (final x in (v! as List).cast>()) 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 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>()) 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 check, List notes, List 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); } }