blob: 45acb71d70a0ffe00cbbc0e9425aff2c78dc2643 [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 'config.dart';
import 'elf_validator.dart';
import 'mach_o_validator.dart';
import 'portable_executable_validator.dart';
/// The input supplied to a [NativeLibraryValidator].
final class NativeLibraryValidationContext {
/// The dynamic library to validate.
final Uri file;
/// The configuration of the hook invocation that produced [file].
final CodeConfig config;
/// Creates a native library validation context.
const NativeLibraryValidationContext({
required this.file,
required this.config,
});
}
/// Validates a bundled dynamic library produced by a build or link hook.
///
/// A validator must return [NativeLibraryValidation.notRecognized] for files
/// outside its domain. A validator may return [NativeLibraryValidation.matched]
/// only when it covers the current contract: the expected container family for
/// the target operating system and the target architecture family in
/// [CodeConfig].
///
/// There is one built-in validator per container format ([ElfValidator],
/// [MachOValidator], and [PortableExecutableValidator]), each in its own file.
/// A validator whose format is not the one expected for the target operating
/// system returns early without opening the file.
abstract interface class NativeLibraryValidator {
/// Validates the library described by [context].
Future<NativeLibraryValidation> validate(
NativeLibraryValidationContext context,
);
}
/// The result of running a [NativeLibraryValidator].
sealed class NativeLibraryValidation {
const NativeLibraryValidation._();
/// The validator does not recognize the file or target.
const factory NativeLibraryValidation.notRecognized() = _NotRecognized;
/// The validator recognizes the file but cannot validate the whole contract.
factory NativeLibraryValidation.inconclusive(String reason) {
if (reason.trim().isEmpty) {
throw ArgumentError.value(reason, 'reason', 'Must not be empty.');
}
return _Inconclusive(reason);
}
/// The file matches every target dimension understood by the validator.
const factory NativeLibraryValidation.matched() = _Matched;
/// The file is invalid for one or more reasons.
factory NativeLibraryValidation.rejected(Iterable<String> errors) {
final snapshot = List<String>.unmodifiable(errors);
if (snapshot.isEmpty || snapshot.any((error) => error.trim().isEmpty)) {
throw ArgumentError.value(
errors,
'errors',
'Must contain only non-empty errors.',
);
}
return _Rejected(snapshot);
}
}
final class _NotRecognized extends NativeLibraryValidation {
const _NotRecognized() : super._();
}
final class _Inconclusive extends NativeLibraryValidation {
const _Inconclusive(this.reason) : super._();
final String reason;
}
final class _Matched extends NativeLibraryValidation {
const _Matched() : super._();
}
final class _Rejected extends NativeLibraryValidation {
const _Rejected(this.errors) : super._();
final List<String> errors;
}
/// Runs the built-in validators in registration order, without allowing one
/// validator to bypass a rejection from another.
Future<({List<String> errors, List<String> warnings})> validateNativeLibrary(
NativeLibraryValidationContext context,
) async {
final errors = <String>[];
final inconclusiveReasons = <String>[];
var matched = false;
for (final validator in <NativeLibraryValidator>[
const ElfValidator(),
const MachOValidator(),
const PortableExecutableValidator(),
]) {
final NativeLibraryValidation result;
try {
result = await validator.validate(context);
} catch (error) {
errors.add('${validator.runtimeType} failed: $error');
continue;
}
switch (result) {
case _NotRecognized():
break;
case _Inconclusive(:final reason):
inconclusiveReasons.add(reason);
case _Matched():
matched = true;
case _Rejected(errors: final validatorErrors):
errors.addAll(validatorErrors);
}
}
if (errors.isNotEmpty || matched) {
return (errors: errors, warnings: const <String>[]);
}
if (inconclusiveReasons.isNotEmpty) {
return (errors: const <String>[], warnings: inconclusiveReasons);
}
return (
errors: const <String>[],
warnings: const ['No registered validator recognized the file and target.'],
);
}