blob: 47e2a780e89d370948dc0cd100d3650b4a4ef8c3 [file] [log] [blame]
// Copyright (c) 2022, 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 'package:front_end/src/api_unstable/vm.dart';
import 'package:vm/kernel_front_end.dart';
import 'package:yaml/yaml.dart';
import 'diagnostic_message.dart';
class NativeAssetsValidator {
final ErrorDetector errorDetector;
List<Uri> involvedFiles = [];
NativeAssetsValidator(this.errorDetector);
void _reportError(
String message, {
CfeSeverity severity = CfeSeverity.error,
}) {
errorDetector(
NativeAssetsDiagnosticMessage(
message: message,
involvedFiles: involvedFiles,
severity: severity,
),
);
}
/// Parses and validates [nativeAssetsYamlString].
///
/// Returns the parsed result if valid, otherwise `null`.
///
/// Reports errors to [errorDetector].
Map? parseAndValidate(String nativeAssetsYamlString) {
late final Object? nativeAssetsYaml;
try {
nativeAssetsYaml = loadYaml(nativeAssetsYamlString) as Object?;
} on YamlException catch (e) {
_reportError("File not formatted as yaml: $e $nativeAssetsYamlString.");
return null;
}
if (nativeAssetsYaml == null) {
_reportError("File not formatted as yaml: $nativeAssetsYamlString.");
return null;
}
final nativeAssetsYamlPruned = _validateAndPrune(nativeAssetsYaml);
return nativeAssetsYamlPruned;
}
static const _formatVersionKey = 'format-version';
static const _nativeAssetsKey = 'native-assets';
/// Modifies [object] to prune invalid data that only causes warnings.
///
/// Returns `null` if invalid.
Map? _validateAndPrune(Object object) {
if (object is! Map) {
_reportError('Unexpected root object: $object. Expected a Map.');
return null;
}
var isValid = true;
if (!object.containsKey(_formatVersionKey)) {
_reportError('Expected $_formatVersionKey in $object.');
isValid = false;
} else {
isValid &= _validateFormatVersion(object[_formatVersionKey]);
}
if (!object.containsKey(_nativeAssetsKey)) {
_reportError('Expected $_nativeAssetsKey in $object.');
isValid = false;
} else {
var nativeAssets = object[_nativeAssetsKey];
if (nativeAssets is Map) {
// Make native-assets modifiable.
nativeAssets = Map.from(nativeAssets);
object = Map.from(object);
object[_nativeAssetsKey] = nativeAssets;
}
isValid &= _validateNativeAssets(nativeAssets);
}
if (!isValid) return null;
return object;
}
/// The VM cannot consume newer formats with breaking changes.
static const _maximumMajor = 1;
/// This VM cannot consume too old versions.
static const _minimumMajor = 1;
static const _minimumMinor = 0;
static const _minimumPatch = 0;
static const _minimumVersion = [_minimumMajor, _minimumMinor, _minimumPatch];
/// Validate [_formatVersionKey] contents.
///
/// Checks the major version upper bound and all version lower bounds.
bool _validateFormatVersion(Object object) {
if (object is! List || object.length != 3) {
_reportError(
'Unexpected format version: $object. Expected a List with length 3.',
);
return false;
}
final major = object[0];
final minor = object[1];
final patch = object[2];
if (major > _maximumMajor) {
_reportError(
'Unexpected format version: $object. Major version above $_maximumMajor not supported.',
);
return false;
}
if (major > _minimumMajor) return true;
if (major < _minimumMajor) {
_reportError(
'Unexpected format version: $object. Versions below $_minimumVersion not supported.',
);
return false;
}
if (minor > _minimumMinor) return true;
if (minor < _minimumMinor) {
_reportError(
'Unexpected format version: $object. Versions below $_minimumVersion not supported.',
);
return false;
}
if (patch < _minimumPatch) {
_reportError(
'Unexpected format version: $object. Versions below $_minimumVersion not supported.',
);
return false;
}
return true;
}
/// Validate [_nativeAssetsKey] contents.
bool _validateNativeAssets(Object object) {
if (object is! Map) {
_reportError('Unexpected native-assets: $object. Expected a Map.');
return false;
}
var isValid = true;
final invalidTargets = [];
for (final entry in object.entries) {
final target = entry.key;
final validTarget = _validateTarget(target);
if (!validTarget) {
invalidTargets.add(target);
} else {
isValid &= _validateAssets(entry.value);
}
}
for (final target in invalidTargets) {
object.remove(target);
}
return isValid;
}
// TODO(http://dartbug.com/49803): Get this from `package:native` when that
// is merged.
static const _validTargets = [
'android_arm',
'android_arm64',
'android_ia32',
'android_x64',
'fuchsia_arm64',
'fuchsia_x64',
'ios_arm',
'ios_arm64',
'ios_x64',
'iossimulator_arm64',
'iossimulator_x64',
'linux_arm',
'linux_arm64',
'linux_ia32',
'linux_riscv32',
'linux_riscv64',
'linux_x64',
'macos_arm64',
'macos_x64',
'windows_arm64',
'windows_ia32',
'windows_x64',
];
/// An invalid target is considered only a warning.
///
/// For example, we may remove ia32 at some point, but we might have existing
/// yamls lingering around.
bool _validateTarget(Object object) {
if (!_validTargets.contains(object)) {
_reportError(
'Unexpected target: $object. Valid targets: $_validTargets.',
severity: CfeSeverity.warning,
);
return false;
}
return true;
}
bool _validateAssets(Object object) {
if (object is! Map) {
_reportError('Unexpected assets mapping: $object. Expected a Map.');
return false;
}
var isValid = true;
for (final entry in object.entries) {
isValid &= _validateAsset(entry.key);
isValid &= _validateAssetPath(entry.value);
}
return isValid;
}
bool _validateAsset(Object object) {
if (object is! String) {
_reportError('Unexpected assets name: $object. Expected a String.');
return false;
}
return true;
}
final _pathTypesWithPath = ['absolute', 'relative', 'system'];
late final _validPathTypes = [..._pathTypesWithPath, 'executable', 'process']
..sort();
bool _validateAssetPath(Object object) {
if (object is! List || object.isEmpty) {
_reportError(
'Unexpected asset path: $object. Expected a non-empty List.',
);
return false;
}
final pathType = object[0];
if (pathType is! String || !_validPathTypes.contains(pathType)) {
_reportError(
'Unexpected path type: $pathType. Valid path types: $_validPathTypes.',
);
return false;
}
final needsPath = _pathTypesWithPath.contains(pathType);
final listLength = 1 + (needsPath ? 1 : 0);
if (object.length != listLength) {
_reportError(
'Unexpected asset path: $object. Expected list with $listLength elements.',
);
return false;
}
if (needsPath) {
final path = object[1];
if (path is! String) {
_reportError('Unexpected path: $path. Expected a String.');
return false;
}
}
return true;
}
}