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/formats_extension_test.dart

385 lines
12 KiB

// The extension mechanism (lib/src/extension.dart), as extension.test.ts of
// datekeys-ts and the tests of package extension of datekeys-go: the order
// of the UTF-8 bytes of extension_id, the copies of the data, the registries
// with their places, and the rule of an encoder of spec §72 with the
// extensions of the specification. The texts of Go are in the differential
// (formats_extension.json). It reads no file: it runs on the VM and compiled
// to JavaScript.
library;
import 'dart:typed_data';
import 'package:datekeys/src/cbor.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/extension.dart';
import 'package:test/test.dart';
String codeOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
} on ArgumentError {
return 'error';
}
}
List<Extension> decodeWhole(List<int> bytes) {
late List<Extension> exts;
unmarshalCbor(
bytes,
(d) => exts = decodeExtensionArray(d),
(e) => encodeExtensionArray(e, exts),
);
return exts;
}
Uint8List encodeWhole(List<Extension> exts) {
final e = CborEncoder();
encodeExtensionArray(e, exts);
return e.out();
}
/// A registry of org.a and org.b of version 1, with data "ok" valid.
final class Validator extends ExtensionRegistry {
const Validator();
@override
bool known(String id, int version) =>
(id == 'org.a' || id == 'org.b') && version == 1;
@override
Object? validateData(Extension e) =>
e.data != null && String.fromCharCodes(e.data!) == 'ok'
? null
: 'want "ok"';
}
/// A registry whose validator throws.
final class Throwing extends ExtensionRegistry {
const Throwing();
@override
bool known(String id, int version) => true;
@override
Object? validateData(Extension e) =>
throw const FormatException('cannot read');
}
void main() {
test('newExtension copies the data and validates the extension', () {
final data = Uint8List.fromList('public label'.codeUnits);
final e = newExtension('org.example.label', 1, data);
data[0] = 0x50;
expect(
e,
Extension('org.example.label', 1, 'public label'.codeUnits.toBytes()),
);
expect(
newExtension('a' * 256, maxExtensionVersion, [0]).id,
hasLength(256),
);
// 128 characters of two bytes are 256 bytes of UTF-8; 129 are too many.
expect(newExtension('é' * 128, 1, [0]).id, hasLength(128));
for (final (id, version, data) in [
('org.a', 1, <int>[]),
('', 1, [1]),
('org.\u{d800}', 1, [1]),
('a' * 257, 1, [1]),
('é' * 129, 1, [1]),
('org.a', maxExtensionVersion + 1, [1]),
('org.a', -1, [1]),
]) {
expect(
codeOf(() => newExtension(id, version, data)),
'ERR_NON_CANONICAL_CBOR',
);
}
});
test('sorts by the UTF-8 bytes of extension_id, never UTF-16 code units', () {
final ids = ['\u{10000}', '\u{ff61}', 'é', 'z', 'Z', 'a\u{0}', 'a'];
// Dart compares UTF-16 code units.
expect('\u{10000}'.compareTo('\u{ff61}'), lessThan(0));
// 5a, 61, 61 00, 7a, c3 a9, ef bd a1, f0 90 80 80.
expect(
canonicalExtensions([for (final id in ids) Extension(id, 1)])
.map((e) => e.id),
['Z', 'a', 'a\u{0}', 'z', 'é', '\u{ff61}', '\u{10000}'],
);
expect(canonicalExtensions(const []), isEmpty);
});
test('canonicalExtensions copies the data, and its array decodes back', () {
final input = [
Extension('org.b', 1),
Extension('org.a', 2, Uint8List.fromList([0x78])),
Extension('Z', 9),
];
final sorted = canonicalExtensions(input);
input[1].data![0] = 0;
expect(sorted[1].data, [0x78]);
expect(decodeWhole(encodeWhole(sorted)), sorted);
expect(() => sorted.add(Extension('x', 1)), throwsUnsupportedError);
});
test('never writes an array that decodeExtensionArray rejects', () {
for (final exts in [
<Extension>[],
[for (var i = 0; i < 65; i++) Extension('o.$i'.padLeft(5, '0'), 1)],
[Extension('org.a', 1, Uint8List(0))],
[Extension('org.b', 1), Extension('org.a', 1)],
[Extension('org.a', 1), Extension('org.a', 2)],
[Extension('org.a', maxExtensionVersion + 1)],
[Extension('', 1)],
[Extension('org.\u{d800}', 1)],
]) {
final e = CborEncoder()..uint(0);
encodeExtensionArray(e, exts);
expect(codeOf(e.out), 'ERR_NON_CANONICAL_CBOR');
}
});
test('keeps the bytes of data exactly, a leading BOM in its identifier', () {
// ef bb bf, the BOM, sorts after the 6f of "org.b".
final exts = [
Extension('org.b', 7),
Extension('\u{feff}org.a', 1, Uint8List.fromList([0xff, 0, 0xa2])),
];
final back = decodeWhole(encodeWhole(exts));
expect(back, exts);
expect(back.last.id.codeUnitAt(0), 0xfeff);
});
test('checkDisjoint merges in canonical order without reordering', () {
final a = [Extension('a', 1), Extension('c', 1), Extension('e', 1)];
final b = [Extension('b', 1), Extension('d', 1), Extension('f', 1)];
checkDisjoint(a, b);
final unsorted = [Extension('z', 1), Extension('e', 2)];
expect(codeOf(() => checkDisjoint(a, unsorted)), 'ERR_NON_CANONICAL_CBOR');
expect(unsorted.map((e) => e.id), ['z', 'e']);
checkDisjoint(const [], b);
checkDisjoint(a, const []);
});
group('registries', () {
final ok = Uint8List.fromList('ok'.codeUnits);
final ko = Uint8List.fromList('ko'.codeUnits);
test('a set knows extension_ids by version', () {
final s = ExtensionSet({
'org.a': [1, 2],
});
expect([s.known('org.a', 2), s.known('org.a', 3)], [true, false]);
expect(ExtensionSet().known('org.a', 1), isFalse);
expect(
knownIn(s, 'org.a', 1, ExtensionObject.head, ExtensionArray.critical),
isTrue,
);
expect(
knownIn(
null,
'org.a',
1,
ExtensionObject.head,
ExtensionArray.critical,
),
isFalse,
);
});
test('an unknown critical extension prevails over invalid data', () {
expect(
codeOf(() => checkCritical([Extension('org.a', 1)], null)),
'ERR_EXTENSION_CRITICAL_UNKNOWN',
);
checkCritical(const [], null);
checkCritical([Extension('org.a', 1, ok)], const Validator());
expect(
codeOf(
() => checkCritical([
Extension('org.a', 1, ok),
Extension('org.b', 1, ko),
], const Validator()),
),
'ERR_EXTENSION_DATA_INVALID',
);
expect(
codeOf(
() => checkCritical([
Extension('org.b', 1, ko),
Extension('org.c', 1),
], const Validator()),
),
'ERR_EXTENSION_CRITICAL_UNKNOWN',
);
});
test('a noncritical extension with invalid data is unusable, the object '
'valid', () {
final all = [
Extension('org.a', 1, ok),
Extension('org.b', 1),
Extension('org.c', 1, ko),
];
expect(checkNoncritical(all, null), isEmpty);
final unusable = checkNoncritical(all, const Validator());
expect(unusable.map((u) => [u.id, u.version]), [
['org.b', 1],
]);
expect(unusable.single.error.code, ErrorCode.extensionDataInvalid);
expect('${unusable.single}', unusable.single.error.message);
});
test('a validator that throws an Exception rejects the data', () {
expect(
codeOf(() => checkCritical([Extension('x', 1)], const Throwing())),
'ERR_EXTENSION_DATA_INVALID',
);
expect(
checkNoncritical([
Extension('x', 1),
], const Throwing()).single.error.message,
contains('cannot read'),
);
});
test('the extensions of the specification go only where spec §72 '
'registers them', () {
final note = Extension(noteExtensionId, 1, Uint8List.fromList([0x41]));
final capsule = Extension(
capsuleExtensionId,
1,
Uint8List.fromList([0xa0]),
);
// Go's extension.Standard, without ValidateCapsule.
const std = StandardExtensions(validateCapsule: null);
for (final o in ExtensionObject.values) {
for (final a in ExtensionArray.values) {
final noteHere =
o == ExtensionObject.publicHeader &&
a == ExtensionArray.noncritical;
final capsuleHere =
o == ExtensionObject.accessKey && a == ExtensionArray.noncritical;
expect(
codeOf(() => checkWrite(std, o, a, [note])),
noteHere ? 'ok' : 'error',
);
expect(
codeOf(() => checkWrite(std, o, a, [capsule])),
capsuleHere ? 'ok' : 'error',
);
// Another version, or another identifier, is the application's.
checkWrite(std, o, a, [
Extension(noteExtensionId, 2),
Extension('org.example', 1),
]);
// A reader that knows them treats them as unknown out of place.
expect(
codeOf(() => checkCritical([note], std, o)),
'ERR_EXTENSION_CRITICAL_UNKNOWN',
);
// A note without data is unusable in the noncritical array of
// PUBLIC_HEADER, and ignored in that of any other object.
expect(checkNoncritical([Extension(noteExtensionId, 1)], std, o), [
if (o == ExtensionObject.publicHeader) isA<Unusable>(),
]);
}
}
expect(
codeOf(
() => checkWrite(
std,
ExtensionObject.accessKey,
ExtensionArray.noncritical,
[Extension(capsuleExtensionId, 1)],
context: 'accesskey',
),
),
'ERR_EXTENSION_DATA_INVALID',
);
expect(
() => checkWrite(
std,
ExtensionObject.accessKey,
ExtensionArray.critical,
[capsule],
context: 'accesskey',
),
throwsA(
isA<ArgumentError>().having(
(e) => '${e.message}',
'message',
startsWith('accesskey: extension: datekeys.capsule version 1 '),
),
),
);
checkWrite(null, ExtensionObject.head, ExtensionArray.critical, [note]);
});
test('the rules of a note are those of spec §24.1, and those of a locator '
'come from its validator', () {
final seen = <String>[];
final std = StandardExtensions(
validateCapsule: (e) {
seen.add('capsule');
return DateKeysException(
ErrorCode.extensionDataInvalid,
'a bad locator',
);
},
);
final good = Extension(noteExtensionId, 1, Uint8List.fromList([0x41]));
// A tab breaks the rules of text of the declared author (spec §29.6).
final bad = Extension(noteExtensionId, 1, Uint8List.fromList([9]));
checkWrite(
std,
ExtensionObject.publicHeader,
ExtensionArray.noncritical,
[good],
);
expect(
codeOf(
() => checkWrite(
std,
ExtensionObject.publicHeader,
ExtensionArray.noncritical,
[bad],
),
),
'ERR_EXTENSION_DATA_INVALID',
);
expect(
checkNoncritical(
[bad],
std,
ExtensionObject.publicHeader,
).single.error.message,
contains('a public note that breaks the rules of text'),
);
expect(
checkNoncritical([good], std, ExtensionObject.publicHeader),
isEmpty,
);
expect(
codeOf(
() => checkWrite(
std,
ExtensionObject.accessKey,
ExtensionArray.noncritical,
[Extension(capsuleExtensionId, 1, Uint8List(1))],
),
),
'ERR_EXTENSION_DATA_INVALID',
);
expect(seen, ['capsule']);
});
});
}
extension on List<int> {
Uint8List toBytes() => Uint8List.fromList(this);
}

Powered by TurnKey Linux.