| // 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 = TrielikePackageTree(); |
| for (var originalPackage in packages) { |
| SimplePackage? newPackage; |
| if (originalPackage is! SimplePackage) { |
| // SimplePackage validates these properties. |
| newPackage = SimplePackage.validate( |
| originalPackage.name, |
| originalPackage.root, |
| originalPackage.packageUriRoot, |
| originalPackage.languageVersion, |
| originalPackage.extraData, |
| originalPackage.relativeRoot, (error) { |
| if (error is PackageConfigArgumentError) { |
| onError(PackageConfigArgumentError(packages, 'packages', |
| 'Package ${newPackage!.name}: ${error.message}')); |
| } else { |
| onError(error); |
| } |
| }); |
| if (newPackage == null) continue; |
| } else { |
| newPackage = originalPackage; |
| } |
| var name = newPackage.name; |
| if (packageNames.contains(name)) { |
| onError(PackageConfigArgumentError( |
| name, 'packages', "Duplicate package name '$name'")); |
| continue; |
| } |
| packageNames.add(name); |
| tree.add(newPackage, (error) { |
| if (error is ConflictException) { |
| // There is a conflict with an existing package. |
| var existingPackage = error.existingPackage; |
| switch (error.conflictType) { |
| case ConflictType.sameRoots: |
| onError(PackageConfigArgumentError( |
| originalPackages, |
| 'packages', |
| 'Packages ${newPackage!.name} and ${existingPackage.name} ' |
| 'have the same root directory: ${newPackage.root}.\n')); |
| break; |
| case ConflictType.interleaving: |
| // The new package is inside the package URI root of the existing |
| // package. |
| onError(PackageConfigArgumentError( |
| originalPackages, |
| 'packages', |
| 'Package ${newPackage!.name} is inside the root of ' |
| 'package ${existingPackage.name}, and the package root ' |
| 'of ${existingPackage.name} is inside the root of ' |
| '${newPackage.name}.\n' |
| '${existingPackage.name} package root: ' |
| '${existingPackage.packageUriRoot}\n' |
| '${newPackage.name} root: ${newPackage.root}\n')); |
| break; |
| case ConflictType.insidePackageRoot: |
| onError(PackageConfigArgumentError( |
| originalPackages, |
| 'packages', |
| 'Package ${newPackage!.name} is inside the package root of ' |
| 'package ${existingPackage.name}.\n' |
| '${existingPackage.name} package root: ' |
| '${existingPackage.packageUriRoot}\n' |
| '${newPackage.name} root: ${newPackage.root}\n')); |
| break; |
| } |
| } 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); |
| } |
| |
| class _TrielikePackageTreeHelper { |
| SimplePackage? package; |
| Map<String, _TrielikePackageTreeHelper> map = {}; |
| } |
| |
| /// 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. |
| /// 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. |
| class TrielikePackageTree implements PackageTree { |
| final Map<String, _TrielikePackageTreeHelper> _map = {}; |
| |
| /// A list of all packages. |
| final List<SimplePackage> _packages = []; |
| |
| @override |
| Iterable<Package> get allPackages sync* { |
| for (var package in _packages) { |
| yield package; |
| } |
| } |
| |
| bool _checkConflict(_TrielikePackageTreeHelper currentMapHelper, |
| SimplePackage newPackage, void Function(Object error) onError) { |
| if (currentMapHelper.package != null) { |
| var existingPackage = currentMapHelper.package!; |
| // Trying to add package that is inside the existing package. |
| // 1) If it's an exact match it's not allowed (i.e. the roots can't be |
| // the same). |
| if (newPackage.root.path.length == existingPackage.root.path.length) { |
| onError(ConflictException( |
| newPackage, existingPackage, ConflictType.sameRoots)); |
| return true; |
| } |
| // 2) The existing package has a packageUriRoot thats inside the |
| // root of the new package. |
| if (_beginsWith(0, newPackage.root.toString(), |
| existingPackage.packageUriRoot.toString())) { |
| onError(ConflictException( |
| newPackage, existingPackage, ConflictType.interleaving)); |
| return true; |
| } |
| /* |
| For internal reasons we allow this (for now). One should still never do it |
| though. |
| // 3) The new package is inside the packageUriRoot of existing package. |
| if (_beginsWith(0, existingPackage.packageUriRoot.toString(), |
| newPackage.root.toString())) { |
| onError(ConflictException( |
| newPackage, existingPackage, ConflictType.insidePackageRoot)); |
| return true; |
| } |
| */ |
| } |
| return false; |
| } |
| |
| /// Tries to add `newPackage` 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 an existing |
| /// package's root or package root, is between the two, or if it's inside the |
| /// package root of an existing package. |
| /// |
| /// If a conflict is detected between [newPackage] and a previous package, |
| /// then [onError] is called with a [ConflictException] object |
| /// and the [newPackage] is not added to the tree. |
| /// |
| /// The packages are added in order of their root path. |
| void add(SimplePackage newPackage, void Function(Object error) onError) { |
| var root = newPackage.root; |
| var currentMapHelper = _map[root.scheme] ??= _TrielikePackageTreeHelper(); |
| if (_checkConflict(currentMapHelper, newPackage, onError)) return; |
| var segments = root.pathSegments; |
| for (var i = 0; i < segments.length - 1; i++) { |
| var path = segments[i]; |
| currentMapHelper = |
| currentMapHelper.map[path] ??= _TrielikePackageTreeHelper(); |
| if (_checkConflict(currentMapHelper, newPackage, onError)) return; |
| } |
| currentMapHelper.package = newPackage; |
| _packages.add(newPackage); |
| } |
| |
| bool _isMatch(String path, _TrielikePackageTreeHelper currentMapHelper, |
| List<SimplePackage> potential) { |
| if (currentMapHelper.package != null) { |
| var currentPackage = currentMapHelper.package!; |
| var currentPackageRootLength = currentPackage.root.toString().length; |
| if (path.length == currentPackageRootLength) return true; |
| var currentPackageUriRoot = currentPackage.packageUriRoot.toString(); |
| // Is [file] is inside the package root of [currentPackage]? |
| if (currentPackageUriRoot.length == currentPackageRootLength || |
| _beginsWith(currentPackageRootLength, currentPackageUriRoot, path)) { |
| return true; |
| } |
| potential.add(currentPackage); |
| } |
| return false; |
| } |
| |
| @override |
| SimplePackage? packageOf(Uri file) { |
| var currentMapHelper = _map[file.scheme]; |
| if (currentMapHelper == null) return null; |
| var path = file.toString(); |
| var potential = <SimplePackage>[]; |
| if (_isMatch(path, currentMapHelper, potential)) { |
| return currentMapHelper.package; |
| } |
| var segments = file.pathSegments; |
| |
| for (var i = 0; i < segments.length - 1; i++) { |
| var segment = segments[i]; |
| currentMapHelper = currentMapHelper!.map[segment]; |
| if (currentMapHelper == null) break; |
| if (_isMatch(path, currentMapHelper, potential)) { |
| return currentMapHelper.package; |
| } |
| } |
| if (potential.isEmpty) return null; |
| return potential.last; |
| } |
| } |
| |
| 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; |
| } |
| |
| enum ConflictType { sameRoots, interleaving, insidePackageRoot } |
| |
| /// 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 ConflictType conflictType; |
| |
| /// Creates a root conflict between [package] and [existingPackage]. |
| ConflictException(this.package, this.existingPackage, this.conflictType); |
| } |
| |
| /// Used for sorting packages by root path. |
| int _compareRoot(Package p1, Package p2) => |
| p1.root.toString().compareTo(p2.root.toString()); |