| // 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:typed_data'; |
| |
| import 'package:meta/meta.dart' show sealed; |
| |
| import 'errors.dart'; |
| import 'package_config_json.dart'; |
| import 'util.dart'; |
| |
| /// A package configuration. |
| /// |
| /// Associates configuration data to packages and files in packages. |
| /// |
| /// More members may be added to this class in the future, |
| /// so classes outside of this package must not implement [PackageConfig] |
| /// or any subclass of it. |
| @sealed |
| abstract class PackageConfig { |
| /// The lowest configuration version currently supported. |
| static const int minVersion = 2; |
| |
| /// The highest configuration version currently supported. |
| static const int maxVersion = 2; |
| |
| /// An empty package configuration. |
| /// |
| /// A package configuration with no available packages. |
| /// Is used as a default value where a package configuration |
| /// is expected, but none have been specified or found. |
| static const PackageConfig empty = SimplePackageConfig.empty(); |
| |
| /// Creates a package configuration with the provided available [packages]. |
| /// |
| /// The packages must be valid packages (valid package name, valid |
| /// absolute directory URIs, valid language version, if any), |
| /// and there must not be two packages with the same name. |
| /// |
| /// The package's root ([Package.root]) and package-root |
| /// ([Package.packageUriRoot]) paths must satisfy a number of constraints |
| /// We say that one path (which we know ends with a `/` character) |
| /// is inside another path, if the latter path is a prefix of the former path, |
| /// including the two paths being the same. |
| /// |
| /// * No package's root must be the same as another package's root. |
| /// * The package-root of a package must be inside the package's root. |
| /// * If one package's package-root is inside another package's root, |
| /// then the latter package's package root must not be inside the former |
| /// package's root. (No getting between a package and its package root!) |
| /// This also disallows a package's root being the same as another |
| /// package's package root. |
| /// |
| /// If supplied, the [extraData] will be available as the |
| /// [PackageConfig.extraData] of the created configuration. |
| /// |
| /// The version of the resulting configuration is always [maxVersion]. |
| factory PackageConfig(Iterable<Package> packages, {Object? extraData}) => |
| SimplePackageConfig(maxVersion, packages, extraData); |
| |
| /// Parses a package configuration file. |
| /// |
| /// The [bytes] must be an UTF-8 encoded JSON object |
| /// containing a valid package configuration. |
| /// |
| /// The [baseUri] is used as the base for resolving relative |
| /// URI references in the configuration file. If the configuration |
| /// has been read from a file, the [baseUri] can be the URI of that |
| /// file, or of the directory it occurs in. |
| /// |
| /// If [onError] is provided, errors found during parsing or building |
| /// the configuration are reported by calling [onError] instead of |
| /// throwing, and parser makes a *best effort* attempt to continue |
| /// despite the error. The input must still be valid JSON. |
| /// The result may be [PackageConfig.empty] if there is no way to |
| /// extract useful information from the bytes. |
| static PackageConfig parseBytes( |
| Uint8List bytes, |
| Uri baseUri, { |
| void Function(Object error)? onError, |
| }) => parsePackageConfigBytes(bytes, baseUri, onError ?? throwError); |
| |
| /// Parses a package configuration file. |
| /// |
| /// The [configuration] must be a JSON object |
| /// containing a valid package configuration. |
| /// |
| /// The [baseUri] is used as the base for resolving relative |
| /// URI references in the configuration file. If the configuration |
| /// has been read from a file, the [baseUri] can be the URI of that |
| /// file, or of the directory it occurs in. |
| /// |
| /// If [onError] is provided, errors found during parsing or building |
| /// the configuration are reported by calling [onError] instead of |
| /// throwing, and parser makes a *best effort* attempt to continue |
| /// despite the error. The input must still be valid JSON. |
| /// The result may be [PackageConfig.empty] if there is no way to |
| /// extract useful information from the bytes. |
| static PackageConfig parseString( |
| String configuration, |
| Uri baseUri, { |
| void Function(Object error)? onError, |
| }) => parsePackageConfigString(configuration, baseUri, onError ?? throwError); |
| |
| /// Parses the JSON data of a package configuration file. |
| /// |
| /// The [jsonData] must be a JSON-like Dart data structure, |
| /// like the one provided by parsing JSON text using `dart:convert`, |
| /// containing a valid package configuration. |
| /// |
| /// The [baseUri] is used as the base for resolving relative |
| /// URI references in the configuration file. If the configuration |
| /// has been read from a file, the [baseUri] can be the URI of that |
| /// file, or of the directory it occurs in. |
| /// |
| /// If [onError] is provided, errors found during parsing or building |
| /// the configuration are reported by calling [onError] instead of |
| /// throwing, and parser makes a *best effort* attempt to continue |
| /// despite the error. The input must still be valid JSON. |
| /// The result may be [PackageConfig.empty] if there is no way to |
| /// extract useful information from the bytes. |
| static PackageConfig parseJson( |
| Object? jsonData, |
| Uri baseUri, { |
| void Function(Object error)? onError, |
| }) => parsePackageConfigJson(jsonData, baseUri, onError ?? throwError); |
| |
| /// Writes a configuration file for this configuration on [output]. |
| /// |
| /// If [baseUri] is provided, URI references in the generated file |
| /// will be made relative to [baseUri] where possible. |
| static void writeBytes( |
| PackageConfig configuration, |
| Sink<Uint8List> output, [ |
| Uri? baseUri, |
| ]) { |
| writePackageConfigJsonUtf8(configuration, baseUri, output); |
| } |
| |
| /// Writes a configuration JSON text for this configuration on [output]. |
| /// |
| /// If [baseUri] is provided, URI references in the generated file |
| /// will be made relative to [baseUri] where possible. |
| static void writeString( |
| PackageConfig configuration, |
| StringSink output, [ |
| Uri? baseUri, |
| ]) { |
| writePackageConfigJsonString(configuration, baseUri, output); |
| } |
| |
| /// Converts a configuration to a JSON-like data structure. |
| /// |
| /// If [baseUri] is provided, URI references in the generated data |
| /// will be made relative to [baseUri] where possible. |
| static Map<String, Object?> toJson( |
| PackageConfig configuration, [ |
| Uri? baseUri, |
| ]) => packageConfigToJson(configuration, baseUri); |
| |
| /// The configuration version number. |
| /// |
| /// So far these have been 1 or 2, where |
| /// * Version one is the `.packages` file format, and is no longer supported. |
| /// * Version two is the first `package_config.json` format. |
| /// |
| /// Instances of this class supports both, and the version |
| /// is only useful for detecting which kind of file the configuration |
| /// was read from. |
| int get version; |
| |
| /// All the available packages of this configuration. |
| /// |
| /// No two of these packages have the same name, |
| /// and no two [Package.root] directories overlap. |
| Iterable<Package> get packages; |
| |
| /// Look up a package by name. |
| /// |
| /// Returns the [Package] from [packages] with [packageName] as |
| /// [Package.name]. Returns `null` if the package is not available in the |
| /// current configuration. |
| Package? operator [](String packageName); |
| |
| /// Provides the associated package for a specific [file] (or directory). |
| /// |
| /// Returns a [Package] which contains the [file]'s path, if any. |
| /// That is, the [Package.root] directory is a parent directory |
| /// of the [file]'s location. |
| /// |
| /// Returns `null` if the file does not belong to any package. |
| Package? packageOf(Uri file); |
| |
| /// Resolves a `package:` URI to a non-package URI |
| /// |
| /// The [packageUri] must be a valid package URI. That means: |
| /// * A URI with `package` as scheme, |
| /// * with no authority part (`package://...`), |
| /// * with a path starting with a valid package name followed by a slash, and |
| /// * with no query or fragment part. |
| /// |
| /// Throws an [ArgumentError] (which also implements [PackageConfigError]) |
| /// if the package URI is not valid. |
| /// |
| /// Returns `null` if the package name of [packageUri] is not available |
| /// in this package configuration. |
| /// Returns the remaining path of the package URI resolved relative to the |
| /// [Package.packageUriRoot] of the corresponding package. |
| Uri? resolve(Uri packageUri); |
| |
| /// The package URI which resolves to [nonPackageUri]. |
| /// |
| /// The [nonPackageUri] must not have any query or fragment part, |
| /// and it must not have `package` as scheme. |
| /// Throws an [ArgumentError] (which also implements [PackageConfigError]) |
| /// if the non-package URI is not valid. |
| /// |
| /// Returns a package URI which [resolve] will convert to [nonPackageUri], |
| /// if any such URI exists. Returns `null` if no such package URI exists. |
| Uri? toPackageUri(Uri nonPackageUri); |
| |
| /// Extra data associated with the package configuration. |
| /// |
| /// The data may be in any format, depending on who introduced it. |
| /// The standard `package_config.json` file storage will only store |
| /// JSON-like list/map data structures. |
| Object? get extraData; |
| } |
| |
| /// Configuration data for a single package. |
| @sealed |
| abstract class Package { |
| /// Creates a package with the provided properties. |
| /// |
| /// The [name] must be a valid package name. |
| /// The [root] must be an absolute directory URI, meaning an absolute URI |
| /// with no query or fragment path and a path starting and ending with `/`. |
| /// The [packageUriRoot], if provided, must be either an absolute |
| /// directory URI or a relative URI reference which is then resolved |
| /// relative to [root]. It must then also be a subdirectory of [root], |
| /// or the same directory, and must end with `/`. |
| /// If [languageVersion] is supplied, it must be a valid Dart language |
| /// version, which means two decimal integer literals separated by a `.`, |
| /// where the integer literals have no leading zeros unless they are |
| /// a single zero digit. |
| /// |
| /// The [relativeRoot] controls whether the [root] is written as |
| /// relative to the `package_config.json` file when the package |
| /// configuration is written to a file. It defaults to being relative. |
| /// |
| /// If [extraData] is supplied, it will be available as the |
| /// [Package.extraData] of the created package. |
| factory Package( |
| String name, |
| Uri root, { |
| Uri? packageUriRoot, |
| LanguageVersion? languageVersion, |
| Object? extraData, |
| bool relativeRoot = true, |
| }) => |
| SimplePackage.validate( |
| name, |
| root, |
| packageUriRoot, |
| languageVersion, |
| extraData, |
| relativeRoot, |
| throwError, |
| )!; |
| |
| /// The package-name of the package. |
| String get name; |
| |
| /// The location of the root of the package. |
| /// |
| /// Is always an absolute URI with no query or fragment parts, |
| /// and with a path ending in `/`. |
| /// |
| /// All files in the [root] directory are considered |
| /// part of the package for purposes where that that matters. |
| Uri get root; |
| |
| /// The root of the files available through `package:` URIs. |
| /// |
| /// A `package:` URI with [name] as the package name is |
| /// resolved relative to this location. |
| /// |
| /// Is always an absolute URI with no query or fragment part |
| /// with a path ending in `/`, |
| /// and with a location which is a subdirectory |
| /// of the [root], or the same as the [root]. |
| Uri get packageUriRoot; |
| |
| /// The default language version associated with this package. |
| /// |
| /// Each package may have a default language version associated, |
| /// which is the language version used to parse and compile |
| /// Dart files in the package. |
| /// A package version is defined by two non-negative numbers, |
| /// the *major* and *minor* version numbers. |
| /// |
| /// A package may have no language version associated with it |
| /// in the package configuration, in which case tools should |
| /// use a default behavior for the package. |
| LanguageVersion? get languageVersion; |
| |
| /// Extra data associated with the specific package. |
| /// |
| /// The data may be in any format, depending on who introduced it. |
| /// The standard `package_config.json` file storage will only store |
| /// JSON-like list/map data structures. |
| Object? get extraData; |
| |
| /// Whether the [root] URI should be written as relative. |
| /// |
| /// When the configuration is written to a `package_config.json` |
| /// file, the [root] URI can be either relative to the file |
| /// location or absolute, controller by this value. |
| bool get relativeRoot; |
| } |
| |
| /// A language version. |
| /// |
| /// A language version is represented by two non-negative integers, |
| /// the [major] and [minor] version numbers. |
| /// |
| /// If errors during parsing are handled using an `onError` handler, |
| /// then an *invalid* language version may be represented by an |
| /// [InvalidLanguageVersion] object. |
| @sealed |
| abstract class LanguageVersion implements Comparable<LanguageVersion> { |
| /// The maximal value allowed by [major] and [minor] values; |
| static const int maxValue = 0x7FFFFFFF; |
| |
| /// Constructs a [LanguageVersion] with the specified |
| /// [major] and [minor] version numbers. |
| /// |
| /// Both [major] and [minor] must be greater than or equal to 0 |
| /// and less than or equal to [maxValue]. |
| factory LanguageVersion(int major, int minor) => SimpleLanguageVersion( |
| RangeError.checkValueInInterval(major, 0, maxValue, 'major'), |
| RangeError.checkValueInInterval(minor, 0, maxValue, 'minor'), |
| null, |
| ); |
| |
| /// Parses a language version string. |
| /// |
| /// A valid language version string has the form |
| /// |
| /// > *decimalNumber* `.` *decimalNumber* |
| /// |
| /// where a *decimalNumber* is a non-empty sequence of decimal digits |
| /// with no unnecessary leading zeros (the decimal number only starts |
| /// with a zero digit if that digit is the entire number). |
| /// No spaces are allowed in the string. |
| /// |
| /// If the [source] is valid then it is parsed into a valid |
| /// [LanguageVersion] object. |
| /// If not, then the [onError] is called with a [FormatException]. |
| /// If [onError] is not supplied, it defaults to throwing the exception. |
| /// If the call does not throw, then an [InvalidLanguageVersion] is returned |
| /// containing the original [source]. |
| static LanguageVersion parse( |
| String source, { |
| void Function(Object error)? onError, |
| }) => parseLanguageVersion(source, onError ?? throwError); |
| |
| /// The major language version. |
| /// |
| /// A non-negative integer less than 2<sup>31</sup>. |
| /// |
| /// The value is negative for objects representing *invalid* language |
| /// versions ([InvalidLanguageVersion]). |
| int get major; |
| |
| /// The minor language version. |
| /// |
| /// A non-negative integer less than 2<sup>31</sup>. |
| /// |
| /// The value is negative for objects representing *invalid* language |
| /// versions ([InvalidLanguageVersion]). |
| int get minor; |
| |
| /// Compares language versions. |
| /// |
| /// Two language versions are equal if they have the same |
| /// major and minor version numbers. |
| /// |
| /// A language version is greater than another if the former's major version |
| /// is greater than the latter's major version, or if they have |
| /// the same major version and the former's minor version is greater than |
| /// the latter's. |
| /// |
| /// Invalid language versions are ordered before all valid versions, |
| /// and are all ordered together. |
| @override |
| int compareTo(LanguageVersion other); |
| |
| /// Valid language versions with the same [major] and [minor] values are |
| /// equal. |
| /// |
| /// Invalid language versions ([InvalidLanguageVersion]) are not equal to |
| /// any other object. |
| @override |
| bool operator ==(Object other); |
| |
| @override |
| int get hashCode; |
| |
| /// A string representation of the language version. |
| /// |
| /// A valid language version is represented as |
| /// `"${version.major}.${version.minor}"`. |
| @override |
| String toString(); |
| } |
| |
| /// An *invalid* language version. |
| /// |
| /// Stored in a [Package] when the original language version string |
| /// was invalid and a `onError` handler was passed to the parser |
| /// which did not throw for that error. |
| /// The caller which provided the non-throwing `onError` handler |
| /// should be prepared to encounter invalid values. |
| @sealed |
| abstract class InvalidLanguageVersion implements LanguageVersion { |
| /// The value -1 for an invalid language version. |
| @override |
| int get major; |
| |
| /// The value -1 for an invalid language version. |
| @override |
| int get minor; |
| |
| /// An invalid language version is only equal to itself. |
| @override |
| bool operator ==(Object other); |
| |
| @override |
| int get hashCode; |
| |
| /// The original invalid version string. |
| @override |
| String toString(); |
| } |
| |
| /// Relational operators for [LanguageVersion]. |
| /// |
| /// Compares valid versions with [LanguageVersion.compareTo], |
| /// and rejects invalid versions. |
| /// |
| /// Versions should be verified as valid before using them. |
| extension LanguageVersionRelationalOperators on LanguageVersion { |
| /// Whether this language version is less than [other]. |
| /// |
| /// Neither version being compared must be an [InvalidLanguageVersion]. |
| /// |
| /// For details on how valid language versions are compared, |
| /// check out [LanguageVersion.compareTo]. |
| bool operator <(LanguageVersion other) { |
| // Throw an error if comparing an invalid language version. |
| if (major < 0) _throwThisInvalid(); |
| if (other.major < 0) _throwOtherInvalid(); |
| return compareTo(other) < 0; |
| } |
| |
| /// Whether this language version is less than or equal to [other]. |
| /// |
| /// Neither version being compared must be an [InvalidLanguageVersion]. |
| /// |
| /// For details on how valid language versions are compared, |
| /// check out [LanguageVersion.compareTo]. |
| bool operator <=(LanguageVersion other) { |
| // Throw an error if comparing an invalid language version. |
| if (major < 0) _throwThisInvalid(); |
| if (other.major < 0) _throwOtherInvalid(); |
| return compareTo(other) <= 0; |
| } |
| |
| /// Whether this language version is greater than [other]. |
| /// |
| /// Neither version being compared must be an [InvalidLanguageVersion]. |
| /// |
| /// For details on how valid language versions are compared, |
| /// check out [LanguageVersion.compareTo]. |
| bool operator >(LanguageVersion other) { |
| // Throw an error if comparing an invalid language version. |
| if (major < 0) _throwThisInvalid(); |
| if (other.major < 0) _throwOtherInvalid(); |
| return compareTo(other) > 0; |
| } |
| |
| /// Whether this language version is greater than or equal to [other]. |
| /// |
| /// If either version being compared is an [InvalidLanguageVersion], |
| /// a [StateError] is thrown. Verify versions are valid before comparing them. |
| /// |
| /// For details on how valid language versions are compared, |
| /// check out [LanguageVersion.compareTo]. |
| bool operator >=(LanguageVersion other) { |
| // Throw an error if comparing an invalid language version. |
| if (major < 0) _throwThisInvalid(); |
| if (other.major < 0) _throwOtherInvalid(); |
| return compareTo(other) >= 0; |
| } |
| |
| static Never _throwThisInvalid() => |
| throw UnsupportedError( |
| 'Can\'t compare an invalid language version to another ' |
| 'language version. ' |
| 'Verify language versions are valid before use.', |
| ); |
| static Never _throwOtherInvalid() => |
| throw UnsupportedError( |
| 'Can\'t compare a language version to an invalid language version. ' |
| 'Verify language versions are valid before use.', |
| ); |
| } |
| |
| // -------------------------------------------------------------------- |
| // Implementation of interfaces. Not exported by top-level libraries. |
| |
| const bool _disallowPackagesInsidePackageUriRoot = false; |
| |
| // Implementations of the main data types exposed by the API of this package. |
| |
| @sealed |
| 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. |
| /// |
| /// The version number is [PackageConfig.maxVersion] to avoid |
| /// minimum-version filters discarding the configuration. |
| const SimplePackageConfig.empty() |
| : version = PackageConfig.maxVersion, |
| _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 = TriePackageTree(); |
| 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]; |
| |
| @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. |
| @sealed |
| 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 [source] is a valid Dart language version string. |
| /// |
| /// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`. |
| /// |
| /// Reports a format exception by calling [onError] if the format isn't correct, |
| /// 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); |
| } |
| |
| @sealed |
| class SimpleLanguageVersion implements LanguageVersion { |
| @override |
| final int major; |
| @override |
| final int minor; |
| |
| /// A cache for `toString`, pre-filled with source if created by parsing. |
| /// |
| /// Also used by [SimpleInvalidLanguageVersion] for its invalid source |
| /// or a suitably invalid `toString` value. |
| 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'; |
| |
| @override |
| int compareTo(LanguageVersion other) { |
| var result = major - other.major; |
| if (result != 0) return result; |
| return minor - other.minor; |
| } |
| } |
| |
| @sealed |
| class SimpleInvalidLanguageVersion extends SimpleLanguageVersion |
| implements InvalidLanguageVersion { |
| SimpleInvalidLanguageVersion(String source) : super(-1, -1, source); |
| |
| @override |
| int get hashCode => identityHashCode(this); |
| @override |
| bool operator ==(Object other) => identical(this, other); |
| } |
| |
| abstract class PackageTree { |
| Iterable<Package> get allPackages; |
| SimplePackage? packageOf(Uri file); |
| } |
| |
| class _PackageTrieNode { |
| SimplePackage? package; |
| |
| /// Indexed by path segment. |
| Map<String, _PackageTrieNode> 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 TriePackageTree implements PackageTree { |
| /// Indexed by URI scheme. |
| final Map<String, _PackageTrieNode> _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( |
| _PackageTrieNode node, |
| SimplePackage newPackage, |
| void Function(Object error) onError, |
| ) { |
| var existingPackage = node.package; |
| if (existingPackage != null) { |
| // 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 (_disallowPackagesInsidePackageUriRoot) { |
| 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 node = _map[root.scheme] ??= _PackageTrieNode(); |
| if (_checkConflict(node, newPackage, onError)) return; |
| var segments = root.pathSegments; |
| // Notice that we're skipping the last segment as it's always the empty |
| // string because roots are directories. |
| for (var i = 0; i < segments.length - 1; i++) { |
| var path = segments[i]; |
| node = node.map[path] ??= _PackageTrieNode(); |
| if (_checkConflict(node, newPackage, onError)) return; |
| } |
| node.package = newPackage; |
| _packages.add(newPackage); |
| } |
| |
| bool _isMatch( |
| String path, |
| _PackageTrieNode node, |
| List<SimplePackage> potential, |
| ) { |
| var currentPackage = node.package; |
| if (currentPackage != null) { |
| var currentPackageRootLength = currentPackage.root.toString().length; |
| if (path.length == currentPackageRootLength) return true; |
| var currentPackageUriRoot = currentPackage.packageUriRoot.toString(); |
| // Is [file] 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 currentTrieNode = _map[file.scheme]; |
| if (currentTrieNode == null) return null; |
| var path = file.toString(); |
| var potential = <SimplePackage>[]; |
| if (_isMatch(path, currentTrieNode, potential)) { |
| return currentTrieNode.package; |
| } |
| var segments = file.pathSegments; |
| |
| for (var i = 0; i < segments.length - 1; i++) { |
| var segment = segments[i]; |
| currentTrieNode = currentTrieNode!.map[segment]; |
| if (currentTrieNode == null) break; |
| if (_isMatch(path, currentTrieNode, potential)) { |
| return currentTrieNode.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 or the package URI root path |
| /// of [existingPackage] is inside the root path of [package]. |
| 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()); |