| // 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; | |
| } | |
| } |