blob: 27abf505cfe1777b8edfaad9c1e4fc9b5895d336 [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 onError(Object error)) {
// TODO(lrn): Make this simpler. Maybe parse directly from bytes.
var 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 onError(Object error)) {
var 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, dynamic>`)
/// 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<dynamic>`)
/// 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(
dynamic json, Uri baseLocation, void onError(Object error)) {
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>(dynamic value, String name, [String /*?*/ packageName]) {
if (value is T) return value;
// The only types we are called with are [int], [String], [List<dynamic>]
// and Map<String, dynamic>. 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, dynamic> entry) {
String /*?*/ name;
String /*?*/ rootUri;
String /*?*/ packageUri;
String /*?*/ languageVersion;
Map<String, dynamic> /*?*/ 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 root = baseLocation.resolve(rootUri);
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,
(error) {
if (error is ArgumentError) {
onError(
PackageConfigFormatException(error.message, error.invalidValue));
} else {
onError(error);
}
});
}
var map = checkType<Map<String, dynamic>>(json, "value");
if (map == null) return const SimplePackageConfig.empty();
Map<String, dynamic> /*?*/ 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<dynamic>>(value, _packagesKey) ?? [];
var packages = <Package>[];
for (var package in packageArray) {
var packageMap =
checkType<Map<String, dynamic>>(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, 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) as Uint8List);
}
Map<String, dynamic> packageConfigToJson(PackageConfig config, Uri baseUri) =>
<String, dynamic>{
...?_extractExtraData(config.extraData, _topNames),
_configVersionKey: PackageConfig.maxVersion,
_packagesKey: [
for (var package in config.packages)
<String, dynamic>{
_nameKey: package.name,
_rootUriKey: relativizeUri(package.root, baseUri).toString(),
if (package.root != package.packageUriRoot)
_packageUriKey:
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 != null) {
String /*?*/ generator = extraData[_generatorKey];
if (generator != null) {
String /*?*/ generated = extraData[_generatedKey];
String /*?*/ generatorVersion = extraData[_generatorVersionKey];
comment = "Generated by $generator"
"${generatorVersion != null ? " $generatorVersion" : ""}"
"${generated != null ? " 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, dynamic> /*?*/ _extractExtraData(
dynamic data, Iterable<String> reservedNames) {
if (data is Map<String, dynamic>) {
if (data.isEmpty) return null;
for (var name in reservedNames) {
if (data.containsKey(name)) {
data = {
for (var key in data.keys)
if (!reservedNames.contains(key)) key: data[key]
};
if (data.isEmpty) return null;
for (var value in data.values) {
if (!_validateJson(value)) return null;
}
}
}
return data;
}
return null;
}
/// Checks that the object is a valid JSON-like data structure.
bool _validateJson(dynamic object) {
if (object == null || true == object || false == object) return true;
if (object is num || object is String) return true;
if (object is List<dynamic>) {
return object.every(_validateJson);
}
if (object is Map<String, dynamic>) {
return object.values.every(_validateJson);
}
return false;
}