blob: 524fe840ca2aa8f69ed4b8ba2a058b24932e17eb [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'dart:typed_data';
import 'package:boring/bindings.dart';
import 'package:boring/bindings.dart' as ssl;
import 'package:ffi/ffi.dart' show using;
import 'package:test/test.dart';
void main() {
group('package:boring/bindings.dart', () {
test('BORINGSSL_self_test passes', () {
expect(ssl.BORINGSSL_self_test(), equals(1));
});
test('opensslAllocator allocates, frees, and exposes nativeFree', () {
using((arena) {
final buf = arena<ffi.Uint8>(32);
expect(ssl.RAND_bytes(buf, 32), equals(1));
final bytes = Uint8List.fromList(buf.asTypedList(32));
expect(bytes.any((b) => b != 0), isTrue);
}, opensslAllocator);
final raw = opensslAllocator<ffi.Uint8>(16);
expect(ssl.RAND_bytes(raw, 16), equals(1));
final view = raw.asTypedList(16, finalizer: opensslAllocator.nativeFree);
expect(view.length, equals(16));
});
test(
'NativeHandle wraps BoringSSL pointers with finalizer and dispose',
() {
final pkey = NativeHandle(
ssl.EVP_PKEY_new(),
ssl.addresses.EVP_PKEY_free,
);
expect(pkey.isDisposed, isFalse);
expect(ssl.EVP_PKEY_id.invoke(pkey), equals(ssl.EVP_PKEY_NONE));
pkey.dispose();
expect(pkey.isDisposed, isTrue);
// Idempotent second dispose does not double-free:
pkey.dispose();
expect(() => ssl.EVP_PKEY_id.invoke(pkey), throwsStateError);
},
);
test('EVP_sha256 computes expected digest', () {
final digest = using((arena) {
final input = utf8.encode('abc');
final inPtr = arena<ffi.Uint8>(input.length);
inPtr.asTypedList(input.length).setAll(0, input);
final md = ssl.EVP_sha256();
final outPtr = arena<ffi.Uint8>(ssl.EVP_MD_size(md));
final outLen = arena<ffi.UnsignedInt>();
final ctx = ssl.EVP_MD_CTX_new();
try {
expect(ssl.EVP_DigestInit(ctx, md), equals(1));
expect(
ssl.EVP_DigestUpdate(ctx, inPtr.cast(), input.length),
equals(1),
);
expect(ssl.EVP_DigestFinal(ctx, outPtr, outLen), equals(1));
} finally {
ssl.EVP_MD_CTX_free(ctx);
}
return Uint8List.fromList(outPtr.asTypedList(outLen.value));
}, opensslAllocator);
final hex = digest.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
expect(
hex,
equals(
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
),
);
});
test('CBB and CBS are concrete ffi.Structs usable with Allocator', () {
expect(ffi.sizeOf<CBB>(), greaterThan(0));
expect(ffi.sizeOf<CBS>(), greaterThan(0));
using((arena) {
final cbb = arena<CBB>();
ssl.CBB_zero(cbb);
expect(ssl.CBB_init(cbb, 64), equals(1));
try {
expect(ssl.CBB_add_u8(cbb, 0x2a), equals(1));
expect(ssl.CBB_flush(cbb), equals(1));
expect(ssl.CBB_len(cbb), equals(1));
final cbs = arena<CBS>()
..ref.data = ssl.CBB_data(cbb)
..ref.len = ssl.CBB_len(cbb);
final out = arena<ffi.Uint8>();
expect(ssl.CBS_get_u8(cbs, out), equals(1));
expect(out.value, equals(0x2a));
} finally {
ssl.CBB_cleanup(cbb);
}
}, opensslAllocator);
});
test('extractBoringSslError and error macro helpers work', () {
ssl.ERR_clear_error();
expect(extractBoringSslError(), isNull);
expect(EC_PKEY_NO_PUBKEY, isNonZero);
expect(HKDF_R_OUTPUT_TOO_LARGE, isNonZero);
expect(ERR_LIB_HKDF, isNonZero);
expect(
point_conversion_form_t.POINT_CONVERSION_UNCOMPRESSED,
equals(4),
);
final packed = (ERR_LIB_HKDF << 24) | HKDF_R_OUTPUT_TOO_LARGE;
expect(ERR_GET_LIB(packed), equals(ERR_LIB_HKDF));
expect(ERR_GET_REASON(packed), equals(HKDF_R_OUTPUT_TOO_LARGE));
});
});
group('BoringArena', () {
test('using releases registrations in reverse order', () {
final log = <String>[];
final result = BoringArena.using((arena) {
arena.register('a', log.add);
arena.onReleaseAll(() => log.add('b'));
arena.register('c', log.add);
expect(log, isEmpty);
return 42;
});
expect(result, equals(42));
expect(log, equals(['c', 'b', 'a']));
});
test('using releases after the returned Future completes', () async {
var released = false;
final future = BoringArena.using((arena) async {
arena.onReleaseAll(() => released = true);
await Future<void>.delayed(Duration.zero);
expect(released, isFalse);
return 'done';
});
expect(released, isFalse);
expect(await future, equals('done'));
expect(released, isTrue);
});
test('using releases when the computation throws', () async {
var released = 0;
expect(
() => BoringArena.using<void>((arena) {
arena.onReleaseAll(() => released++);
throw StateError('sync failure');
}),
throwsStateError,
);
await expectLater(
BoringArena.using((arena) async {
arena.onReleaseAll(() => released++);
throw StateError('async failure');
}),
throwsStateError,
);
expect(released, equals(2));
});
test('using rejects Stream results', () {
var released = false;
expect(
() => BoringArena.using((arena) {
arena.onReleaseAll(() => released = true);
return Stream.value(1);
}),
throwsArgumentError,
);
expect(released, isTrue);
});
test('stream releases when done and when cancelled', () async {
var released = 0;
Stream<int> numbers() => BoringArena.stream((arena) async* {
arena.onReleaseAll(() => released++);
yield 1;
yield 2;
yield 3;
});
expect(await numbers().toList(), equals([1, 2, 3]));
expect(released, equals(1));
expect(await numbers().first, equals(1));
expect(released, equals(2));
});
test('move transfers ownership out of the arena', () {
final log = <String>[];
BoringArena.using((arena) {
arena.register('kept', log.add);
final moved = arena.register('moved', log.add);
expect(arena.move(moved), equals('moved'));
expect(() => arena.move('unknown'), throwsArgumentError);
});
expect(log, equals(['kept']));
// Memory moved out of the arena is owned (and freed) by the caller.
final pointer = BoringArena.using(
(arena) => arena.move(arena.copyBytes<ffi.Uint8>([1, 2, 3])),
);
expect(pointer.asTypedList(3), equals([1, 2, 3]));
opensslAllocator.free(pointer);
});
test('cannot be used after release', () {
final arena = BoringArena()..releaseAll();
expect(() => arena.allocate<ffi.Uint8>(1), throwsStateError);
expect(() => arena.register(1, (_) {}), throwsStateError);
expect(() => arena.onReleaseAll(() {}), throwsStateError);
arena.releaseAll(); // Releasing again is a no-op.
});
test('releaseAll releases everything even if a callback throws', () {
final log = <String>[];
final arena = BoringArena()
..register('a', log.add)
..onReleaseAll(() => throw StateError('released second'))
..onReleaseAll(() => throw ArgumentError('released first'))
..register('d', log.add);
expect(arena.releaseAll, throwsArgumentError);
expect(log, equals(['d', 'a']));
});
test('copyBytes, cbs, cbb, and toBytes round-trip bytes', () {
BoringArena.using((arena) {
final cbs = arena.cbs([0x2a, 0x01, 0x02]);
final u8 = arena<ffi.Uint8>();
expect(ssl.CBS_get_u8(cbs, u8), equals(1));
expect(u8.value, equals(0x2a));
expect(cbs.ref.len, equals(2));
final cbb = arena.cbb();
expect(
ssl.CBB_add_bytes(cbb, arena.copyBytes([1, 2, 3]), 3),
equals(1),
);
expect(ssl.CBB_add_u8(cbb, 4), equals(1));
expect(cbb.toBytes(), equals([1, 2, 3, 4]));
expect(arena.cbb().toBytes(), isEmpty);
});
});
test('cbb marshals and cbs parses an SPKI public key', () {
final spki = BoringArena.using((arena) {
final ec = arena.register(
ssl.EC_KEY_new_by_curve_name(ssl.NID_X9_62_prime256v1),
ssl.EC_KEY_free,
);
expect(ssl.EC_KEY_generate_key(ec), equals(1));
final pkey = arena.register(ssl.EVP_PKEY_new(), ssl.EVP_PKEY_free);
expect(ssl.EVP_PKEY_set1_EC_KEY(pkey, ec), equals(1));
final cbb = arena.cbb();
expect(ssl.EVP_marshal_public_key(cbb, pkey), equals(1));
return cbb.toBytes();
});
expect(spki, isNotEmpty);
BoringArena.using((arena) {
final cbs = arena.cbs(spki);
final pkey = arena.register(
ssl.EVP_parse_public_key(cbs),
ssl.EVP_PKEY_free,
);
expect(pkey, isNot(equals(ffi.nullptr)));
expect(cbs.ref.len, equals(0), reason: 'no trailing bytes');
expect(ssl.EVP_PKEY_id(pkey), equals(ssl.EVP_PKEY_EC));
});
});
});
}