blob: 47e7e96b6700e50eea1c6c08fbfc3ee5cbb65256 [file] [log] [blame]
// Copyright (c) 2019, 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.
// Parsing and serialization of package configurations.
import 'dart:convert';
import 'dart:typed_data';
import 'errors.dart';
import 'package_config_impl.dart';
import 'packages_file.dart' as packages_file;
import 'util.dart';
const String _configVersionKey = 'configVersion';
const String _packagesKey = 'packages';
const List<String> _topNames = [_configVersionKey, _packagesKey];
const String _nameKey = 'name';
const String _rootUriKey = 'rootUri';
const String _packageUriKey = 'packageUri';
const String _languageVersionKey = 'languageVersion';
const List<String> _packageNames = [
_nameKey,
_rootUriKey,
_packageUriKey,
_languageVersionKey
];
const String _generatedKey = 'generated';
const String _generatorKey = 'generator';
const String _generatorVersionKey = 'generatorVersion';
final _jsonUtf8Decoder = json.fuse(utf8).decoder;
PackageConfig parsePackageConfigBytes(
Uint8List bytes, Uri file, void Function(Object error) onError) {
// TODO(lrn): Make this simpler. Maybe parse directly from bytes.
Object? jsonObject;
try {
jsonObject = _jsonUtf8Decoder.convert(bytes);
} on FormatException catch (e) {
onError(PackageConfigFormatException.from(e));
return const SimplePackageConfig.empty();
}
return parsePackageConfigJson(jsonObject, file, onError);
}
PackageConfig parsePackageConfigString(
String source, Uri file, void Function(Object error) onError) {
Object? jsonObject;
try {
jsonObject = jsonDecode(source);
} on FormatException catch (e) {
onError(PackageConfigFormatException.from(e));
return const SimplePackageConfig.empty();
}
return parsePackageConfigJson(jsonObject, file, onError);
}
/// Creates a [PackageConfig] from a parsed JSON-like object structure.
///
/// The [json] argument must be a JSON object (`Map<String, Object?>`)
/// containing a `"configVersion"` entry with an integer value in the range
/// 1 to [PackageConfig.maxVersion],
/// and with a `"packages"` entry which is a JSON array (`List<Object?>`)
/// containing JSON objects which each has the following properties:
///
/// * `"name"`: The package name as a string.
/// * `"rootUri"`: The root of the package as a URI stored as a string.
/// * `"packageUri"`: Optionally the root of for `package:` URI resolution
/// for the package, as a relative URI below the root URI
/// stored as a string.
/// * `"languageVersion"`: Optionally a language version string which is a
/// an integer numeral, a decimal point (`.`) and another integer numeral,
/// where the integer numeral cannot have a sign, and can only have a
/// leading zero if the entire numeral is a single zero.
///
/// All other properties are stored in [extraData].
///
/// The [baseLocation] is used as base URI to resolve the "rootUri"
/// URI referencestring.
PackageConfig parsePackageConfigJson(
Object? json, Uri baseLocation, void Function(Object error) onError) {
if (!baseLocation.hasScheme || baseLocation.isScheme('package')) {
throw PackageConfigArgumentError(baseLocation.toString(), 'baseLocation',
'Must be an absolute non-package: URI');
}
if (!baseLocation.path.endsWith('/')) {
baseLocation = baseLocation.resolveUri(Uri(path: '.'));
}
String typeName<T>() {
if (0 is T) return 'int';
if ('' is T) return 'string';
if (const [] is T) return 'array';
return 'object';
}
T? checkType<T>(Object? value, String name, [String? packageName]) {
if (value is T) return value;
// The only types we are called with are [int], [String], [List<Object?>]
// and Map<String, Object?>. Recognize which to give a better error message.
var message =
"$name${packageName != null ? " of package $packageName" : ""}"
' is not a JSON ${typeName<T>()}';
onError(PackageConfigFormatException(message, value));
return null;
}
Package? parsePackage(Map<String, Object?> entry) {
String? name;
String? rootUri;
String? packageUri;
String? languageVersion;
Map<String, Object?>? extraData;
var hasName = false;
var hasRoot = false;
var hasVersion = false;
entry.forEach((key, value) {
switch (key) {
case _nameKey:
hasName = true;
name = checkType<String>(value, _nameKey);
break;
case _rootUriKey:
hasRoot = true;
rootUri = checkType<String>(value, _rootUriKey, name);
break;
case _packageUriKey:
packageUri = checkType<String>(value, _packageUriKey, name);
break;
case _languageVersionKey:
hasVersion = true;
languageVersion = checkType<String>(value, _languageVersionKey, name);
break;
default:
(extraData ??= {})[key] = value;
break;
}
});
if (!hasName) {
onError(PackageConfigFormatException('Missing name entry', entry));
}
if (!hasRoot) {
onError(PackageConfigFormatException('Missing rootUri entry', entry));
}
if (name == null || rootUri == null) return null;
var parsedRootUri = Uri.parse(rootUri!);
var relativeRoot = !hasAbsolutePath(parsedRootUri);
var root = baseLocation.resolveUri(parsedRootUri);
if (!root.path.endsWith('/')) root = root.replace(path: '${root.path}/');
var packageRoot = root;
if (packageUri != null) packageRoot = root.resolve(packageUri!);
if (!packageRoot.path.endsWith('/')) {
packageRoot = packageRoot.replace(path: '${packageRoot.path}/');
}
LanguageVersion? version;
if (languageVersion != null) {
version = parseLanguageVersion(languageVersion, onError);
} else if (hasVersion) {
version = SimpleInvalidLanguageVersion('invalid');
}
return SimplePackage.validate(
name!, root, packageRoot, version, extraData, relativeRoot, (error) {
if (error is ArgumentError) {
onError(
PackageConfigFormatException(
error.message.toString(), error.invalidValue),
);
} else {
onError(error);
}
});
}
var map = checkType<Map<String, Object?>>(json, 'value');
if (map == null) return const SimplePackageConfig.empty();
Map<String, Object?>? extraData;
List<Package>? packageList;
int? configVersion;
map.forEach((key, value) {
switch (key) {
case _configVersionKey:
configVersion = checkType<int>(value, _configVersionKey) ?? 2;
break;
case _packagesKey:
var packageArray = checkType<List<Object?>>(value, _packagesKey) ?? [];
var packages = <Package>[];
for (var package in packageArray) {
var packageMap =
checkType<Map<String, Object?>>(package, 'package entry');
if (packageMap != null) {
var entry = parsePackage(packageMap);
if (entry != null) {
packages.add(entry);
}
}
}
packageList = packages;
break;
default:
(extraData ??= {})[key] = value;
break;
}
});
if (configVersion == null) {
onError(PackageConfigFormatException('Missing configVersion entry', json));
configVersion = 2;
}
if (packageList == null) {
onError(PackageConfigFormatException('Missing packages list', json));
packageList = [];
}
return SimplePackageConfig(configVersion!, packageList!, extraData, (error) {
if (error is ArgumentError) {
onError(
PackageConfigFormatException(
error.message.toString(), error.invalidValue),
);
} else {
onError(error);
}
});
}
final _jsonUtf8Encoder = JsonUtf8Encoder(' ');
void writePackageConfigJsonUtf8(
PackageConfig config, Uri? baseUri, Sink<List<int>> output) {
// Can be optimized.
var data = packageConfigToJson(config, baseUri);
output.add(_jsonUtf8Encoder.convert(data) as Uint8List);
}
void writePackageConfigJsonString(
PackageConfig config, Uri? baseUri, StringSink output) {
// Can be optimized.
var data = packageConfigToJson(config, baseUri);
output.write(JsonEncoder.withIndent(' ').convert(data));
}
Map<String, Object?> packageConfigToJson(PackageConfig config, Uri? baseUri) =>
<String, Object?>{
...?_extractExtraData(config.extraData, _topNames),
_configVersionKey: PackageConfig.maxVersion,
_packagesKey: [
for (var package in config.packages)
<String, Object?>{
_nameKey: package.name,
_rootUriKey: trailingSlash((package.relativeRoot
? relativizeUri(package.root, baseUri)
: package.root)
.toString()),
if (package.root != package.packageUriRoot)
_packageUriKey: trailingSlash(
relativizeUri(package.packageUriRoot, package.root)
.toString()),
if (package.languageVersion != null &&
package.languageVersion is! InvalidLanguageVersion)
_languageVersionKey: package.languageVersion.toString(),
...?_extractExtraData(package.extraData, _packageNames),
}
],
};
void writeDotPackages(PackageConfig config, Uri baseUri, StringSink output) {
var extraData = config.extraData;
// Write .packages too.
String? comment;
if (extraData is Map<String, Object?>) {
var generator = extraData[_generatorKey];
if (generator is String) {
var generated = extraData[_generatedKey];
var generatorVersion = extraData[_generatorVersionKey];
comment = 'Generated by $generator'
"${generatorVersion is String ? " $generatorVersion" : ""}"
"${generated is String ? " on $generated" : ""}.";
}
}
packages_file.write(output, config, baseUri: baseUri, comment: comment);
}
/// If "extraData" is a JSON map, then return it, otherwise return null.
///
/// If the value contains any of the [reservedNames] for the current context,
/// entries with that name in the extra data are dropped.
Map<String, Object?>? _extractExtraData(
Object? data, Iterable<String> reservedNames) {
if (data is Map<String, Object?>) {
if (data.isEmpty) return null;
for (var name in reservedNames) {
if (data.containsKey(name)) {
var filteredData = {
for (var key in data.keys)
if (!reservedNames.contains(key)) key: data[key]
};
if (filteredData.isEmpty) return null;
for (var value in filteredData.values) {
if (!_validateJson(value)) return null;
}
return filteredData;
}
}
return data;
}
return null;
}
/// Checks that the object is a valid JSON-like data structure.
bool _validateJson(Object? object) {
if (object == null || true == object || false == object) return true;
if (object is num || object is String) return true;
if (object is List<Object?>) {
return object.every(_validateJson);
}
if (object is Map<String, Object?>) {
return object.values.every(_validateJson);
}
return false;
}