blob: 4167d35f8931418e9efad09909d9fee4fb14cf43 [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 'errors.dart';
import 'package_config.dart';
import 'util.dart';
export 'package_config.dart';
// Implementations of the main data types exposed by the API of this package.
class SimplePackageConfig implements PackageConfig {
@override
final int version;
final Map<String, Package> _packages;
final PackageTree _packageTree;
@override
final Object? extraData;
factory SimplePackageConfig(int version, Iterable<Package> packages,
[Object? extraData, void Function(Object error)? onError]) {
onError ??= throwError;
var validVersion = _validateVersion(version, onError);
var sortedPackages = [...packages]..sort(_compareRoot);
var packageTree = _validatePackages(packages, sortedPackages, onError);
return SimplePackageConfig._(validVersion, packageTree,
{for (var p in packageTree.allPackages) p.name: p}, extraData);
}
SimplePackageConfig._(
this.version, this._packageTree, this._packages, this.extraData);
/// Creates empty configuration.
///
/// The empty configuration can be used in cases where no configuration is
/// found, but code expects a non-null configuration.
const SimplePackageConfig.empty()
: version = 1,
_packageTree = const EmptyPackageTree(),
_packages = const <String, Package>{},
extraData = null;
static int _validateVersion(
int version, void Function(Object error) onError) {
if (version < 0 || version > PackageConfig.maxVersion) {
onError(PackageConfigArgumentError(version, 'version',
'Must be in the range 1 to ${PackageConfig.maxVersion}'));
return 2; // The minimal version supporting a SimplePackageConfig.
}
return version;
}
static PackageTree _validatePackages(Iterable<Package> originalPackages,
List<Package> packages, void Function(Object error) onError) {
var packageNames = <String>{};
var tree = MutablePackageTree();
for (var originalPackage in packages) {
SimplePackage? package;
if (originalPackage is! SimplePackage) {
// SimplePackage validates these properties.
package = SimplePackage.validate(
originalPackage.name,
originalPackage.root,
originalPackage.packageUriRoot,
originalPackage.languageVersion,
originalPackage.extraData,
originalPackage.relativeRoot, (error) {
if (error is PackageConfigArgumentError) {
onError(PackageConfigArgumentError(packages, 'packages',
'Package ${package!.name}: ${error.message}'));
} else {
onError(error);
}
});
if (package == null) continue;
} else {
package = originalPackage;
}
var name = package.name;
if (packageNames.contains(name)) {
onError(PackageConfigArgumentError(
name, 'packages', "Duplicate package name '$name'"));
continue;
}
packageNames.add(name);
tree.add(0, package, (error) {
if (error is ConflictException) {
// There is a conflict with an existing package.
var existingPackage = error.existingPackage;
if (error.isRootConflict) {
onError(PackageConfigArgumentError(
originalPackages,
'packages',
'Packages ${package!.name} and ${existingPackage.name} '
'have the same root directory: ${package.root}.\n'));
} else {
assert(error.isPackageRootConflict);
// Package is inside the package URI root of the existing package.
onError(PackageConfigArgumentError(
originalPackages,
'packages',
'Package ${package!.name} is inside the package URI root of '
'package ${existingPackage.name}.\n'
'${existingPackage.name} URI root: '
'${existingPackage.packageUriRoot}\n'
'${package.name} root: ${package.root}\n'));
}
} else {
// Any other error.
onError(error);
}
});
}
return tree;
}
@override
Iterable<Package> get packages => _packages.values;
@override
Package? operator [](String packageName) => _packages[packageName];
/// Provides the associated package for a specific [file] (or directory).
///
/// Returns a [Package] which contains the [file]'s path.
/// That is, the [Package.rootUri] directory is a parent directory
/// of the [file]'s location.
/// Returns `null` if the file does not belong to any package.
@override
Package? packageOf(Uri file) => _packageTree.packageOf(file);
@override
Uri? resolve(Uri packageUri) {
var packageName = checkValidPackageUri(packageUri, 'packageUri');
return _packages[packageName]?.packageUriRoot.resolveUri(
Uri(path: packageUri.path.substring(packageName.length + 1)));
}
@override
Uri? toPackageUri(Uri nonPackageUri) {
if (nonPackageUri.isScheme('package')) {
throw PackageConfigArgumentError(
nonPackageUri, 'nonPackageUri', 'Must not be a package URI');
}
if (nonPackageUri.hasQuery || nonPackageUri.hasFragment) {
throw PackageConfigArgumentError(nonPackageUri, 'nonPackageUri',
'Must not have query or fragment part');
}
// Find package that file belongs to.
var package = _packageTree.packageOf(nonPackageUri);
if (package == null) return null;
// Check if it is inside the package URI root.
var path = nonPackageUri.toString();
var root = package.packageUriRoot.toString();
if (_beginsWith(package.root.toString().length, root, path)) {
var rest = path.substring(root.length);
return Uri(scheme: 'package', path: '${package.name}/$rest');
}
return null;
}
}
/// Configuration data for a single package.
class SimplePackage implements Package {
@override
final String name;
@override
final Uri root;
@override
final Uri packageUriRoot;
@override
final LanguageVersion? languageVersion;
@override
final Object? extraData;
@override
final bool relativeRoot;
SimplePackage._(this.name, this.root, this.packageUriRoot,
this.languageVersion, this.extraData, this.relativeRoot);
/// Creates a [SimplePackage] with the provided content.
///
/// The provided arguments must be valid.
///
/// If the arguments are invalid then the error is reported by
/// calling [onError], then the erroneous entry is ignored.
///
/// If [onError] is provided, the user is expected to be able to handle
/// errors themselves. An invalid [languageVersion] string
/// will be replaced with the string `"invalid"`. This allows
/// users to detect the difference between an absent version and
/// an invalid one.
///
/// Returns `null` if the input is invalid and an approximately valid package
/// cannot be salvaged from the input.
static SimplePackage? validate(
String name,
Uri root,
Uri? packageUriRoot,
LanguageVersion? languageVersion,
Object? extraData,
bool relativeRoot,
void Function(Object error) onError) {
var fatalError = false;
var invalidIndex = checkPackageName(name);
if (invalidIndex >= 0) {
onError(PackageConfigFormatException(
'Not a valid package name', name, invalidIndex));
fatalError = true;
}
if (root.isScheme('package')) {
onError(PackageConfigArgumentError(
'$root', 'root', 'Must not be a package URI'));
fatalError = true;
} else if (!isAbsoluteDirectoryUri(root)) {
onError(PackageConfigArgumentError(
'$root',
'root',
'In package $name: Not an absolute URI with no query or fragment '
'with a path ending in /'));
// Try to recover. If the URI has a scheme,
// then ensure that the path ends with `/`.
if (!root.hasScheme) {
fatalError = true;
} else if (!root.path.endsWith('/')) {
root = root.replace(path: root.path + '/');
}
}
if (packageUriRoot == null) {
packageUriRoot = root;
} else if (!fatalError) {
packageUriRoot = root.resolveUri(packageUriRoot);
if (!isAbsoluteDirectoryUri(packageUriRoot)) {
onError(PackageConfigArgumentError(
packageUriRoot,
'packageUriRoot',
'In package $name: Not an absolute URI with no query or fragment '
'with a path ending in /'));
packageUriRoot = root;
} else if (!isUriPrefix(root, packageUriRoot)) {
onError(PackageConfigArgumentError(packageUriRoot, 'packageUriRoot',
'The package URI root is not below the package root'));
packageUriRoot = root;
}
}
if (fatalError) return null;
return SimplePackage._(
name, root, packageUriRoot, languageVersion, extraData, relativeRoot);
}
}
/// Checks whether [version] is a valid Dart language version string.
///
/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
///
/// Reports a format exception on [onError] if not, or if the numbers
/// are too large (at most 32-bit signed integers).
LanguageVersion parseLanguageVersion(
String? source, void Function(Object error) onError) {
var index = 0;
// Reads a positive decimal numeral. Returns the value of the numeral,
// or a negative number in case of an error.
// Starts at [index] and increments the index to the position after
// the numeral.
// It is an error if the numeral value is greater than 0x7FFFFFFFF.
// It is a recoverable error if the numeral starts with leading zeros.
int readNumeral() {
const maxValue = 0x7FFFFFFF;
if (index == source!.length) {
onError(PackageConfigFormatException('Missing number', source, index));
return -1;
}
var start = index;
var char = source.codeUnitAt(index);
var digit = char ^ 0x30;
if (digit > 9) {
onError(PackageConfigFormatException('Missing number', source, index));
return -1;
}
var firstDigit = digit;
var value = 0;
do {
value = value * 10 + digit;
if (value > maxValue) {
onError(
PackageConfigFormatException('Number too large', source, start));
return -1;
}
index++;
if (index == source.length) break;
char = source.codeUnitAt(index);
digit = char ^ 0x30;
} while (digit <= 9);
if (firstDigit == 0 && index > start + 1) {
onError(PackageConfigFormatException(
'Leading zero not allowed', source, start));
}
return value;
}
var major = readNumeral();
if (major < 0) {
return SimpleInvalidLanguageVersion(source);
}
if (index == source!.length || source.codeUnitAt(index) != $dot) {
onError(PackageConfigFormatException("Missing '.'", source, index));
return SimpleInvalidLanguageVersion(source);
}
index++;
var minor = readNumeral();
if (minor < 0) {
return SimpleInvalidLanguageVersion(source);
}
if (index != source.length) {
onError(PackageConfigFormatException(
'Unexpected trailing character', source, index));
return SimpleInvalidLanguageVersion(source);
}
return SimpleLanguageVersion(major, minor, source);
}
abstract class _SimpleLanguageVersionBase implements LanguageVersion {
@override
int compareTo(LanguageVersion other) {
var result = major.compareTo(other.major);
if (result != 0) return result;
return minor.compareTo(other.minor);
}
}
class SimpleLanguageVersion extends _SimpleLanguageVersionBase {
@override
final int major;
@override
final int minor;
String? _source;
SimpleLanguageVersion(this.major, this.minor, this._source);
@override
bool operator ==(Object other) =>
other is LanguageVersion && major == other.major && minor == other.minor;
@override
int get hashCode => (major * 17 ^ minor * 37) & 0x3FFFFFFF;
@override
String toString() => _source ??= '$major.$minor';
}
class SimpleInvalidLanguageVersion extends _SimpleLanguageVersionBase
implements InvalidLanguageVersion {
final String? _source;
SimpleInvalidLanguageVersion(this._source);
@override
int get major => -1;
@override
int get minor => -1;
@override
String toString() => _source!;
}
abstract class PackageTree {
Iterable<Package> get allPackages;
SimplePackage? packageOf(Uri file);
}
/// Packages of a package configuration ordered by root path.
///
/// A package has a root path and a package root path, where the latter
/// contains the files exposed by `package:` URIs.
///
/// A package is said to be inside another package if the root path URI of
/// the latter is a prefix of the root path URI of the former.
///
/// No two packages of a package may have the same root path, so this
/// path prefix ordering defines a tree-like partial ordering on packages
/// of a configuration.
///
/// The package root path of a package must not be inside another package's
/// root path.
/// Entire other packages are allowed inside a package's root or
/// package root path.
///
/// The package tree contains an ordered mapping of unrelated packages
/// (represented by their name) to their immediately nested packages' names.
class MutablePackageTree implements PackageTree {
/// A list of packages that are not nested inside each other.
final List<SimplePackage> packages = [];
/// The tree of the immediately nested packages inside each package.
///
/// Indexed by [Package.name].
/// If a package has no nested packages (which is most often the case),
/// there is no tree object associated with it.
Map<String, MutablePackageTree>? _packageChildren;
@override
Iterable<Package> get allPackages sync* {
for (var package in packages) {
yield package;
}
var children = _packageChildren;
if (children != null) {
for (var tree in children.values) {
yield* tree.allPackages;
}
}
}
/// Tries to (add) `package` to the tree.
///
/// Reports a [ConflictException] if the added package conflicts with an
/// existing package.
/// It conflicts if its root or package root is the same as another
/// package's root or package root, or is between the two.
///
/// If a conflict is detected between [package] and a previous package,
/// then [onError] is called with a [ConflictException] object
/// and the [package] is not added to the tree.
///
/// The packages are added in order of their root path.
/// It is never necessary to insert a node between two existing levels.
void add(
int start, SimplePackage package, void Function(Object error) onError) {
var path = package.root.toString();
for (var treePackage in packages) {
// Check is package is inside treePackage.
var treePackagePath = treePackage.root.toString();
assert(treePackagePath.length > start);
assert(path.startsWith(treePackagePath.substring(0, start)));
if (_beginsWith(start, treePackagePath, path)) {
// Package *is* inside treePackage.
var treePackagePathLength = treePackagePath.length;
if (path.length == treePackagePathLength) {
// Has same root. Do not add package.
onError(ConflictException.root(package, treePackage));
return;
}
var treePackageUriRoot = treePackage.packageUriRoot.toString();
if (_beginsWith(treePackagePathLength, path, treePackageUriRoot)) {
// The treePackage's package root is inside package, which is inside
// the treePackage. This is not allowed.
onError(ConflictException.packageRoot(package, treePackage));
return;
}
_treeOf(treePackage).add(treePackagePathLength, package, onError);
return;
}
}
packages.add(package);
}
@override
SimplePackage? packageOf(Uri file) {
return findPackageOf(0, file.toString());
}
/// Finds package containing [path] in this tree.
///
/// Returns `null` if no such package is found.
///
/// Assumes the first [start] characters of path agrees with all
/// the packages at this level of the tree.
SimplePackage? findPackageOf(int start, String path) {
for (var childPackage in packages) {
var childPath = childPackage.root.toString();
if (_beginsWith(start, childPath, path)) {
// The [package] is inside [childPackage].
var childPathLength = childPath.length;
if (path.length == childPathLength) return childPackage;
var uriRoot = childPackage.packageUriRoot.toString();
// Is [package] is inside the URI root of [childPackage].
if (uriRoot.length == childPathLength ||
_beginsWith(childPathLength, uriRoot, path)) {
return childPackage;
}
return _packageChildren?[childPackage.name]
?.findPackageOf(childPathLength, path) ??
childPackage;
}
}
return null;
}
/// Returns the [PackageTree] of the children of [package].
///
/// Ensures that the object is allocated if necessary.
MutablePackageTree _treeOf(SimplePackage package) {
var children = _packageChildren ??= {};
return children[package.name] ??= MutablePackageTree();
}
}
class EmptyPackageTree implements PackageTree {
const EmptyPackageTree();
@override
Iterable<Package> get allPackages => const Iterable<Package>.empty();
@override
SimplePackage? packageOf(Uri file) => null;
}
/// Checks whether [longerPath] begins with [parentPath].
///
/// Skips checking the [start] first characters which are assumed to
/// already have been matched.
bool _beginsWith(int start, String parentPath, String longerPath) {
if (longerPath.length < parentPath.length) return false;
for (var i = start; i < parentPath.length; i++) {
if (longerPath.codeUnitAt(i) != parentPath.codeUnitAt(i)) return false;
}
return true;
}
/// Conflict between packages added to the same configuration.
///
/// The [package] conflicts with [existingPackage] if it has
/// the same root path ([isRootConflict]) or the package URI root path
/// of [existingPackage] is inside the root path of [package]
/// ([isPackageRootConflict]).
class ConflictException {
/// The existing package that [package] conflicts with.
final SimplePackage existingPackage;
/// The package that could not be added without a conflict.
final SimplePackage package;
/// Whether the conflict is with the package URI root of [existingPackage].
final bool isPackageRootConflict;
/// Creates a root conflict between [package] and [existingPackage].
ConflictException.root(this.package, this.existingPackage)
: isPackageRootConflict = false;
/// Creates a package root conflict between [package] and [existingPackage].
ConflictException.packageRoot(this.package, this.existingPackage)
: isPackageRootConflict = true;
/// WHether the conflict is with the root URI of [existingPackage].
bool get isRootConflict => !isPackageRootConflict;
}
/// Used for sorting packages by root path.
int _compareRoot(Package p1, Package p2) =>
p1.root.toString().compareTo(p2.root.toString());