blob: cfda8b618e9afb7455c1ae0de7af630756e4d1d9 [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.
import 'dart:convert';
import 'package:pub_semver/pub_semver.dart';
/// Parse the content of a `package_config.json` file located at the [uri].
PackageConfigJson parsePackageConfigJson(Uri uri, String content) {
assert(uri.isAbsolute);
return _PackageConfigJsonParser(uri, content).parse();
}
class LanguageVersion {
final int major;
final int minor;
const LanguageVersion(this.major, this.minor);
@override
bool operator ==(Object other) {
return other is LanguageVersion &&
other.major == major &&
other.minor == minor;
}
@override
String toString() => '$major.$minor';
}
/// Information about packages used by a Pub package.
///
/// It represents a parsed and processed `package_config.json` file.
class PackageConfigJson {
/// The absolute URI of the file.
final Uri uri;
/// The version of the format.
final int configVersion;
/// The list of packages.
final List<PackageConfigJsonPackage> packages;
/// The timestamp for when the file was generated.
///
/// Might be `null`.
final DateTime? generated;
/// The generator which created the file, typically "pub".
///
/// Might be `null`.
final String? generator;
/// The version of the generator, if the generator wants to remember that
/// information. The version must be a Semantic Version. Pub can use the
/// SDK version.
///
/// Might be `null`.
final Version? generatorVersion;
PackageConfigJson({
required this.uri,
required this.configVersion,
required this.packages,
required this.generated,
required this.generator,
required this.generatorVersion,
});
}
/// Description of a single package in [PackageConfigJson].
class PackageConfigJsonPackage {
/// The name of the package.
final String name;
/// The root directory of the package. All files inside this directory,
/// including any subdirectories, are considered to belong to the package.
final Uri rootUri;
/// The package directory, containing files available to Dart programs
/// using `package:packageName/...` URIs. It is either the [rootUri], or a
/// sub-directory of the [rootUri].
final Uri packageUri;
/// The language version for the package, or `null` if not specified.
final LanguageVersion? languageVersion;
PackageConfigJsonPackage({
required this.name,
required this.rootUri,
required this.packageUri,
required this.languageVersion,
});
}
class _PackageConfigJsonParser {
final RegExp _languageVersionRegExp = RegExp(r'(0|[1-9]\d*)\.(0|[1-9]\d*)');
final Uri uri;
final String content;
late int version;
List<PackageConfigJsonPackage> packages = [];
DateTime? generated;
String? generator;
Version? generatorVersion;
_PackageConfigJsonParser(this.uri, this.content);
PackageConfigJson parse() {
var contentObject = json.decode(content);
if (contentObject is Map<String, dynamic>) {
_parseVersion(contentObject);
_parsePackages(contentObject);
_parseGenerated(contentObject);
return PackageConfigJson(
uri: uri,
configVersion: version,
packages: packages,
generated: generated,
generator: generator,
generatorVersion: generatorVersion,
);
} else {
throw FormatException("Expected a JSON object.", content);
}
}
T? _getOptionalField<T>(Map<String, dynamic> map, String name) {
var object = map[name];
if (object is T?) {
return object;
} else {
var actualType = object.runtimeType;
throw FormatException(
"Expected '$T' value for the '$name' field, found '$actualType'.",
content,
);
}
}
T _getRequiredField<T>(Map<String, dynamic> map, String name) {
var object = map[name];
if (object is T) {
return object;
} else if (object == null) {
throw FormatException("Missing the '$name' field.", content);
} else {
var actualType = object.runtimeType;
throw FormatException(
"Expected '$T' value for the '$name' field, found '$actualType'.",
content,
);
}
}
void _parseGenerated(Map<String, dynamic> map) {
var generatedStr = _getOptionalField<String>(map, 'generated');
if (generatedStr != null) {
generated = DateTime.parse(generatedStr);
}
generator = _getOptionalField<String>(map, 'generator');
var generatorVersionStr = _getOptionalField<String>(
map,
'generatorVersion',
);
if (generatorVersionStr != null) {
generatorVersion = Version.parse(generatorVersionStr);
}
}
void _parsePackage(Map<String, Object?> map) {
var name = _getRequiredField<String>(map, 'name');
var rootUriStr = _getRequiredField<String>(map, 'rootUri');
var rootUri = uri.resolve(rootUriStr);
rootUri = _ensureDirectoryUri(rootUri);
var packageUri = rootUri;
var packageUriStr = _getOptionalField<String>(map, 'packageUri');
if (packageUriStr != null) {
var packageUriRel = Uri.parse(packageUriStr);
if (packageUriRel.isAbsolute) {
throw FormatException(
"The value of the field 'packageUri' must be relative, "
"actually '$packageUriStr', for the package '$name'.",
content,
);
}
packageUri = rootUri.resolveUri(packageUriRel);
packageUri = _ensureDirectoryUri(packageUri);
if (!_isNestedUri(packageUri, rootUri)) {
throw FormatException(
"The resolved 'packageUri' must be inside the rootUri, "
"actually '$packageUri' is not in '$rootUri', "
"for the package '$name'.",
content,
);
}
}
var languageVersion = _parsePackageLanguageVersion(map);
packages.add(
PackageConfigJsonPackage(
name: name,
rootUri: rootUri,
packageUri: packageUri,
languageVersion: languageVersion,
),
);
}
LanguageVersion? _parsePackageLanguageVersion(Map<String, Object?> map) {
var versionStr = _getOptionalField<String>(map, 'languageVersion');
if (versionStr == null) {
return null;
}
var match = _languageVersionRegExp.matchAsPrefix(versionStr);
if (match != null && match.end == versionStr.length) {
var major = int.parse(match.group(1)!);
var minor = int.parse(match.group(2)!);
return LanguageVersion(major, minor);
} else {
throw FormatException(
"Invalid 'languageVersion' format '$versionStr'.",
content,
);
}
}
void _parsePackages(Map<String, Object?> map) {
var packagesObject = _getRequiredField<List<Object?>>(map, 'packages');
for (var packageObject in packagesObject) {
if (packageObject is Map<String, dynamic>) {
_parsePackage(packageObject);
}
}
}
void _parseVersion(Map<String, Object?> map) {
version = _getRequiredField(map, 'configVersion');
if (version != 2) {
throw FormatException("Unsupported config version: $version");
}
}
static Uri _ensureDirectoryUri(Uri uri) {
var path = uri.path;
if (path.endsWith('/')) {
return uri;
} else {
return uri.replace(path: '$path/');
}
}
/// Return `true` if the [nested] is the [enclosing], or is in it.
static bool _isNestedUri(Uri nested, Uri enclosing) {
var nestedStr = '$nested';
var enclosingStr = '$enclosing';
return nestedStr.contains(enclosingStr);
}
}