lib/src/locator.dart gains the rest of package locator of datekeys-go but Seal and the data of the extension, with the same checks in the same order and the same texts: - Locator: unmarshalLocator, the map of spec 44.1 with the length that Marshal gives and nothing else, and marshal, its form, its addresses and key 6 up to the least multiple of 4096 that it fills, as padFor and PlaintextLength; usable, the addresses that a reader uses. - openLocator, Open of Go: the profile, then age with the tlock identity of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the chunks after it are neither decrypted nor checked. - The envelope: restIn, openEnvelope with the size and the SHA-256 of the rest and of the .dkc, hideRest, and splitEnvelope, the part of NewEnvelope after its age encryption, which needs the writer of age. A Locator keeps the types of Go: keys and digests of 32 bytes, and no negative size or offset. Its errors carry no code, as in Go. The tests run the cases of locator.json, the padding of every base, 482 plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the envelope, the rests and the split; a part also on Node.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
bd421e0317
commit
b2a495c0c9
@ -0,0 +1,234 @@
|
||||
// The locator and the envelope against Go (spec §44.1), from the part of
|
||||
// locator_vectors.json that locator_vectors.g.dart holds, so that it runs on
|
||||
// the VM and compiled to JavaScript: the padding of every base, plaintexts
|
||||
// valid and broken, Marshal at every limit, sealed locators opened with
|
||||
// their release and with others, and the envelope, its rest and its
|
||||
// split. locator_vm_test.dart runs every case of the files.
|
||||
library;
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:datekeys/src/bytes.dart';
|
||||
import 'package:datekeys/src/locator.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'locator_support.dart';
|
||||
import 'vectors/locator_vectors.g.dart';
|
||||
|
||||
/// The cases of the locator, shared with locator_vm_test.dart, which runs
|
||||
/// them on the whole file.
|
||||
void locatorCases(Json v) {
|
||||
final envelope = v['envelope']! as Json;
|
||||
final plainBases = [
|
||||
for (final h in v['plaintext_bases']! as List<Object?>)
|
||||
fromHex(h! as String),
|
||||
];
|
||||
final sealedBases = [
|
||||
for (final s in (v['sealed_bases']! as List<Object?>).cast<Json>())
|
||||
fromHex(s['sealed']! as String),
|
||||
];
|
||||
Locator base({
|
||||
List<LocatorAddress>? addresses,
|
||||
List<int>? key,
|
||||
List<int>? header,
|
||||
List<int>? restDigest,
|
||||
int? restSize,
|
||||
List<int>? capsuleDigest,
|
||||
}) => Locator(
|
||||
addresses:
|
||||
addresses ?? [LocatorAddress('https://ejemplo.org/foto.jpg', 3000)],
|
||||
envelopeKey: key ?? fromHex(envelope['key']! as String),
|
||||
envelopeHeader: header ?? fromHex(envelope['header']! as String),
|
||||
restDigest: restDigest ?? fromHex(envelope['rest_digest']! as String),
|
||||
restSize: restSize ?? envelope['rest_size']! as int,
|
||||
capsuleDigest:
|
||||
capsuleDigest ?? fromHex(envelope['capsule_digest']! as String),
|
||||
);
|
||||
|
||||
test('locatorPlaintextLength gives the least multiple that key 6 fills, '
|
||||
'on every base from -4100 to 16484', () {
|
||||
var n = 0;
|
||||
for (final r in rows(v, 'padding')) {
|
||||
for (var b = r[0]! as int; b <= (r[1]! as int); b++) {
|
||||
expect(locatorPlaintextLength(b), r[2], reason: '$b');
|
||||
n++;
|
||||
}
|
||||
}
|
||||
expect(n, 4100 + 1 + 16484);
|
||||
});
|
||||
|
||||
test('unmarshalLocator reads plaintexts as Unmarshal', () {
|
||||
for (final c in rows(v, 'plaintexts')) {
|
||||
final b = applyLocatorEdits(plainBases[c[0]! as int], c[1]);
|
||||
Locator? l;
|
||||
expect(
|
||||
errorText(() => l = unmarshalLocator(b)),
|
||||
textOf(v, c[2]),
|
||||
reason: '${c[1]}',
|
||||
);
|
||||
if (c[3] != null) {
|
||||
expect(summaryOf(l!), expandSummary(c[3]), reason: '${c[1]}');
|
||||
// As FuzzUnmarshal of Go: an address that a reader uses passes the
|
||||
// rules, and has a host to show.
|
||||
for (final a in l!.usable) {
|
||||
checkAddressUri(a.uri);
|
||||
expect(a.host, isNotEmpty);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('marshal writes the plaintext of Marshal, or its error', () {
|
||||
for (final c in rows(v, 'marshal')) {
|
||||
final l = base(
|
||||
addresses: [
|
||||
for (final a in (c[0]! as List<Object?>).cast<List<Object?>>())
|
||||
LocatorAddress(uriOf(a[0]), a[1]! as int),
|
||||
],
|
||||
header: c[1] == null ? null : fromHex(c[1]! as String),
|
||||
restSize: c[2]! as int,
|
||||
);
|
||||
Uint8List? b;
|
||||
expect(errorText(() => b = l.marshal()), textOf(v, c[3]), reason: '$c');
|
||||
if (b != null) {
|
||||
expect([b!.length, sha256Hex(b!)], [c[4], c[5]], reason: '$c');
|
||||
// It reads back as it was written.
|
||||
expect(summaryOf(unmarshalLocator(b!)), summaryOf(l));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('openLocator opens a sealed locator with the release of its round, '
|
||||
'as Open', () {
|
||||
for (final c in rows(v, 'open')) {
|
||||
final round = c[1]! as int;
|
||||
final b = applyLocatorEdits(sealedBases[c[0]! as int], c[4]);
|
||||
Locator? l;
|
||||
expect(
|
||||
errorText(
|
||||
() => l = openLocator(
|
||||
profileOf(c[3]! as String),
|
||||
round,
|
||||
openRelease(v, round, c[2]),
|
||||
b,
|
||||
),
|
||||
),
|
||||
textOf(v, c[5]),
|
||||
reason: '$c',
|
||||
);
|
||||
if (c[6] != null) {
|
||||
expect(summaryOf(l!), expandSummary(c[6]), reason: '$c');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('openEnvelope checks the rest and the .dkc as OpenEnvelope', () {
|
||||
final rest = fromHex(envelope['rest']! as String);
|
||||
final header = fromHex(envelope['header']! as String);
|
||||
for (final c in rows(v, 'envelopes')) {
|
||||
final l = base(
|
||||
key: c[1] == '' ? null : fromHex(c[1]! as String),
|
||||
header: applyLocatorEdits(header, c[2]),
|
||||
restDigest: c[3] == '' ? null : fromHex(c[3]! as String),
|
||||
restSize: c[4]! as int,
|
||||
capsuleDigest: c[5] == '' ? null : fromHex(c[5]! as String),
|
||||
);
|
||||
Uint8List? dkc;
|
||||
final r = applyLocatorEdits(rest, c[6]);
|
||||
expect(
|
||||
errorText(() => dkc = l.openEnvelope(r)),
|
||||
textOf(v, c[7]),
|
||||
reason: '${c[0]}',
|
||||
);
|
||||
if (dkc != null) {
|
||||
expect(sha256Hex(dkc!), c[8]);
|
||||
expect(toHex(dkc!), envelope['dkc']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('restIn reads rest_size bytes from the offset, as RestIn', () {
|
||||
final resources = [fromHex(v['rest_in_resource']! as String), Uint8List(0)];
|
||||
for (final c in rows(v, 'rest_in')) {
|
||||
final l = base(restSize: c[2]! as int);
|
||||
Uint8List? r;
|
||||
expect(
|
||||
errorText(() => r = l.restIn(resources[c[0]! as int], c[1]! as int)),
|
||||
textOf(v, c[3]),
|
||||
reason: '$c',
|
||||
);
|
||||
if (r != null) expect(sha256Hex(r!), c[4]);
|
||||
}
|
||||
});
|
||||
|
||||
test('hideRest appends the rest, as Hide', () {
|
||||
for (final c in rows(v, 'hide')) {
|
||||
final (:file, :offset) = hideRest(
|
||||
fromHex(c[0]! as String),
|
||||
fromHex(c[1]! as String),
|
||||
);
|
||||
expect([toHex(file), offset], [c[2], c[3]]);
|
||||
}
|
||||
});
|
||||
|
||||
test('splitEnvelope splits an age file where NewEnvelope does', () {
|
||||
final key = fromHex(envelope['key']! as String);
|
||||
final dkc = fromHex(envelope['dkc']! as String);
|
||||
for (final c in rows(v, 'split')) {
|
||||
final file = fromHex(c[0]! as String);
|
||||
({Locator locator, Uint8List rest})? s;
|
||||
expect(
|
||||
errorText(() => s = splitEnvelope(file, key, dkc)),
|
||||
textOf(v, c[1]),
|
||||
reason: '$c',
|
||||
);
|
||||
if (s != null) {
|
||||
final end = c[2]! as int;
|
||||
expect(toHex(s!.locator.envelopeHeader), toHex(file.sublist(0, end)));
|
||||
expect(toHex(s!.rest), toHex(file.sublist(end)));
|
||||
expect(s!.locator.addresses, isEmpty);
|
||||
}
|
||||
}
|
||||
// The envelope of NewEnvelope: its locator is the one of the vectors.
|
||||
final rows0 = rows(v, 'split').first;
|
||||
final s = splitEnvelope(fromHex(rows0[0]! as String), key, dkc);
|
||||
expect(toHex(s.locator.envelopeHeader), envelope['header']);
|
||||
expect(toHex(s.rest), envelope['rest']);
|
||||
expect(toHex(s.locator.restDigest), envelope['rest_digest']);
|
||||
expect(s.locator.restSize, envelope['rest_size']);
|
||||
expect(toHex(s.locator.capsuleDigest), envelope['capsule_digest']);
|
||||
expect(s.locator.openEnvelope(s.rest), dkc);
|
||||
});
|
||||
}
|
||||
|
||||
void main() {
|
||||
final v = decodeJson(locatorVectorsJson);
|
||||
group('the part of locator_vectors.json', () => locatorCases(v));
|
||||
|
||||
test('a Locator has the types of Go: keys and digests of 32 bytes and no '
|
||||
'negative size or offset', () {
|
||||
final k = Uint8List(32);
|
||||
Locator make({int key = 32, int size = 0}) => Locator(
|
||||
addresses: const [],
|
||||
envelopeKey: Uint8List(key),
|
||||
envelopeHeader: [1],
|
||||
restDigest: k,
|
||||
restSize: size,
|
||||
capsuleDigest: k,
|
||||
);
|
||||
make();
|
||||
expect(() => make(key: 31), throwsArgumentError);
|
||||
expect(() => make(size: -1), throwsArgumentError);
|
||||
expect(() => LocatorAddress('https://a.org/', -1), throwsArgumentError);
|
||||
expect(() => make().restIn([1, 2], -1), throwsArgumentError);
|
||||
// An envelope without addresses yet does not marshal.
|
||||
expect(
|
||||
errorText(() => make().marshal()),
|
||||
'locator: 0 addresses, not 1 to 8',
|
||||
);
|
||||
final l = make().withAddresses([LocatorAddress('https://a.org/', 5)]);
|
||||
expect(l.addresses.single.offset, 5);
|
||||
l.wipe();
|
||||
expect(l.envelopeKey, Uint8List(32));
|
||||
});
|
||||
}
|
||||
@ -1,42 +1,272 @@
|
||||
// The addresses of a locator against files, on the VM: every address of
|
||||
// testdata/vectors/locator.json with the result and the text of Go, and the
|
||||
// constant of locator_uris.g.dart checked against its file.
|
||||
// The locator against files, on the VM: every case of
|
||||
// testdata/vectors/locator.json but the data of the extension, with the
|
||||
// result and the text of Go; every case of test/vectors/locator_vectors.json, of which
|
||||
// locator_test.dart runs a part also compiled to JavaScript; the sealed
|
||||
// locators whose plaintext passes 1 MiB, which Go reads through
|
||||
// io.LimitReader. The constants of locator_uris.g.dart and
|
||||
// locator_vectors.g.dart are checked against their files.
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:datekeys/src/bytes.dart';
|
||||
import 'package:datekeys/src/chacha20poly1305.dart';
|
||||
import 'package:datekeys/src/locator.dart';
|
||||
import 'package:datekeys/src/profile.dart';
|
||||
import 'package:datekeys/src/sha256.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'locator_support.dart';
|
||||
import 'locator_test.dart' show locatorCases;
|
||||
import 'vectors/locator_uris.g.dart';
|
||||
import 'vectors/locator_vectors.g.dart';
|
||||
|
||||
Json readJson(String path) => decodeJson(File(path).readAsStringSync());
|
||||
|
||||
void main() {
|
||||
final v = readJson('test/vectors/locator_vectors.json');
|
||||
final uris = readJson('test/vectors/locator_uris.json');
|
||||
final td = readJson('testdata/vectors/locator.json');
|
||||
|
||||
test('locator_uris.g.dart holds locator_uris.json', () {
|
||||
expect(
|
||||
locatorUrisJson,
|
||||
File('test/vectors/locator_uris.json').readAsStringSync(),
|
||||
group('the constants', () {
|
||||
test('locator_uris.g.dart holds locator_uris.json', () {
|
||||
expect(
|
||||
locatorUrisJson,
|
||||
File('test/vectors/locator_uris.json').readAsStringSync(),
|
||||
);
|
||||
});
|
||||
|
||||
test('locator_vectors.g.dart holds a part of locator_vectors.json', () {
|
||||
final part = decodeJson(locatorVectorsJson);
|
||||
List<Object?> every(Object? xs, int n) => [
|
||||
for (final (i, x) in (xs! as List<Object?>).indexed)
|
||||
if (i % n == 0) x,
|
||||
];
|
||||
for (final MapEntry(:key, :value) in part.entries) {
|
||||
if (key == 'description') continue;
|
||||
final want = switch (key) {
|
||||
'plaintexts' => every(v[key], 3),
|
||||
'open' => (v[key]! as List<Object?>).sublist(0, 24),
|
||||
'parse' => every(v[key], 2),
|
||||
_ => v[key],
|
||||
};
|
||||
expect(value, want, reason: key);
|
||||
}
|
||||
expect(
|
||||
part.keys.toSet(),
|
||||
v.keys.toSet().difference({'limit', 'capsule'}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('locator_vectors.json', () => locatorCases(v));
|
||||
|
||||
group('testdata/vectors/locator.json', () {
|
||||
final p = quicknet();
|
||||
final round = td['round']! as int;
|
||||
final release = releaseOf(v, round);
|
||||
final tdTexts = v['testdata']! as Json;
|
||||
|
||||
test('its locator opens with the release of its round, and the rest '
|
||||
'in the host opens the envelope', () {
|
||||
final loc = openLocator(
|
||||
p,
|
||||
round,
|
||||
release,
|
||||
fromHex(td['locator_sealed']! as String),
|
||||
);
|
||||
expect(summaryOf(loc), expandSummary(tdTexts['main']));
|
||||
expect(toHex(loc.marshal()), td['locator_plaintext']);
|
||||
expect(loc.marshal(), hasLength(locatorBlock));
|
||||
expect(toHex(loc.envelopeKey), td['envelope_key']);
|
||||
expect(toHex(loc.restDigest), td['rest_digest']);
|
||||
expect(loc.restSize, td['rest_size']);
|
||||
expect(toHex(loc.capsuleDigest), td['capsule_digest']);
|
||||
expect(toHex(loc.envelopeHeader), td['envelope_header']);
|
||||
final addresses = (td['addresses']! as List<Object?>).cast<Json>();
|
||||
expect(loc.addresses, hasLength(addresses.length));
|
||||
for (final (i, a) in addresses.indexed) {
|
||||
expect(
|
||||
[
|
||||
loc.addresses[i].uri,
|
||||
loc.addresses[i].offset,
|
||||
loc.addresses[i].host,
|
||||
],
|
||||
[a['uri'], a['offset'], a['host']],
|
||||
);
|
||||
}
|
||||
final rest = loc.restIn(
|
||||
fromHex(td['host']! as String),
|
||||
td['host_offset']! as int,
|
||||
);
|
||||
expect(toHex(rest), td['rest']);
|
||||
expect(toHex(loc.openEnvelope(rest)), td['dkc']);
|
||||
});
|
||||
|
||||
test('the padding of each base', () {
|
||||
for (final c in (td['padding_cases']! as List<Object?>).cast<Json>()) {
|
||||
expect(locatorPlaintextLength(c['base']! as int), c['total']);
|
||||
expect((c['total']! as int) % locatorBlock, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('every address, with the text of Go', () {
|
||||
final cases = (td['uri_cases']! as List<Object?>).cast<Json>();
|
||||
final texts = rows(uris, 'testdata');
|
||||
expect(texts, hasLength(cases.length));
|
||||
for (final (i, c) in cases.indexed) {
|
||||
final uri = c['uri']! as String;
|
||||
expect(texts[i][0], uri);
|
||||
final got = errorText(() => checkAddressUri(uri));
|
||||
expect(got.isEmpty, c['ok'], reason: uri);
|
||||
expect(got, textOf(uris, texts[i][1]), reason: uri);
|
||||
expect(LocatorAddress(uri).host, texts[i][2], reason: uri);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'the mixed locator reads, and a reader uses the address it accepts',
|
||||
() {
|
||||
final m = td['mixed']! as Json;
|
||||
final mixed = openLocator(
|
||||
p,
|
||||
round,
|
||||
release,
|
||||
fromHex(m['locator_sealed']! as String),
|
||||
);
|
||||
final back = unmarshalLocator(
|
||||
fromHex(m['locator_plaintext']! as String),
|
||||
);
|
||||
expect(summaryOf(mixed), expandSummary(tdTexts['mixed']));
|
||||
expect(summaryOf(back), summaryOf(mixed));
|
||||
final usable = <LocatorAddress>[];
|
||||
for (final (i, a)
|
||||
in (m['addresses']! as List<Object?>).cast<Json>().indexed) {
|
||||
expect(
|
||||
mixed.addresses[i],
|
||||
LocatorAddress(a['uri']! as String, a['offset']! as int),
|
||||
);
|
||||
if (a['usable'] == true) usable.add(mixed.addresses[i]);
|
||||
}
|
||||
expect(usable, isNotEmpty);
|
||||
expect(mixed.usable, usable);
|
||||
// It cannot be written: a writer never writes an address that a reader
|
||||
// rejects.
|
||||
expect(() => mixed.marshal(), throwsA(isA<LocatorException>()));
|
||||
final rest = mixed.restIn(
|
||||
fromHex(td['host']! as String),
|
||||
usable.first.offset,
|
||||
);
|
||||
expect(toHex(mixed.openEnvelope(rest)), td['dkc']);
|
||||
},
|
||||
);
|
||||
|
||||
test('the rests: rest_size bytes from the offset, used only when their '
|
||||
'SHA-256 is resto_digest, with the texts of Go', () {
|
||||
final loc = openLocator(
|
||||
p,
|
||||
round,
|
||||
release,
|
||||
fromHex(td['locator_sealed']! as String),
|
||||
);
|
||||
final texts = rows(tdTexts, 'rest_cases');
|
||||
for (final (i, c)
|
||||
in (td['rest_cases']! as List<Object?>).cast<Json>().indexed) {
|
||||
Uint8List? r;
|
||||
expect(
|
||||
errorText(
|
||||
() => r = loc.restIn(
|
||||
fromHex(c['resource']! as String),
|
||||
c['offset']! as int,
|
||||
),
|
||||
),
|
||||
textOf(v, texts[i][0]),
|
||||
reason: '${c['name']}',
|
||||
);
|
||||
var opens = false;
|
||||
if (r != null) {
|
||||
Uint8List? dkc;
|
||||
expect(
|
||||
errorText(() => dkc = loc.openEnvelope(r!)),
|
||||
textOf(v, texts[i][1]),
|
||||
reason: '${c['name']}',
|
||||
);
|
||||
opens = dkc != null && toHex(dkc!) == td['dkc'];
|
||||
}
|
||||
expect(opens, c['opens'], reason: '${c['name']}');
|
||||
}
|
||||
});
|
||||
|
||||
test('the plaintexts of the locator, with the texts of Go', () {
|
||||
final texts = tdTexts['plaintext_cases']! as List<Object?>;
|
||||
for (final (i, c)
|
||||
in (td['plaintext_cases']! as List<Object?>).cast<Json>().indexed) {
|
||||
final want = textOf(v, texts[i]);
|
||||
expect(
|
||||
errorText(
|
||||
() => unmarshalLocator(fromHex(c['locator_plaintext']! as String)),
|
||||
),
|
||||
want,
|
||||
reason: '${c['name']}',
|
||||
);
|
||||
expect(want.isEmpty, c['ok'], reason: '${c['name']}');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('every address of testdata/vectors/locator.json, with the text of '
|
||||
'Go', () {
|
||||
final cases = (td['uri_cases']! as List<Object?>).cast<Json>();
|
||||
final texts = rows(uris, 'testdata');
|
||||
expect(texts, hasLength(cases.length));
|
||||
for (final (i, c) in cases.indexed) {
|
||||
final uri = c['uri']! as String;
|
||||
expect(texts[i][0], uri);
|
||||
final got = errorText(() => checkAddressUri(uri));
|
||||
expect(got.isEmpty, c['ok'], reason: uri);
|
||||
expect(got, textOf(uris, texts[i][1]), reason: uri);
|
||||
expect(LocatorAddress(uri).host, texts[i][2], reason: uri);
|
||||
test('openLocator reads at most 1 MiB of plaintext, as Go through '
|
||||
'io.LimitReader: what follows is neither decrypted nor checked', () {
|
||||
final lim = v['limit']! as Json;
|
||||
final header = fromHex(lim['header']! as String);
|
||||
final nonce = fromHex(lim['nonce']! as String);
|
||||
final streamKey = hkdfSha256(
|
||||
fromHex(lim['file_key']! as String),
|
||||
nonce,
|
||||
'payload'.codeUnits,
|
||||
32,
|
||||
);
|
||||
expect(toHex(streamKey), lim['stream_key']);
|
||||
final plain = fromHex(
|
||||
(v['plaintext_bases']! as List<Object?>)[0]! as String,
|
||||
);
|
||||
final release = releaseOf(v, lim['round']! as int);
|
||||
for (final c in (lim['cases']! as List<Object?>).cast<Json>()) {
|
||||
final chunks = rows(c, 'chunks');
|
||||
final total = chunks.fold(0, (n, ch) => n + (ch[0]! as int));
|
||||
final content = Uint8List(total)
|
||||
..setRange(0, plain.length < total ? plain.length : total, plain);
|
||||
final out = BytesBuilder(copy: false)
|
||||
..add(header)
|
||||
..add(nonce);
|
||||
var at = 0;
|
||||
for (final (i, ch) in chunks.indexed) {
|
||||
final n = ch[0]! as int;
|
||||
// An 11-byte big-endian counter and the flag of the last chunk.
|
||||
final chunkNonce = Uint8List(12)
|
||||
..[7] = i >> 24
|
||||
..[8] = (i >> 16) & 0xff
|
||||
..[9] = (i >> 8) & 0xff
|
||||
..[10] = i & 0xff
|
||||
..[11] = ch[1] == true ? 1 : 0;
|
||||
final ct = chacha20Poly1305Seal(
|
||||
streamKey,
|
||||
chunkNonce,
|
||||
Uint8List.sublistView(content, at, at + n),
|
||||
);
|
||||
if (ch[2] == true) ct[0] ^= 1;
|
||||
out.add(ct);
|
||||
at += n;
|
||||
}
|
||||
out.add(Uint8List(c['trailing']! as int));
|
||||
final file = out.takeBytes();
|
||||
expect([file.length, sha256Hex(file)], [c['length'], c['sha256']]);
|
||||
expect(
|
||||
errorText(() => openLocator(quicknet(), 1000, release, file)),
|
||||
textOf(v, c['text']),
|
||||
reason: '${c['name']}',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in new issue