Merge package:package_config into the tools monorepo
diff --git a/pkgs/package_config/.github/dependabot.yml b/pkgs/package_config/.github/dependabot.yml
new file mode 100644
index 0000000..a19a66a
--- /dev/null
+++ b/pkgs/package_config/.github/dependabot.yml
@@ -0,0 +1,16 @@
+# Set update schedule for GitHub Actions
+# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot
+
+version: 2
+updates:
+
+- package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/package_config/.github/workflows/test-package.yml b/pkgs/package_config/.github/workflows/test-package.yml
new file mode 100644
index 0000000..718ec07
--- /dev/null
+++ b/pkgs/package_config/.github/workflows/test-package.yml
@@ -0,0 +1,60 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run tests
+ run: dart test -p chrome,vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/package_config/.gitignore b/pkgs/package_config/.gitignore
new file mode 100644
index 0000000..7b888b8
--- /dev/null
+++ b/pkgs/package_config/.gitignore
@@ -0,0 +1,7 @@
+.packages
+.pub
+.dart_tool/
+.vscode/
+packages
+pubspec.lock
+doc/api/
diff --git a/pkgs/package_config/AUTHORS b/pkgs/package_config/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/package_config/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/package_config/CHANGELOG.md b/pkgs/package_config/CHANGELOG.md
new file mode 100644
index 0000000..f9fec1d
--- /dev/null
+++ b/pkgs/package_config/CHANGELOG.md
@@ -0,0 +1,107 @@
+## 2.1.1-wip
+
+- Require Dart 3.4
+
+## 2.1.0
+
+- Adds `minVersion` to `findPackageConfig` and `findPackageConfigVersion`
+ which allows ignoring earlier versions (which currently only means
+ ignoring version 1, aka. `.packages` files.)
+
+- Changes the version number of `SimplePackageConfig.empty` to the
+ current maximum version.
+
+- Improve file read performance; improve lookup performance.
+- Emit an error when a package is inside the package root of another package.
+- Fix a link in the readme.
+
+## 2.0.2
+
+- Update package description and README.
+- Change to package:lints for style checking.
+- Add an example.
+
+## 2.0.1
+
+- Use unique library names to correct docs issue.
+
+## 2.0.0
+
+- Migrate to null safety.
+- Remove legacy APIs.
+- Adds `relativeRoot` property to `Package` which controls whether to
+ make the root URI relative when writing a configuration file.
+
+## 1.9.3
+
+- Fix `Package` constructor not accepting relative `packageUriRoot`.
+
+## 1.9.2
+
+- Updated to support new rules for picking `package_config.json` over
+ a specified `.packages`.
+- Deduce package root from `.packages` derived package configuration,
+ and default all such packages to language version 2.7.
+
+## 1.9.1
+
+- Remove accidental transitive import of `dart:io` from entrypoints that are
+ supposed to be cross-platform compatible.
+
+## 1.9.0
+
+- Based on new JSON file format with more content.
+- This version includes all the new functionality intended for a 2.0.0
+ version, as well as the, now deprecated, version 1 functionality.
+ When we release 2.0.0, the deprecated functionality will be removed.
+
+## 1.1.0
+
+- Allow parsing files with default-package entries and metadata.
+ A default-package entry has an empty key and a valid package name
+ as value.
+ Metadata is attached as fragments to base URIs.
+
+## 1.0.5
+
+- Fix usage of SDK constants.
+
+## 1.0.4
+
+- Set max SDK version to <3.0.0.
+
+## 1.0.3
+
+- Removed unneeded dependency constraint on SDK.
+
+## 1.0.2
+
+- Update SDK constraint to be 2.0.0 dev friendly.
+
+## 1.0.1
+
+- Fix test to not write to sink after it's closed.
+
+## 1.0.0
+
+- Public API marked stable.
+
+## 0.1.5
+
+- `FilePackagesDirectoryPackages.getBase(..)` performance improvements.
+
+## 0.1.4
+
+- Strong mode fixes.
+
+## 0.1.3
+
+- Invalid test cleanup (to keep up with changes in `Uri`).
+
+## 0.1.1
+
+- Syntax updates.
+
+## 0.1.0
+
+- Initial implementation.
diff --git a/pkgs/package_config/CONTRIBUTING.md b/pkgs/package_config/CONTRIBUTING.md
new file mode 100644
index 0000000..8423ff9
--- /dev/null
+++ b/pkgs/package_config/CONTRIBUTING.md
@@ -0,0 +1,33 @@
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+### Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+All submissions, including submissions by project members, require review.
+
+### File headers
+All files in the project must start with the following header.
+
+ // 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.
+
+### The small print
+Contributions made by corporations are covered by a different agreement than the
+one above, the
+[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate).
diff --git a/pkgs/package_config/LICENSE b/pkgs/package_config/LICENSE
new file mode 100644
index 0000000..7670007
--- /dev/null
+++ b/pkgs/package_config/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/package_config/README.md b/pkgs/package_config/README.md
new file mode 100644
index 0000000..9e2e41f
--- /dev/null
+++ b/pkgs/package_config/README.md
@@ -0,0 +1,26 @@
+[](https://github.com/dart-lang/package_config/actions?query=workflow%3A"Dart+CI"+branch%3Amaster)
+[](https://pub.dev/packages/package_config)
+[](https://pub.dev/packages/package_config/publisher)
+
+Support for working with **Package Configuration** files as described
+in the Package Configuration v2 [design document](https://github.com/dart-lang/language/blob/master/accepted/2.8/language-versioning/package-config-file-v2.md).
+
+A Dart package configuration file is used to resolve Dart package names (e.g.
+`foobar`) to Dart files containing the source code for that package (e.g.
+`file:///Users/myuser/.pub-cache/hosted/pub.dartlang.org/foobar-1.1.0`). The
+standard package configuration file is `.dart_tool/package_config.json`, and is
+written by the Dart tool when the command `dart pub get` is run.
+
+The primary libraries of this package are
+* `package_config.dart`:
+ Defines the `PackageConfig` class and other types needed to use
+ package configurations, and provides functions to find, read and
+ write package configuration files.
+
+* `package_config_types.dart`:
+ Just the `PackageConfig` class and other types needed to use
+ package configurations. This library does not depend on `dart:io`.
+
+The package includes deprecated backwards compatible functionality to
+work with the `.packages` file. This functionality will not be maintained,
+and will be removed in a future version of this package.
diff --git a/pkgs/package_config/analysis_options.yaml b/pkgs/package_config/analysis_options.yaml
new file mode 100644
index 0000000..c0249e5
--- /dev/null
+++ b/pkgs/package_config/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright (c) 2020, 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.
+
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/package_config/example/main.dart b/pkgs/package_config/example/main.dart
new file mode 100644
index 0000000..db137ca
--- /dev/null
+++ b/pkgs/package_config/example/main.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2020, 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:io' show Directory;
+
+import 'package:package_config/package_config.dart';
+
+void main() async {
+ var packageConfig = await findPackageConfig(Directory.current);
+ if (packageConfig == null) {
+ print('Failed to locate or read package config.');
+ } else {
+ print('This package depends on ${packageConfig.packages.length} packages:');
+ for (var package in packageConfig.packages) {
+ print('- ${package.name}');
+ }
+ }
+}
diff --git a/pkgs/package_config/lib/package_config.dart b/pkgs/package_config/lib/package_config.dart
new file mode 100644
index 0000000..074c977
--- /dev/null
+++ b/pkgs/package_config/lib/package_config.dart
@@ -0,0 +1,199 @@
+// 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.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa.
+///
+/// This package provides functionality to find, read and write package
+/// configurations in the [specified format](https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/package-config-file-v2.md).
+library;
+
+import 'dart:io' show Directory, File;
+import 'dart:typed_data' show Uint8List;
+
+import 'src/discovery.dart' as discover;
+import 'src/errors.dart' show throwError;
+import 'src/package_config.dart';
+import 'src/package_config_io.dart';
+
+export 'package_config_types.dart';
+
+/// Reads a specific package configuration file.
+///
+/// The file must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first character
+/// is a `{`.
+///
+/// If the file is a `.packages` file (the file name is `.packages`)
+/// and [preferNewest] is true, the default, also checks if there is
+/// a `.dart_tool/package_config.json` file next
+/// to the original file, and if so, loads that instead.
+/// If [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfig(File file,
+ {bool preferNewest = true, void Function(Object error)? onError}) =>
+ readAnyConfigFile(file, preferNewest, onError ?? throwError);
+
+/// Reads a specific package configuration URI.
+///
+/// The file of the URI must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first
+/// non-whitespace character is a `{`.
+///
+/// If [preferNewest] is true, the default, and the file is a `.packages` file,
+/// as determined by its file name being `.packages`,
+/// first checks if there is a `.dart_tool/package_config.json` file
+/// next to the original file, and if so, loads that instead.
+/// The [file] *must not* be a `package:` URI.
+/// If [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content encoded as UTF-8,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+/// As such, it may throw any error that [loader] throws.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfigUri(Uri file,
+ {Future<Uint8List?> Function(Uri uri)? loader,
+ bool preferNewest = true,
+ void Function(Object error)? onError}) =>
+ readAnyConfigFileUri(file, loader, onError ?? throwError, preferNewest);
+
+/// Finds a package configuration relative to [directory].
+///
+/// If [directory] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to `false`, this parent directory check is not
+/// performed.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfig(Directory directory,
+ {bool recurse = true,
+ void Function(Object error)? onError,
+ int minVersion = 1}) {
+ if (minVersion > PackageConfig.maxVersion) {
+ throw ArgumentError.value(minVersion, 'minVersion',
+ 'Maximum known version is ${PackageConfig.maxVersion}');
+ }
+ return discover.findPackageConfig(
+ directory, minVersion, recurse, onError ?? throwError);
+}
+
+/// Finds a package configuration relative to [location].
+///
+/// If [location] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+/// The [location] URI *must not* be a `package:` URI.
+/// It should be a hierarchical URI which is supported
+/// by [loader].
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to `false`, this parent directory check is not
+/// performed.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfigUri(Uri location,
+ {bool recurse = true,
+ int minVersion = 1,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error)? onError}) {
+ if (minVersion > PackageConfig.maxVersion) {
+ throw ArgumentError.value(minVersion, 'minVersion',
+ 'Maximum known version is ${PackageConfig.maxVersion}');
+ }
+ return discover.findPackageConfigUri(
+ location, minVersion, loader, onError ?? throwError, recurse);
+}
+
+/// Writes a package configuration to the provided directory.
+///
+/// Writes `.dart_tool/package_config.json` relative to [directory].
+/// If the `.dart_tool/` directory does not exist, it is created.
+/// If it cannot be created, this operation fails.
+///
+/// Also writes a `.packages` file in [directory].
+/// This will stop happening eventually as the `.packages` file becomes
+/// discontinued.
+/// A comment is generated if `[PackageConfig.extraData]` contains a
+/// `"generator"` entry.
+Future<void> savePackageConfig(
+ PackageConfig configuration, Directory directory) =>
+ writePackageConfigJsonFile(configuration, directory);
diff --git a/pkgs/package_config/lib/package_config_types.dart b/pkgs/package_config/lib/package_config_types.dart
new file mode 100644
index 0000000..825f7ac
--- /dev/null
+++ b/pkgs/package_config/lib/package_config_types.dart
@@ -0,0 +1,17 @@
+// 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.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa.
+///
+/// {@canonicalFor package_config.InvalidLanguageVersion}
+/// {@canonicalFor package_config.LanguageVersion}
+/// {@canonicalFor package_config.Package}
+/// {@canonicalFor package_config.PackageConfig}
+/// {@canonicalFor errors.PackageConfigError}
+library;
+
+export 'src/errors.dart' show PackageConfigError;
+export 'src/package_config.dart'
+ show InvalidLanguageVersion, LanguageVersion, Package, PackageConfig;
diff --git a/pkgs/package_config/lib/src/discovery.dart b/pkgs/package_config/lib/src/discovery.dart
new file mode 100644
index 0000000..b678410
--- /dev/null
+++ b/pkgs/package_config/lib/src/discovery.dart
@@ -0,0 +1,148 @@
+// 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:io';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_io.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util_io.dart' show defaultLoader, pathJoin;
+
+final Uri packageConfigJsonPath = Uri(path: '.dart_tool/package_config.json');
+final Uri dotPackagesPath = Uri(path: '.packages');
+final Uri currentPath = Uri(path: '.');
+final Uri parentPath = Uri(path: '..');
+
+/// Discover the package configuration for a Dart script.
+///
+/// The [baseDirectory] points to the directory of the Dart script.
+/// A package resolution strategy is found by going through the following steps,
+/// and stopping when something is found.
+///
+/// * Check if a `.dart_tool/package_config.json` file exists in the directory.
+/// * Check if a `.packages` file exists in the directory
+/// (if `minVersion <= 1`).
+/// * Repeat these checks for the parent directories until reaching the
+/// root directory if [recursive] is true.
+///
+/// If any of these tests succeed, a `PackageConfig` class is returned.
+/// Returns `null` if no configuration was found. If a configuration
+/// is needed, then the caller can supply [PackageConfig.empty].
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfig(Directory baseDirectory,
+ int minVersion, bool recursive, void Function(Object error) onError) async {
+ var directory = baseDirectory;
+ if (!directory.isAbsolute) directory = directory.absolute;
+ if (!await directory.exists()) {
+ return null;
+ }
+ do {
+ // Check for $cwd/.packages
+ var packageConfig =
+ await findPackageConfigInDirectory(directory, minVersion, onError);
+ if (packageConfig != null) return packageConfig;
+ if (!recursive) break;
+ // Check in parent directories.
+ var parentDirectory = directory.parent;
+ if (parentDirectory.path == directory.path) break;
+ directory = parentDirectory;
+ } while (true);
+ return null;
+}
+
+/// Similar to [findPackageConfig] but based on a URI.
+Future<PackageConfig?> findPackageConfigUri(
+ Uri location,
+ int minVersion,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error) onError,
+ bool recursive) async {
+ if (location.isScheme('package')) {
+ onError(PackageConfigArgumentError(
+ location, 'location', 'Must not be a package: URI'));
+ return null;
+ }
+ if (loader == null) {
+ if (location.isScheme('file')) {
+ return findPackageConfig(
+ Directory.fromUri(location.resolveUri(currentPath)),
+ minVersion,
+ recursive,
+ onError);
+ }
+ loader = defaultLoader;
+ }
+ if (!location.path.endsWith('/')) location = location.resolveUri(currentPath);
+ while (true) {
+ var file = location.resolveUri(packageConfigJsonPath);
+ var bytes = await loader(file);
+ if (bytes != null) {
+ var config = parsePackageConfigBytes(bytes, file, onError);
+ if (config.version >= minVersion) return config;
+ }
+ if (minVersion <= 1) {
+ file = location.resolveUri(dotPackagesPath);
+ bytes = await loader(file);
+ if (bytes != null) {
+ return packages_file.parse(bytes, file, onError);
+ }
+ }
+ if (!recursive) break;
+ var parent = location.resolveUri(parentPath);
+ if (parent == location) break;
+ location = parent;
+ }
+ return null;
+}
+
+/// Finds a `.packages` or `.dart_tool/package_config.json` file in [directory].
+///
+/// Loads the file, if it is there, and returns the resulting [PackageConfig].
+/// Returns `null` if the file isn't there.
+/// Reports a [FormatException] if a file is there but the content is not valid.
+/// If the file exists, but fails to be read, the file system error is reported.
+///
+/// If [onError] is supplied, parsing errors are reported using that, and
+/// a best-effort attempt is made to return a package configuration.
+/// This may be the empty package configuration.
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfigInDirectory(Directory directory,
+ int minVersion, void Function(Object error) onError) async {
+ var packageConfigFile = await checkForPackageConfigJsonFile(directory);
+ if (packageConfigFile != null) {
+ var config = await readPackageConfigJsonFile(packageConfigFile, onError);
+ if (config.version < minVersion) return null;
+ return config;
+ }
+ if (minVersion <= 1) {
+ packageConfigFile = await checkForDotPackagesFile(directory);
+ if (packageConfigFile != null) {
+ return await readDotPackagesFile(packageConfigFile, onError);
+ }
+ }
+ return null;
+}
+
+Future<File?> checkForPackageConfigJsonFile(Directory directory) async {
+ assert(directory.isAbsolute);
+ var file =
+ File(pathJoin(directory.path, '.dart_tool', 'package_config.json'));
+ if (await file.exists()) return file;
+ return null;
+}
+
+Future<File?> checkForDotPackagesFile(Directory directory) async {
+ var file = File(pathJoin(directory.path, '.packages'));
+ if (await file.exists()) return file;
+ return null;
+}
diff --git a/pkgs/package_config/lib/src/errors.dart b/pkgs/package_config/lib/src/errors.dart
new file mode 100644
index 0000000..a66fef7
--- /dev/null
+++ b/pkgs/package_config/lib/src/errors.dart
@@ -0,0 +1,34 @@
+// 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.
+
+/// General superclass of most errors and exceptions thrown by this package.
+///
+/// Only covers errors thrown while parsing package configuration files.
+/// Programming errors and I/O exceptions are not covered.
+abstract class PackageConfigError {
+ PackageConfigError._();
+}
+
+class PackageConfigArgumentError extends ArgumentError
+ implements PackageConfigError {
+ PackageConfigArgumentError(
+ Object? super.value, String super.name, String super.message)
+ : super.value();
+
+ PackageConfigArgumentError.from(ArgumentError error)
+ : super.value(error.invalidValue, error.name, error.message);
+}
+
+class PackageConfigFormatException extends FormatException
+ implements PackageConfigError {
+ PackageConfigFormatException(super.message, Object? super.source,
+ [super.offset]);
+
+ PackageConfigFormatException.from(FormatException exception)
+ : super(exception.message, exception.source, exception.offset);
+}
+
+/// The default `onError` handler.
+// ignore: only_throw_errors
+Never throwError(Object error) => throw error;
diff --git a/pkgs/package_config/lib/src/package_config.dart b/pkgs/package_config/lib/src/package_config.dart
new file mode 100644
index 0000000..155dfc5
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config.dart
@@ -0,0 +1,402 @@
+// 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 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.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.
+abstract class PackageConfig {
+ /// The largest configuration version currently recognized.
+ 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.
+ ///
+ /// Currently this is 1 or 2, where
+ /// * Version one is the `.packages` file format and
+ /// * 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.
+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.
+abstract class LanguageVersion implements Comparable<LanguageVersion> {
+ /// The maximal value allowed by [major] and [minor] values;
+ static const int maxValue = 0x7FFFFFFF;
+ factory LanguageVersion(int major, int minor) {
+ RangeError.checkValueInInterval(major, 0, maxValue, 'major');
+ RangeError.checkValueInInterval(minor, 0, maxValue, 'major');
+ return SimpleLanguageVersion(major, 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 considered equal if they have the
+ /// same major and minor version numbers.
+ ///
+ /// A language version is greater then 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.
+ @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 on an error.
+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();
+}
diff --git a/pkgs/package_config/lib/src/package_config_impl.dart b/pkgs/package_config/lib/src/package_config_impl.dart
new file mode 100644
index 0000000..865e99a
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_impl.dart
@@ -0,0 +1,568 @@
+// 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';
+
+const bool _disallowPackagesInsidePackageUriRoot = false;
+
+// 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.
+ ///
+ /// 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.
+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 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 _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());
diff --git a/pkgs/package_config/lib/src/package_config_io.dart b/pkgs/package_config/lib/src/package_config_io.dart
new file mode 100644
index 0000000..8c5773b
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_io.dart
@@ -0,0 +1,166 @@
+// 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.
+
+// dart:io dependent functionality for reading and writing configuration files.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util.dart';
+import 'util_io.dart';
+
+/// Name of directory where Dart tools store their configuration.
+///
+/// Directory is created in the package root directory.
+const dartToolDirName = '.dart_tool';
+
+/// Name of file containing new package configuration data.
+///
+/// File is stored in the dart tool directory.
+const packageConfigFileName = 'package_config.json';
+
+/// Name of file containing legacy package configuration data.
+///
+/// File is stored in the package root directory.
+const packagesFileName = '.packages';
+
+/// Reads a package configuration file.
+///
+/// Detects whether the [file] is a version one `.packages` file or
+/// a version two `package_config.json` file.
+///
+/// If the [file] is a `.packages` file and [preferNewest] is true,
+/// first checks whether there is an adjacent `.dart_tool/package_config.json`
+/// file, and if so, reads that instead.
+/// If [preferNewest] is false, the specified file is loaded even if it is
+/// a `.packages` file and there is an available `package_config.json` file.
+///
+/// The file must exist and be a normal file.
+Future<PackageConfig> readAnyConfigFile(
+ File file, bool preferNewest, void Function(Object error) onError) async {
+ if (preferNewest && fileName(file.path) == packagesFileName) {
+ var alternateFile = File(
+ pathJoin(dirName(file.path), dartToolDirName, packageConfigFileName));
+ if (alternateFile.existsSync()) {
+ return await readPackageConfigJsonFile(alternateFile, onError);
+ }
+ }
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ return parseAnyConfigFile(bytes, file.uri, onError);
+}
+
+/// Like [readAnyConfigFile] but uses a URI and an optional loader.
+Future<PackageConfig> readAnyConfigFileUri(
+ Uri file,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error) onError,
+ bool preferNewest) async {
+ if (file.isScheme('package')) {
+ throw PackageConfigArgumentError(
+ file, 'file', 'Must not be a package: URI');
+ }
+ if (loader == null) {
+ if (file.isScheme('file')) {
+ return await readAnyConfigFile(File.fromUri(file), preferNewest, onError);
+ }
+ loader = defaultLoader;
+ }
+ if (preferNewest && file.pathSegments.last == packagesFileName) {
+ var alternateFile = file.resolve('$dartToolDirName/$packageConfigFileName');
+ Uint8List? bytes;
+ try {
+ bytes = await loader(alternateFile);
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ if (bytes != null) {
+ return parsePackageConfigBytes(bytes, alternateFile, onError);
+ }
+ }
+ Uint8List? bytes;
+ try {
+ bytes = await loader(file);
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ if (bytes == null) {
+ onError(PackageConfigArgumentError(
+ file.toString(), 'file', 'File cannot be read'));
+ return const SimplePackageConfig.empty();
+ }
+ return parseAnyConfigFile(bytes, file, onError);
+}
+
+/// Parses a `.packages` or `package_config.json` file's contents.
+///
+/// Assumes it's a JSON file if the first non-whitespace character
+/// is `{`, otherwise assumes it's a `.packages` file.
+PackageConfig parseAnyConfigFile(
+ Uint8List bytes, Uri file, void Function(Object error) onError) {
+ var firstChar = firstNonWhitespaceChar(bytes);
+ if (firstChar != $lbrace) {
+ // Definitely not a JSON object, probably a .packages.
+ return packages_file.parse(bytes, file, onError);
+ }
+ return parsePackageConfigBytes(bytes, file, onError);
+}
+
+Future<PackageConfig> readPackageConfigJsonFile(
+ File file, void Function(Object error) onError) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (error) {
+ onError(error);
+ return const SimplePackageConfig.empty();
+ }
+ return parsePackageConfigBytes(bytes, file.uri, onError);
+}
+
+Future<PackageConfig> readDotPackagesFile(
+ File file, void Function(Object error) onError) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (error) {
+ onError(error);
+ return const SimplePackageConfig.empty();
+ }
+ return packages_file.parse(bytes, file.uri, onError);
+}
+
+Future<void> writePackageConfigJsonFile(
+ PackageConfig config, Directory targetDirectory) async {
+ // Write .dart_tool/package_config.json first.
+ var dartToolDir = Directory(pathJoin(targetDirectory.path, dartToolDirName));
+ await dartToolDir.create(recursive: true);
+ var file = File(pathJoin(dartToolDir.path, packageConfigFileName));
+ var baseUri = file.uri;
+
+ var sink = file.openWrite(encoding: utf8);
+ writePackageConfigJsonUtf8(config, baseUri, sink);
+ var doneJson = sink.close();
+
+ // Write .packages too.
+ file = File(pathJoin(targetDirectory.path, packagesFileName));
+ baseUri = file.uri;
+ sink = file.openWrite(encoding: utf8);
+ writeDotPackages(config, baseUri, sink);
+ var donePackages = sink.close();
+
+ await Future.wait([doneJson, donePackages]);
+}
diff --git a/pkgs/package_config/lib/src/package_config_json.dart b/pkgs/package_config/lib/src/package_config_json.dart
new file mode 100644
index 0000000..65560a0
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_json.dart
@@ -0,0 +1,321 @@
+// 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 Function(Object error) onError) {
+ // TODO(lrn): Make this simpler. Maybe parse directly from bytes.
+ Object? 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 Function(Object error) onError) {
+ Object? 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, Object?>`)
+/// 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<Object?>`)
+/// 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.
+///
+/// The [baseLocation] is used as base URI to resolve the "rootUri"
+/// URI reference string.
+PackageConfig parsePackageConfigJson(
+ Object? json, Uri baseLocation, void Function(Object error) onError) {
+ 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 <Object?>[] is T) return 'array';
+ return 'object';
+ }
+
+ T? checkType<T>(Object? value, String name, [String? packageName]) {
+ if (value is T) return value;
+ // The only types we are called with are [int], [String], [List<Object?>]
+ // and Map<String, Object?>. 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, Object?> entry) {
+ String? name;
+ String? rootUri;
+ String? packageUri;
+ String? languageVersion;
+ Map<String, Object?>? 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 parsedRootUri = Uri.parse(rootUri!);
+ var relativeRoot = !hasAbsolutePath(parsedRootUri);
+ var root = baseLocation.resolveUri(parsedRootUri);
+ 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, relativeRoot, (error) {
+ if (error is ArgumentError) {
+ onError(
+ PackageConfigFormatException(
+ error.message.toString(), error.invalidValue),
+ );
+ } else {
+ onError(error);
+ }
+ });
+ }
+
+ var map = checkType<Map<String, Object?>>(json, 'value');
+ if (map == null) return const SimplePackageConfig.empty();
+ Map<String, Object?>? 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<Object?>>(value, _packagesKey) ?? [];
+ var packages = <Package>[];
+ for (var package in packageArray) {
+ var packageMap =
+ checkType<Map<String, Object?>>(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.toString(), 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(const JsonEncoder.withIndent(' ').convert(data));
+}
+
+Map<String, Object?> packageConfigToJson(PackageConfig config, Uri? baseUri) =>
+ <String, Object?>{
+ ...?_extractExtraData(config.extraData, _topNames),
+ _configVersionKey: PackageConfig.maxVersion,
+ _packagesKey: [
+ for (var package in config.packages)
+ <String, Object?>{
+ _nameKey: package.name,
+ _rootUriKey: trailingSlash((package.relativeRoot
+ ? relativizeUri(package.root, baseUri)
+ : package.root)
+ .toString()),
+ if (package.root != package.packageUriRoot)
+ _packageUriKey: trailingSlash(
+ 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 is Map<String, Object?>) {
+ var generator = extraData[_generatorKey];
+ if (generator is String) {
+ var generated = extraData[_generatedKey];
+ var generatorVersion = extraData[_generatorVersionKey];
+ comment = 'Generated by $generator'
+ "${generatorVersion is String ? " $generatorVersion" : ""}"
+ "${generated is String ? " 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, Object?>? _extractExtraData(
+ Object? data, Iterable<String> reservedNames) {
+ if (data is Map<String, Object?>) {
+ if (data.isEmpty) return null;
+ for (var name in reservedNames) {
+ if (data.containsKey(name)) {
+ var filteredData = {
+ for (var key in data.keys)
+ if (!reservedNames.contains(key)) key: data[key]
+ };
+ if (filteredData.isEmpty) return null;
+ for (var value in filteredData.values) {
+ if (!_validateJson(value)) return null;
+ }
+ return filteredData;
+ }
+ }
+ return data;
+ }
+ return null;
+}
+
+/// Checks that the object is a valid JSON-like data structure.
+bool _validateJson(Object? object) {
+ if (object == null || true == object || false == object) return true;
+ if (object is num || object is String) return true;
+ if (object is List<Object?>) {
+ return object.every(_validateJson);
+ }
+ if (object is Map<String, Object?>) {
+ return object.values.every(_validateJson);
+ }
+ return false;
+}
diff --git a/pkgs/package_config/lib/src/packages_file.dart b/pkgs/package_config/lib/src/packages_file.dart
new file mode 100644
index 0000000..bf68f2c
--- /dev/null
+++ b/pkgs/package_config/lib/src/packages_file.dart
@@ -0,0 +1,193 @@
+// 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_impl.dart';
+import 'util.dart';
+
+/// The language version prior to the release of language versioning.
+///
+/// This is the default language version used by all packages from a
+/// `.packages` file.
+final LanguageVersion _languageVersion = LanguageVersion(2, 7);
+
+/// Parses a `.packages` file into a [PackageConfig].
+///
+/// The [source] is the byte content of a `.packages` file, assumed to be
+/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
+/// so Latin-1 or Windows-1252 encoding will also work fine.
+///
+/// If the file content is available as a string, its [String.codeUnits] can
+/// be used as the `source` argument of this function.
+///
+/// The [baseLocation] is used as a base URI to resolve all relative
+/// URI references against.
+/// If the content was read from a file, `baseLocation` should be the
+/// location of that file.
+///
+/// Returns a simple package configuration where each package's
+/// [Package.packageUriRoot] is the same as its [Package.root]
+/// and it has no [Package.languageVersion].
+PackageConfig parse(
+ List<int> source, Uri baseLocation, void Function(Object error) onError) {
+ if (baseLocation.isScheme('package')) {
+ onError(PackageConfigArgumentError(
+ baseLocation, 'baseLocation', 'Must not be a package: URI'));
+ return PackageConfig.empty;
+ }
+ var index = 0;
+ var packages = <Package>[];
+ var packageNames = <String>{};
+ while (index < source.length) {
+ var ignoreLine = false;
+ var start = index;
+ var separatorIndex = -1;
+ var end = source.length;
+ var char = source[index++];
+ if (char == $cr || char == $lf) {
+ continue;
+ }
+ if (char == $colon) {
+ onError(PackageConfigFormatException(
+ 'Missing package name', source, index - 1));
+ ignoreLine = true; // Ignore if package name is invalid.
+ } else {
+ ignoreLine = char == $hash; // Ignore if comment.
+ }
+ var queryStart = -1;
+ var fragmentStart = -1;
+ while (index < source.length) {
+ char = source[index++];
+ if (char == $colon && separatorIndex < 0) {
+ separatorIndex = index - 1;
+ } else if (char == $cr || char == $lf) {
+ end = index - 1;
+ break;
+ } else if (char == $question && queryStart < 0 && fragmentStart < 0) {
+ queryStart = index - 1;
+ } else if (char == $hash && fragmentStart < 0) {
+ fragmentStart = index - 1;
+ }
+ }
+ if (ignoreLine) continue;
+ if (separatorIndex < 0) {
+ onError(
+ PackageConfigFormatException("No ':' on line", source, index - 1));
+ continue;
+ }
+ var packageName = String.fromCharCodes(source, start, separatorIndex);
+ var invalidIndex = checkPackageName(packageName);
+ if (invalidIndex >= 0) {
+ onError(PackageConfigFormatException(
+ 'Not a valid package name', source, start + invalidIndex));
+ continue;
+ }
+ if (queryStart >= 0) {
+ onError(PackageConfigFormatException(
+ 'Location URI must not have query', source, queryStart));
+ end = queryStart;
+ } else if (fragmentStart >= 0) {
+ onError(PackageConfigFormatException(
+ 'Location URI must not have fragment', source, fragmentStart));
+ end = fragmentStart;
+ }
+ var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
+ Uri packageLocation;
+ try {
+ packageLocation = Uri.parse(packageValue);
+ } on FormatException catch (e) {
+ onError(PackageConfigFormatException.from(e));
+ continue;
+ }
+ var relativeRoot = !hasAbsolutePath(packageLocation);
+ packageLocation = baseLocation.resolveUri(packageLocation);
+ if (packageLocation.isScheme('package')) {
+ onError(PackageConfigFormatException(
+ 'Package URI as location for package', source, separatorIndex + 1));
+ continue;
+ }
+ var path = packageLocation.path;
+ if (!path.endsWith('/')) {
+ path += '/';
+ packageLocation = packageLocation.replace(path: path);
+ }
+ if (packageNames.contains(packageName)) {
+ onError(PackageConfigFormatException(
+ 'Same package name occurred more than once', source, start));
+ continue;
+ }
+ var rootUri = packageLocation;
+ if (path.endsWith('/lib/')) {
+ // Assume default Pub package layout. Include package itself in root.
+ rootUri =
+ packageLocation.replace(path: path.substring(0, path.length - 4));
+ }
+ var package = SimplePackage.validate(packageName, rootUri, packageLocation,
+ _languageVersion, null, relativeRoot, (error) {
+ if (error is ArgumentError) {
+ onError(PackageConfigFormatException(error.message.toString(), source));
+ } else {
+ onError(error);
+ }
+ });
+ if (package != null) {
+ packages.add(package);
+ packageNames.add(packageName);
+ }
+ }
+ return SimplePackageConfig(1, packages, null, onError);
+}
+
+/// Writes the configuration to a [StringSink].
+///
+/// If [comment] is provided, the output will contain this comment
+/// with `# ` in front of each line.
+/// Lines are defined as ending in line feed (`'\n'`). If the final
+/// line of the comment doesn't end in a line feed, one will be added.
+///
+/// If [baseUri] is provided, package locations will be made relative
+/// to the base URI, if possible, before writing.
+void write(StringSink output, PackageConfig config,
+ {Uri? baseUri, String? comment}) {
+ if (baseUri != null && !baseUri.isAbsolute) {
+ throw PackageConfigArgumentError(baseUri, 'baseUri', 'Must be absolute');
+ }
+
+ if (comment != null) {
+ var lines = comment.split('\n');
+ if (lines.last.isEmpty) lines.removeLast();
+ for (var commentLine in lines) {
+ output.write('# ');
+ output.writeln(commentLine);
+ }
+ } else {
+ output.write('# generated by package:package_config at ');
+ output.write(DateTime.now());
+ output.writeln();
+ }
+ for (var package in config.packages) {
+ var packageName = package.name;
+ var uri = package.packageUriRoot;
+ // Validate packageName.
+ if (!isValidPackageName(packageName)) {
+ throw PackageConfigArgumentError(
+ config, 'config', '"$packageName" is not a valid package name');
+ }
+ if (uri.scheme == 'package') {
+ throw PackageConfigArgumentError(
+ config, 'config', 'Package location must not be a package URI: $uri');
+ }
+ output.write(packageName);
+ output.write(':');
+ // If baseUri is provided, make the URI relative to baseUri.
+ if (baseUri != null) {
+ uri = relativizeUri(uri, baseUri)!;
+ }
+ if (!uri.path.endsWith('/')) {
+ uri = uri.replace(path: '${uri.path}/');
+ }
+ output.write(uri);
+ output.writeln();
+ }
+}
diff --git a/pkgs/package_config/lib/src/util.dart b/pkgs/package_config/lib/src/util.dart
new file mode 100644
index 0000000..4f0210c
--- /dev/null
+++ b/pkgs/package_config/lib/src/util.dart
@@ -0,0 +1,253 @@
+// 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.
+
+/// Utility methods used by more than one library in the package.
+library;
+
+import 'errors.dart';
+
+// All ASCII characters that are valid in a package name, with space
+// for all the invalid ones (including space).
+const String _validPackageNameCharacters =
+ r" ! $ &'()*+,-. 0123456789 ; = "
+ r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ _ abcdefghijklmnopqrstuvwxyz ~ ';
+
+/// Tests whether something is a valid Dart package name.
+bool isValidPackageName(String string) {
+ return checkPackageName(string) < 0;
+}
+
+/// Check if a string is a valid package name.
+///
+/// Valid package names contain only characters in [_validPackageNameCharacters]
+/// and must contain at least one non-'.' character.
+///
+/// Returns `-1` if the string is valid.
+/// Otherwise returns the index of the first invalid character,
+/// or `string.length` if the string contains no non-'.' character.
+int checkPackageName(String string) {
+ // Becomes non-zero if any non-'.' character is encountered.
+ var nonDot = 0;
+ for (var i = 0; i < string.length; i++) {
+ var c = string.codeUnitAt(i);
+ if (c > 0x7f || _validPackageNameCharacters.codeUnitAt(c) <= $space) {
+ return i;
+ }
+ nonDot += c ^ $dot;
+ }
+ if (nonDot == 0) return string.length;
+ return -1;
+}
+
+/// Validate that a [Uri] is a valid `package:` URI.
+///
+/// Used to validate user input.
+///
+/// Returns the package name extracted from the package URI,
+/// which is the path segment between `package:` and the first `/`.
+String checkValidPackageUri(Uri packageUri, String name) {
+ if (packageUri.scheme != 'package') {
+ throw PackageConfigArgumentError(packageUri, name, 'Not a package: URI');
+ }
+ if (packageUri.hasAuthority) {
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a host part');
+ }
+ if (packageUri.hasQuery) {
+ // A query makes no sense if resolved to a file: URI.
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a query part');
+ }
+ if (packageUri.hasFragment) {
+ // We could leave the fragment after the URL when resolving,
+ // but it would be odd if "package:foo/foo.dart#1" and
+ // "package:foo/foo.dart#2" were considered different libraries.
+ // Keep the syntax open in case we ever get multiple libraries in one file.
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a fragment part');
+ }
+ if (packageUri.path.startsWith('/')) {
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not start with a '/'");
+ }
+ var firstSlash = packageUri.path.indexOf('/');
+ if (firstSlash == -1) {
+ throw PackageConfigArgumentError(packageUri, name,
+ "Package URIs must start with the package name followed by a '/'");
+ }
+ var packageName = packageUri.path.substring(0, firstSlash);
+ var badIndex = checkPackageName(packageName);
+ if (badIndex >= 0) {
+ if (packageName.isEmpty) {
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package names mus be non-empty');
+ }
+ if (badIndex == packageName.length) {
+ throw PackageConfigArgumentError(packageUri, name,
+ "Package names must contain at least one non-'.' character");
+ }
+ assert(badIndex < packageName.length);
+ var badCharCode = packageName.codeUnitAt(badIndex);
+ var badChar = 'U+${badCharCode.toRadixString(16).padLeft(4, '0')}';
+ if (badCharCode >= 0x20 && badCharCode <= 0x7e) {
+ // Printable character.
+ badChar = "'${packageName[badIndex]}' ($badChar)";
+ }
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package names must not contain $badChar');
+ }
+ return packageName;
+}
+
+/// Checks whether URI is just an absolute directory.
+///
+/// * It must have a scheme.
+/// * It must not have a query or fragment.
+/// * The path must end with `/`.
+bool isAbsoluteDirectoryUri(Uri uri) {
+ if (uri.hasQuery) return false;
+ if (uri.hasFragment) return false;
+ if (!uri.hasScheme) return false;
+ var path = uri.path;
+ if (!path.endsWith('/')) return false;
+ return true;
+}
+
+/// Whether the former URI is a prefix of the latter.
+bool isUriPrefix(Uri prefix, Uri path) {
+ assert(!prefix.hasFragment);
+ assert(!prefix.hasQuery);
+ assert(!path.hasQuery);
+ assert(!path.hasFragment);
+ assert(prefix.path.endsWith('/'));
+ return path.toString().startsWith(prefix.toString());
+}
+
+/// Finds the first non-JSON-whitespace character in a file.
+///
+/// Used to heuristically detect whether a file is a JSON file or an .ini file.
+int firstNonWhitespaceChar(List<int> bytes) {
+ for (var i = 0; i < bytes.length; i++) {
+ var char = bytes[i];
+ if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
+ return char;
+ }
+ }
+ return -1;
+}
+
+/// Appends a trailing `/` if the path doesn't end with one.
+String trailingSlash(String path) {
+ if (path.isEmpty || path.endsWith('/')) return path;
+ return '$path/';
+}
+
+/// Whether a URI should not be considered relative to the base URI.
+///
+/// Used to determine whether a parsed root URI is relative
+/// to the configuration file or not.
+/// If it is relative, then it's rewritten as relative when
+/// output again later. If not, it's output as absolute.
+bool hasAbsolutePath(Uri uri) =>
+ uri.hasScheme || uri.hasAuthority || uri.hasAbsolutePath;
+
+/// Attempts to return a relative path-only URI for [uri].
+///
+/// First removes any query or fragment part from [uri].
+///
+/// If [uri] is already relative (has no scheme), it's returned as-is.
+/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
+/// as the [uri] instead.
+///
+/// If the [uri] has a scheme or authority part which differs from
+/// the [baseUri], or if there is no overlap in the paths of the
+/// two URIs at all, the [uri] is returned as-is.
+///
+/// Otherwise the result is a path-only URI which satisfies
+/// `baseUri.resolveUri(result) == uri`,
+///
+/// The `baseUri` must be absolute.
+Uri? relativizeUri(Uri? uri, Uri? baseUri) {
+ if (baseUri == null) return uri;
+ assert(baseUri.isAbsolute);
+ if (uri!.hasQuery || uri.hasFragment) {
+ uri = Uri(
+ scheme: uri.scheme,
+ userInfo: uri.hasAuthority ? uri.userInfo : null,
+ host: uri.hasAuthority ? uri.host : null,
+ port: uri.hasAuthority ? uri.port : null,
+ path: uri.path);
+ }
+
+ // Already relative. We assume the caller knows what they are doing.
+ if (!uri.isAbsolute) return uri;
+
+ if (baseUri.scheme != uri.scheme) {
+ return uri;
+ }
+
+ // If authority differs, we could remove the scheme, but it's not worth it.
+ if (uri.hasAuthority != baseUri.hasAuthority) return uri;
+ if (uri.hasAuthority) {
+ if (uri.userInfo != baseUri.userInfo ||
+ uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
+ uri.port != baseUri.port) {
+ return uri;
+ }
+ }
+
+ baseUri = baseUri.normalizePath();
+ var base = [...baseUri.pathSegments];
+ if (base.isNotEmpty) base.removeLast();
+ uri = uri.normalizePath();
+ var target = [...uri.pathSegments];
+ if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
+ var index = 0;
+ while (index < base.length && index < target.length) {
+ if (base[index] != target[index]) {
+ break;
+ }
+ index++;
+ }
+ if (index == base.length) {
+ if (index == target.length) {
+ return Uri(path: './');
+ }
+ return Uri(path: target.skip(index).join('/'));
+ } else if (index > 0) {
+ var buffer = StringBuffer();
+ for (var n = base.length - index; n > 0; --n) {
+ buffer.write('../');
+ }
+ buffer.writeAll(target.skip(index), '/');
+ return Uri(path: buffer.toString());
+ } else {
+ return uri;
+ }
+}
+
+// Character constants used by this package.
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// "Carriage return" control character.
+const int $cr = 0x0d;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `.`.
+const int $dot = 0x2e;
+
+/// Character `:`.
+const int $colon = 0x3a;
+
+/// Character `?`.
+const int $question = 0x3f;
+
+/// Character `{`.
+const int $lbrace = 0x7b;
diff --git a/pkgs/package_config/lib/src/util_io.dart b/pkgs/package_config/lib/src/util_io.dart
new file mode 100644
index 0000000..4680eef
--- /dev/null
+++ b/pkgs/package_config/lib/src/util_io.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2020, 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.
+
+/// Utility methods requiring dart:io and used by more than one library in the
+/// package.
+library;
+
+import 'dart:io';
+import 'dart:typed_data';
+
+Future<Uint8List?> defaultLoader(Uri uri) async {
+ if (uri.isScheme('file')) {
+ var file = File.fromUri(uri);
+ try {
+ return await file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+ }
+ if (uri.isScheme('http') || uri.isScheme('https')) {
+ return _httpGet(uri);
+ }
+ throw UnsupportedError('Default URI unsupported scheme: $uri');
+}
+
+Future<Uint8List?> _httpGet(Uri uri) async {
+ assert(uri.isScheme('http') || uri.isScheme('https'));
+ var client = HttpClient();
+ var request = await client.getUrl(uri);
+ var response = await request.close();
+ if (response.statusCode != HttpStatus.ok) {
+ return null;
+ }
+ var splitContent = await response.toList();
+ var totalLength = 0;
+ if (splitContent.length == 1) {
+ var part = splitContent[0];
+ if (part is Uint8List) {
+ return part;
+ }
+ }
+ for (var list in splitContent) {
+ totalLength += list.length;
+ }
+ var result = Uint8List(totalLength);
+ var offset = 0;
+ for (var contentPart in splitContent as Iterable<Uint8List>) {
+ result.setRange(offset, offset + contentPart.length, contentPart);
+ offset += contentPart.length;
+ }
+ return result;
+}
+
+/// The file name of a path.
+///
+/// The file name is everything after the last occurrence of
+/// [Platform.pathSeparator], or the entire string if no
+/// path separator occurs in the string.
+String fileName(String path) {
+ var separator = Platform.pathSeparator;
+ var lastSeparator = path.lastIndexOf(separator);
+ if (lastSeparator < 0) return path;
+ return path.substring(lastSeparator + separator.length);
+}
+
+/// The directory name of a path.
+///
+/// The directory name is everything before the last occurrence of
+/// [Platform.pathSeparator], or the empty string if no
+/// path separator occurs in the string.
+String dirName(String path) {
+ var separator = Platform.pathSeparator;
+ var lastSeparator = path.lastIndexOf(separator);
+ if (lastSeparator < 0) return '';
+ return path.substring(0, lastSeparator);
+}
+
+/// Join path parts with the [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoin(String part1, String part2, [String? part3]) {
+ var separator = Platform.pathSeparator;
+ var separator1 = part1.endsWith(separator) ? '' : separator;
+ if (part3 == null) {
+ return '$part1$separator1$part2';
+ }
+ var separator2 = part2.endsWith(separator) ? '' : separator;
+ return '$part1$separator1$part2$separator2$part3';
+}
+
+/// Join an unknown number of path parts with [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoinAll(Iterable<String> parts) {
+ var buffer = StringBuffer();
+ var separator = '';
+ for (var part in parts) {
+ buffer
+ ..write(separator)
+ ..write(part);
+ separator =
+ part.endsWith(Platform.pathSeparator) ? '' : Platform.pathSeparator;
+ }
+ return buffer.toString();
+}
diff --git a/pkgs/package_config/pubspec.yaml b/pkgs/package_config/pubspec.yaml
new file mode 100644
index 0000000..545701d
--- /dev/null
+++ b/pkgs/package_config/pubspec.yaml
@@ -0,0 +1,14 @@
+name: package_config
+version: 2.1.1-wip
+description: Support for reading and writing Dart Package Configuration files.
+repository: https://github.com/dart-lang/package_config
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ path: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.0
diff --git a/pkgs/package_config/test/bench.dart b/pkgs/package_config/test/bench.dart
new file mode 100644
index 0000000..8428481
--- /dev/null
+++ b/pkgs/package_config/test/bench.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, 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 'dart:typed_data';
+
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+
+void bench(final int size, final bool doPrint) {
+ var sb = StringBuffer();
+ sb.writeln('{');
+ sb.writeln('"configVersion": 2,');
+ sb.writeln('"packages": [');
+ for (var i = 0; i < size; i++) {
+ if (i != 0) {
+ sb.writeln(',');
+ }
+ sb.writeln('{');
+ sb.writeln(' "name": "p_$i",');
+ sb.writeln(' "rootUri": "file:///p_$i/",');
+ sb.writeln(' "packageUri": "lib/",');
+ sb.writeln(' "languageVersion": "2.5",');
+ sb.writeln(' "nonstandard": true');
+ sb.writeln('}');
+ }
+ sb.writeln('],');
+ sb.writeln('"generator": "pub",');
+ sb.writeln('"other": [42]');
+ sb.writeln('}');
+ var stopwatch = Stopwatch()..start();
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(sb.toString()) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError,
+ );
+ final read = stopwatch.elapsedMilliseconds;
+
+ stopwatch.reset();
+ for (var i = 0; i < size; i++) {
+ if (config.packageOf(Uri.parse('file:///p_$i/lib/src/foo.dart'))!.name !=
+ 'p_$i') {
+ throw StateError('Unexpected result!');
+ }
+ }
+ final lookup = stopwatch.elapsedMilliseconds;
+
+ if (doPrint) {
+ print('Read file with $size packages in $read ms, '
+ 'looked up all packages in $lookup ms');
+ }
+}
+
+void main(List<String> args) {
+ if (args.length != 1 && args.length != 2) {
+ throw ArgumentError('Expects arguments: <size> <warmup iterations>?');
+ }
+ final size = int.parse(args[0]);
+ if (args.length > 1) {
+ final warmups = int.parse(args[1]);
+ print('Performing $warmups warmup iterations.');
+ for (var i = 0; i < warmups; i++) {
+ bench(10, false);
+ }
+ }
+
+ // Benchmark.
+ bench(size, true);
+}
diff --git a/pkgs/package_config/test/discovery_test.dart b/pkgs/package_config/test/discovery_test.dart
new file mode 100644
index 0000000..6d1b655
--- /dev/null
+++ b/pkgs/package_config/test/discovery_test.dart
@@ -0,0 +1,346 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+import 'src/util_io.dart';
+
+const packagesFile = '''
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+''';
+
+const packageConfigFile = '''
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+''';
+
+void validatePackagesFile(PackageConfig resolver, Directory directory) {
+ expect(resolver, isNotNull);
+ expect(resolver.resolve(pkg('foo', 'bar/baz')),
+ equals(Uri.parse('file:///dart/packages/foo/bar/baz')));
+ expect(resolver.resolve(pkg('bar', 'baz/qux')),
+ equals(Uri.parse('file:///dart/packages/bar/baz/qux')));
+ expect(resolver.resolve(pkg('baz', 'qux/foo')),
+ equals(Uri.directory(directory.path).resolve('packages/baz/qux/foo')));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(['foo', 'bar', 'baz']));
+}
+
+void main() {
+ group('findPackages', () {
+ // Finds package_config.json if there.
+ fileTest('package_config.json', {
+ '.packages': 'invalid .packages file',
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <Never, Never>{}},
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(directory))!;
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages if no package_config.json.
+ fileTest('.packages', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <Object, Object>{}}
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(directory))!;
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ fileTest('package_config.json recursive', {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ 'script.dart': 'main(){}',
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/')))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ fileTest('.packages recursive', {
+ '.packages': packagesFile,
+ 'subdir': {'script.dart': 'main(){}'}
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/')))!;
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ fileTest('package directory packages not supported', {
+ 'packages': {
+ 'foo': <String, dynamic>{},
+ }
+ }, (Directory directory) async {
+ var config = await findPackageConfig(directory);
+ expect(config, null);
+ });
+
+ group('throws', () {
+ fileTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+ });
+
+ group('handles error', () {
+ fileTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid package_config not JSON', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid package config as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+ });
+
+ // Does not find .packages if no package_config.json and minVersion > 1.
+ fileTest('.packages ignored', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}'
+ }, (Directory directory) async {
+ var config = await findPackageConfig(directory, minVersion: 2);
+ expect(config, null);
+ });
+
+ // Finds package_config.json in super-directory, with .packages in
+ // subdir and minVersion > 1.
+ fileTest('package_config.json recursive .packages ignored', {
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/'),
+ minVersion: 2))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ group('loadPackageConfig', () {
+ // Load a specific files
+ group('package_config.json', () {
+ var files = {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ };
+ fileTest('directly', files, (Directory directory) async {
+ var file =
+ dirFile(subdir(directory, '.dart_tool'), 'package_config.json');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ fileTest('indirectly through .packages', files,
+ (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ fileTest('prefer .packages', files, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file, preferNewest: false);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ fileTest('package_config.json non-default name', {
+ '.packages': packagesFile,
+ 'subdir': {
+ 'pheldagriff': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'subdir/pheldagriff');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('package_config.json named .packages', {
+ 'subdir': {
+ '.packages': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'subdir/.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('.packages', {
+ '.packages': packagesFile,
+ }, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('.packages non-default name', {
+ 'pheldagriff': packagesFile,
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'pheldagriff');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('no config found', {}, (Directory directory) {
+ var file = dirFile(directory, 'anyname');
+ expect(
+ () => loadPackageConfig(file), throwsA(isA<FileSystemException>()));
+ });
+
+ fileTest('no config found, handled', {}, (Directory directory) async {
+ var file = dirFile(directory, 'anyname');
+ var hadError = false;
+ await loadPackageConfig(file,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FileSystemException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('specified file syntax error', {
+ 'anyname': 'syntax error',
+ }, (Directory directory) {
+ var file = dirFile(directory, 'anyname');
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
+
+ // Find package_config.json in subdir even if initial file syntax error.
+ fileTest('specified file syntax onError', {
+ '.packages': 'syntax error',
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ fileTest('file syntax error with {', {
+ '.packages': '{syntax error',
+ }, (Directory directory) {
+ var file = dirFile(directory, '.packages');
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
+ });
+}
diff --git a/pkgs/package_config/test/discovery_uri_test.dart b/pkgs/package_config/test/discovery_uri_test.dart
new file mode 100644
index 0000000..542bf0a
--- /dev/null
+++ b/pkgs/package_config/test/discovery_uri_test.dart
@@ -0,0 +1,310 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+
+const packagesFile = '''
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+''';
+
+const packageConfigFile = '''
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+''';
+
+void validatePackagesFile(PackageConfig resolver, Uri directory) {
+ expect(resolver, isNotNull);
+ expect(resolver.resolve(pkg('foo', 'bar/baz')),
+ equals(Uri.parse('file:///dart/packages/foo/bar/baz')));
+ expect(resolver.resolve(pkg('bar', 'baz/qux')),
+ equals(directory.resolve('/dart/packages/bar/baz/qux')));
+ expect(resolver.resolve(pkg('baz', 'qux/foo')),
+ equals(directory.resolve('packages/baz/qux/foo')));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(['foo', 'bar', 'baz']));
+}
+
+void main() {
+ group('findPackages', () {
+ // Finds package_config.json if there.
+ loaderTest('package_config.json', {
+ '.packages': 'invalid .packages file',
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <String, dynamic>{}},
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory, loader: loader))!;
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages if no package_config.json.
+ loaderTest('.packages', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <String, dynamic>{}}
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory, loader: loader))!;
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ loaderTest('package_config.json recursive', {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ 'script.dart': 'main(){}',
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ loader: loader))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ loaderTest('.packages recursive', {
+ '.packages': packagesFile,
+ 'subdir': {'script.dart': 'main(){}'}
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ loader: loader))!;
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ loaderTest('package directory packages not supported', {
+ 'packages': {
+ 'foo': <String, dynamic>{},
+ }
+ }, (Uri directory, loader) async {
+ var config = await findPackageConfigUri(directory, loader: loader);
+ expect(config, null);
+ });
+
+ loaderTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ // Does not find .packages if no package_config.json and minVersion > 1.
+ loaderTest('.packages ignored', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}'
+ }, (directory, loader) async {
+ var config =
+ await findPackageConfigUri(directory, minVersion: 2, loader: loader);
+ expect(config, null);
+ });
+
+ // Finds package_config.json in super-directory, with .packages in
+ // subdir and minVersion > 1.
+ loaderTest('package_config.json recursive ignores .packages', {
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ minVersion: 2, loader: loader))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ group('loadPackageConfig', () {
+ // Load a specific files
+ group('package_config.json', () {
+ var files = {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ };
+ loaderTest('directly', files, (Uri directory, loader) async {
+ var file = directory.resolve('.dart_tool/package_config.json');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ loaderTest('indirectly through .packages', files,
+ (Uri directory, loader) async {
+ var file = directory.resolve('.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ loaderTest('package_config.json non-default name', {
+ '.packages': packagesFile,
+ 'subdir': {
+ 'pheldagriff': packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('subdir/pheldagriff');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('package_config.json named .packages', {
+ 'subdir': {
+ '.packages': packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('subdir/.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('.packages', {
+ '.packages': packagesFile,
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('.packages non-default name', {
+ 'pheldagriff': packagesFile,
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('pheldagriff');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('no config found', {}, (Uri directory, loader) {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsA(isA<ArgumentError>()));
+ });
+
+ loaderTest('no config found, handle error', {},
+ (Uri directory, loader) async {
+ var file = directory.resolve('anyname');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<ArgumentError>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ loaderTest('specified file syntax error', {
+ 'anyname': 'syntax error',
+ }, (Uri directory, loader) {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+
+ loaderTest('specified file syntax onError', {
+ 'anyname': 'syntax error',
+ }, (directory, loader) async {
+ var file = directory.resolve('anyname');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ // Don't look for package_config.json if original file not named .packages.
+ loaderTest('specified file syntax error with alternative', {
+ 'anyname': 'syntax error',
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ }, (directory, loader) async {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ loaderTest('file syntax error with {', {
+ '.packages': '{syntax error',
+ }, (directory, loader) async {
+ var file = directory.resolve('.packages');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+ });
+}
diff --git a/pkgs/package_config/test/package_config_impl_test.dart b/pkgs/package_config/test/package_config_impl_test.dart
new file mode 100644
index 0000000..0f39963
--- /dev/null
+++ b/pkgs/package_config/test/package_config_impl_test.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2020, 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' show jsonDecode;
+
+import 'package:package_config/package_config_types.dart';
+import 'package:test/test.dart';
+import 'src/util.dart';
+
+void main() {
+ var unique = Object();
+ var root = Uri.file('/tmp/root/');
+
+ group('LanguageVersion', () {
+ test('minimal', () {
+ var version = LanguageVersion(3, 5);
+ expect(version.major, 3);
+ expect(version.minor, 5);
+ });
+
+ test('negative major', () {
+ expect(() => LanguageVersion(-1, 1), throwsArgumentError);
+ });
+
+ test('negative minor', () {
+ expect(() => LanguageVersion(1, -1), throwsArgumentError);
+ });
+
+ test('minimal parse', () {
+ var version = LanguageVersion.parse('3.5');
+ expect(version.major, 3);
+ expect(version.minor, 5);
+ });
+
+ void failParse(String name, String input) {
+ test('$name - error', () {
+ expect(() => LanguageVersion.parse(input),
+ throwsA(isA<PackageConfigError>()));
+ expect(() => LanguageVersion.parse(input), throwsFormatException);
+ var failed = false;
+ var actual = LanguageVersion.parse(input, onError: (_) {
+ failed = true;
+ });
+ expect(failed, true);
+ expect(actual, isA<LanguageVersion>());
+ });
+ }
+
+ failParse('Leading zero major', '01.1');
+ failParse('Leading zero minor', '1.01');
+ failParse('Sign+ major', '+1.1');
+ failParse('Sign- major', '-1.1');
+ failParse('Sign+ minor', '1.+1');
+ failParse('Sign- minor', '1.-1');
+ failParse('WhiteSpace 1', ' 1.1');
+ failParse('WhiteSpace 2', '1 .1');
+ failParse('WhiteSpace 3', '1. 1');
+ failParse('WhiteSpace 4', '1.1 ');
+ });
+
+ group('Package', () {
+ test('minimal', () {
+ var package = Package('name', root, extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, root);
+ expect(package.languageVersion, null);
+ expect(package.extraData, same(unique));
+ });
+
+ test('absolute package root', () {
+ var version = LanguageVersion(1, 1);
+ var absolute = root.resolve('foo/bar/');
+ var package = Package('name', root,
+ packageUriRoot: absolute,
+ relativeRoot: false,
+ languageVersion: version,
+ extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, absolute);
+ expect(package.languageVersion, version);
+ expect(package.extraData, same(unique));
+ expect(package.relativeRoot, false);
+ });
+
+ test('relative package root', () {
+ var relative = Uri.parse('foo/bar/');
+ var absolute = root.resolveUri(relative);
+ var package = Package('name', root,
+ packageUriRoot: relative, relativeRoot: true, extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, absolute);
+ expect(package.relativeRoot, true);
+ expect(package.languageVersion, null);
+ expect(package.extraData, same(unique));
+ });
+
+ for (var badName in ['a/z', 'a:z', '', '...']) {
+ test("Invalid name '$badName'", () {
+ expect(() => Package(badName, root), throwsPackageConfigError);
+ });
+ }
+
+ test('Invalid root, not absolute', () {
+ expect(
+ () => Package('name', Uri.parse('/foo/')), throwsPackageConfigError);
+ });
+
+ test('Invalid root, not ending in slash', () {
+ expect(() => Package('name', Uri.parse('file:///foo')),
+ throwsPackageConfigError);
+ });
+
+ test('invalid package root, not ending in slash', () {
+ expect(() => Package('name', root, packageUriRoot: Uri.parse('foo')),
+ throwsPackageConfigError);
+ });
+
+ test('invalid package root, not inside root', () {
+ expect(() => Package('name', root, packageUriRoot: Uri.parse('../baz/')),
+ throwsPackageConfigError);
+ });
+ });
+
+ group('package config', () {
+ test('empty', () {
+ var empty = PackageConfig([], extraData: unique);
+ expect(empty.version, 2);
+ expect(empty.packages, isEmpty);
+ expect(empty.extraData, same(unique));
+ expect(empty.resolve(pkg('a', 'b')), isNull);
+ });
+
+ test('single', () {
+ var package = Package('name', root);
+ var single = PackageConfig([package], extraData: unique);
+ expect(single.version, 2);
+ expect(single.packages, hasLength(1));
+ expect(single.extraData, same(unique));
+ expect(single.resolve(pkg('a', 'b')), isNull);
+ var resolved = single.resolve(pkg('name', 'a/b'));
+ expect(resolved, root.resolve('a/b'));
+ });
+ });
+ test('writeString', () {
+ var config = PackageConfig([
+ Package('foo', Uri.parse('file:///pkg/foo/'),
+ packageUriRoot: Uri.parse('file:///pkg/foo/lib/'),
+ relativeRoot: false,
+ languageVersion: LanguageVersion(2, 4),
+ extraData: {'foo': 'foo!'}),
+ Package('bar', Uri.parse('file:///pkg/bar/'),
+ packageUriRoot: Uri.parse('file:///pkg/bar/lib/'),
+ relativeRoot: true,
+ extraData: {'bar': 'bar!'}),
+ ], extraData: {
+ 'extra': 'data'
+ });
+ var buffer = StringBuffer();
+ PackageConfig.writeString(config, buffer, Uri.parse('file:///pkg/'));
+ var text = buffer.toString();
+ var json = jsonDecode(text); // Is valid JSON.
+ expect(json, {
+ 'configVersion': 2,
+ 'packages': unorderedEquals([
+ {
+ 'name': 'foo',
+ 'rootUri': 'file:///pkg/foo/',
+ 'packageUri': 'lib/',
+ 'languageVersion': '2.4',
+ 'foo': 'foo!',
+ },
+ {
+ 'name': 'bar',
+ 'rootUri': 'bar/',
+ 'packageUri': 'lib/',
+ 'bar': 'bar!',
+ },
+ ]),
+ 'extra': 'data',
+ });
+ });
+}
+
+final Matcher throwsPackageConfigError = throwsA(isA<PackageConfigError>());
diff --git a/pkgs/package_config/test/parse_test.dart b/pkgs/package_config/test/parse_test.dart
new file mode 100644
index 0000000..a92b9bf
--- /dev/null
+++ b/pkgs/package_config/test/parse_test.dart
@@ -0,0 +1,552 @@
+// 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 'dart:typed_data';
+
+import 'package:package_config/package_config_types.dart';
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+import 'package:package_config/src/packages_file.dart' as packages;
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+
+void main() {
+ group('.packages', () {
+ test('valid', () {
+ var packagesFile = '# Generated by pub yadda yadda\n'
+ 'foo:file:///foo/lib/\n'
+ 'bar:/bar/lib/\n'
+ 'baz:lib/\n';
+ var result = packages.parse(utf8.encode(packagesFile),
+ Uri.parse('file:///tmp/file.dart'), throwError);
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, {'foo', 'bar', 'baz'});
+ expect(result.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(result.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(result.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+
+ var foo = result['foo']!;
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse('file:///foo/'));
+ expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
+ expect(foo.languageVersion, LanguageVersion(2, 7));
+ expect(foo.relativeRoot, false);
+ });
+
+ test('valid empty', () {
+ var packagesFile = '# Generated by pub yadda yadda\n';
+ var result = packages.parse(
+ utf8.encode(packagesFile), Uri.file('/tmp/file.dart'), throwError);
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, <String>{});
+ });
+
+ group('invalid', () {
+ var baseFile = Uri.file('/tmp/file.dart');
+ void testThrows(String name, String content) {
+ test(name, () {
+ expect(
+ () => packages.parse(utf8.encode(content), baseFile, throwError),
+ throwsA(isA<FormatException>()));
+ });
+ test('$name, handle error', () {
+ var hadError = false;
+ packages.parse(utf8.encode(content), baseFile, (error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ });
+ expect(hadError, true);
+ });
+ }
+
+ testThrows('repeated package name', 'foo:lib/\nfoo:lib\n');
+ testThrows('no colon', 'foo\n');
+ testThrows('empty package name', ':lib/\n');
+ testThrows('dot only package name', '.:lib/\n');
+ testThrows('dot only package name', '..:lib/\n');
+ testThrows('invalid package name character', 'f\\o:lib/\n');
+ testThrows('package URI', 'foo:package:bar/lib/');
+ testThrows('location with query', 'f\\o:lib/?\n');
+ testThrows('location with fragment', 'f\\o:lib/#\n');
+ });
+ });
+
+ group('package_config.json', () {
+ test('valid', () {
+ var packageConfigFile = '''
+ {
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///foo/",
+ "packageUri": "lib/",
+ "languageVersion": "2.5",
+ "nonstandard": true
+ },
+ {
+ "name": "bar",
+ "rootUri": "/bar/",
+ "packageUri": "lib/",
+ "languageVersion": "9999.9999"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../",
+ "packageUri": "lib/"
+ },
+ {
+ "name": "noslash",
+ "rootUri": "../noslash",
+ "packageUri": "lib"
+ }
+ ],
+ "generator": "pub",
+ "other": [42]
+ }
+ ''';
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(packageConfigFile) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name},
+ {'foo', 'bar', 'baz', 'noslash'});
+
+ expect(config.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(config.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(config.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+
+ var foo = config['foo']!;
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse('file:///foo/'));
+ expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
+ expect(foo.languageVersion, LanguageVersion(2, 5));
+ expect(foo.extraData, {'nonstandard': true});
+ expect(foo.relativeRoot, false);
+
+ var bar = config['bar']!;
+ expect(bar, isNotNull);
+ expect(bar.root, Uri.parse('file:///bar/'));
+ expect(bar.packageUriRoot, Uri.parse('file:///bar/lib/'));
+ expect(bar.languageVersion, LanguageVersion(9999, 9999));
+ expect(bar.extraData, null);
+ expect(bar.relativeRoot, false);
+
+ var baz = config['baz']!;
+ expect(baz, isNotNull);
+ expect(baz.root, Uri.parse('file:///tmp/'));
+ expect(baz.packageUriRoot, Uri.parse('file:///tmp/lib/'));
+ expect(baz.languageVersion, null);
+ expect(baz.relativeRoot, true);
+
+ // No slash after root or package root. One is inserted.
+ var noslash = config['noslash']!;
+ expect(noslash, isNotNull);
+ expect(noslash.root, Uri.parse('file:///tmp/noslash/'));
+ expect(noslash.packageUriRoot, Uri.parse('file:///tmp/noslash/lib/'));
+ expect(noslash.languageVersion, null);
+ expect(noslash.relativeRoot, true);
+
+ expect(config.extraData, {
+ 'generator': 'pub',
+ 'other': [42]
+ });
+ });
+
+ test('valid other order', () {
+ // The ordering in the file is not important.
+ var packageConfigFile = '''
+ {
+ "generator": "pub",
+ "other": [42],
+ "packages": [
+ {
+ "languageVersion": "2.5",
+ "packageUri": "lib/",
+ "rootUri": "file:///foo/",
+ "name": "foo"
+ },
+ {
+ "packageUri": "lib/",
+ "languageVersion": "9999.9999",
+ "rootUri": "/bar/",
+ "name": "bar"
+ },
+ {
+ "packageUri": "lib/",
+ "name": "baz",
+ "rootUri": "../"
+ }
+ ],
+ "configVersion": 2
+ }
+ ''';
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(packageConfigFile) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name}, {'foo', 'bar', 'baz'});
+
+ expect(config.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(config.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(config.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+ expect(config.extraData, {
+ 'generator': 'pub',
+ 'other': [42]
+ });
+ });
+
+ // Check that a few minimal configurations are valid.
+ // These form the basis of invalid tests below.
+ var cfg = '"configVersion":2';
+ var pkgs = '"packages":[]';
+ var name = '"name":"foo"';
+ var root = '"rootUri":"/foo/"';
+ test('minimal', () {
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode('{$cfg,$pkgs}') as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(config.packages, isEmpty);
+ });
+ test('minimal package', () {
+ // A package must have a name and a rootUri, the remaining properties
+ // are optional.
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode('{$cfg,"packages":[{$name,$root}]}') as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(config.packages.first.name, 'foo');
+ });
+
+ test('nested packages', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': '/foo/', 'packageUri': 'lib/'},
+ {'name': 'bar', 'rootUri': '/foo/bar/', 'packageUri': 'lib/'},
+ {'name': 'baz', 'rootUri': '/foo/bar/baz/', 'packageUri': 'lib/'},
+ {'name': 'qux', 'rootUri': '/foo/qux/', 'packageUri': 'lib/'},
+ ]
+ }));
+ // ignore: unnecessary_cast
+ var config = parsePackageConfigBytes(configBytes as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+ expect(config.version, 2);
+ expect(config.packageOf(Uri.parse('file:///foo/lala/lala.dart'))!.name,
+ 'foo');
+ expect(config.packageOf(Uri.parse('file:///foo/bar/lala.dart'))!.name,
+ 'bar');
+ expect(config.packageOf(Uri.parse('file:///foo/bar/baz/lala.dart'))!.name,
+ 'baz');
+ expect(config.packageOf(Uri.parse('file:///foo/qux/lala.dart'))!.name,
+ 'qux');
+ expect(config.toPackageUri(Uri.parse('file:///foo/lib/diz')),
+ Uri.parse('package:foo/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/bar/lib/diz')),
+ Uri.parse('package:bar/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/bar/baz/lib/diz')),
+ Uri.parse('package:baz/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/qux/lib/diz')),
+ Uri.parse('package:qux/diz'));
+ });
+
+ test('nested packages 2', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': '/', 'packageUri': 'lib/'},
+ {'name': 'bar', 'rootUri': '/bar/', 'packageUri': 'lib/'},
+ {'name': 'baz', 'rootUri': '/bar/baz/', 'packageUri': 'lib/'},
+ {'name': 'qux', 'rootUri': '/qux/', 'packageUri': 'lib/'},
+ ]
+ }));
+ // ignore: unnecessary_cast
+ var config = parsePackageConfigBytes(configBytes as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+ expect(config.version, 2);
+ expect(
+ config.packageOf(Uri.parse('file:///lala/lala.dart'))!.name, 'foo');
+ expect(config.packageOf(Uri.parse('file:///bar/lala.dart'))!.name, 'bar');
+ expect(config.packageOf(Uri.parse('file:///bar/baz/lala.dart'))!.name,
+ 'baz');
+ expect(config.packageOf(Uri.parse('file:///qux/lala.dart'))!.name, 'qux');
+ expect(config.toPackageUri(Uri.parse('file:///lib/diz')),
+ Uri.parse('package:foo/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///bar/lib/diz')),
+ Uri.parse('package:bar/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///bar/baz/lib/diz')),
+ Uri.parse('package:baz/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///qux/lib/diz')),
+ Uri.parse('package:qux/diz'));
+ });
+
+ test('packageOf is case sensitive on windows', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': 'file:///C:/Foo/', 'packageUri': 'lib/'},
+ ]
+ }));
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ configBytes as Uint8List,
+ Uri.parse('file:///C:/tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(
+ config.packageOf(Uri.parse('file:///C:/foo/lala/lala.dart')), null);
+ expect(config.packageOf(Uri.parse('file:///C:/Foo/lala/lala.dart'))!.name,
+ 'foo');
+ });
+
+ group('invalid', () {
+ void testThrows(String name, String source) {
+ test(name, () {
+ expect(
+ // ignore: unnecessary_cast
+ () => parsePackageConfigBytes(utf8.encode(source) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError),
+ throwsA(isA<FormatException>()));
+ });
+ }
+
+ void testThrowsContains(
+ String name, String source, String containsString) {
+ test(name, () {
+ dynamic exception;
+ try {
+ parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(source) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError,
+ );
+ } catch (e) {
+ exception = e;
+ }
+ if (exception == null) fail("Didn't get exception");
+ expect('$exception', contains(containsString));
+ });
+ }
+
+ testThrows('comment', '# comment\n {$cfg,$pkgs}');
+ testThrows('.packages file', 'foo:/foo\n');
+ testThrows('no configVersion', '{$pkgs}');
+ testThrows('no packages', '{$cfg}');
+ group('config version:', () {
+ testThrows('null', '{"configVersion":null,$pkgs}');
+ testThrows('string', '{"configVersion":"2",$pkgs}');
+ testThrows('array', '{"configVersion":[2],$pkgs}');
+ });
+ group('packages:', () {
+ testThrows('null', '{$cfg,"packages":null}');
+ testThrows('string', '{$cfg,"packages":"foo"}');
+ testThrows('object', '{$cfg,"packages":{}}');
+ });
+ group('packages entry:', () {
+ testThrows('null', '{$cfg,"packages":[null]}');
+ testThrows('string', '{$cfg,"packages":["foo"]}');
+ testThrows('array', '{$cfg,"packages":[[]]}');
+ });
+ group('package', () {
+ testThrows('no name', '{$cfg,"packages":[{$root}]}');
+ group('name:', () {
+ testThrows('null', '{$cfg,"packages":[{"name":null,$root}]}');
+ testThrows('num', '{$cfg,"packages":[{"name":1,$root}]}');
+ testThrows('object', '{$cfg,"packages":[{"name":{},$root}]}');
+ testThrows('empty', '{$cfg,"packages":[{"name":"",$root}]}');
+ testThrows('one-dot', '{$cfg,"packages":[{"name":".",$root}]}');
+ testThrows('two-dot', '{$cfg,"packages":[{"name":"..",$root}]}');
+ testThrows(
+ "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
+ testThrows(
+ "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
+ testThrows(
+ "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
+ });
+
+ testThrows('no root', '{$cfg,"packages":[{$name}]}');
+ group('root:', () {
+ testThrows('null', '{$cfg,"packages":[{$name,"rootUri":null}]}');
+ testThrows('num', '{$cfg,"packages":[{$name,"rootUri":1}]}');
+ testThrows('object', '{$cfg,"packages":[{$name,"rootUri":{}}]}');
+ testThrows('fragment', '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
+ testThrows('query', '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
+ testThrows('package-URI',
+ '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
+ });
+ group('package-URI root:', () {
+ testThrows(
+ 'null', '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
+ testThrows('num', '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
+ testThrows(
+ 'object', '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
+ testThrows('fragment',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
+ testThrows(
+ 'query', '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
+ testThrows('package: URI',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
+ testThrows('not inside root',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
+ });
+ group('language version', () {
+ testThrows('null',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
+ testThrows(
+ 'num', '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
+ testThrows('object',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
+ testThrows('empty',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
+ testThrows('non number.number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
+ testThrows('number.non number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
+ testThrows('non number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
+ testThrows('one number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
+ testThrows('three numbers',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
+ testThrows('leading zero first',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
+ testThrows('leading zero second',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
+ testThrows('trailing-',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
+ testThrows('trailing+',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
+ });
+ });
+ testThrows('duplicate package name',
+ '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
+ testThrowsContains(
+ // The roots of foo and bar are the same.
+ 'same roots',
+ '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}',
+ 'the same root directory');
+ testThrowsContains(
+ // The roots of foo and bar are the same.
+ 'same roots 2',
+ '{$cfg,"packages":[{$name,"rootUri":"/"},{"name":"bar","rootUri":"/"}]}',
+ 'the same root directory');
+ testThrowsContains(
+ // The root of bar is inside the root of foo,
+ // but the package root of foo is inside the root of bar.
+ 'between root and lib',
+ '{$cfg,"packages":['
+ '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},'
+ '{"name":"bar","rootUri":"/foo/bar/","packageUri":"baz/lib"}]}',
+ 'package root of foo is inside the root of bar');
+
+ // This shouldn't be allowed, but for internal reasons it is.
+ test('package inside package root', () {
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(
+ '{$cfg,"packages":['
+ '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},'
+ '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}',
+ ) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(
+ config
+ .packageOf(Uri.parse('file:///foo/lib/bar/lib/lala.dart'))!
+ .name,
+ 'foo'); // why not bar?
+ expect(config.toPackageUri(Uri.parse('file:///foo/lib/bar/lib/diz')),
+ Uri.parse('package:foo/bar/lib/diz')); // why not package:bar/diz?
+ });
+ });
+ });
+
+ group('factories', () {
+ void testConfig(String name, PackageConfig config, PackageConfig expected) {
+ group(name, () {
+ test('structure', () {
+ expect(config.version, expected.version);
+ var expectedPackages = {for (var p in expected.packages) p.name};
+ var actualPackages = {for (var p in config.packages) p.name};
+ expect(actualPackages, expectedPackages);
+ });
+ for (var package in config.packages) {
+ var name = package.name;
+ test('package $name', () {
+ var expectedPackage = expected[name]!;
+ expect(expectedPackage, isNotNull);
+ expect(package.root, expectedPackage.root, reason: 'root');
+ expect(package.packageUriRoot, expectedPackage.packageUriRoot,
+ reason: 'package root');
+ expect(package.languageVersion, expectedPackage.languageVersion,
+ reason: 'languageVersion');
+ });
+ }
+ });
+ }
+
+ var configText = '''
+ {"configVersion": 2, "packages": [
+ {
+ "name": "foo",
+ "rootUri": "foo/",
+ "packageUri": "bar/",
+ "languageVersion": "1.2"
+ }
+ ]}
+ ''';
+ var baseUri = Uri.parse('file:///start/');
+ var config = PackageConfig([
+ Package('foo', Uri.parse('file:///start/foo/'),
+ packageUriRoot: Uri.parse('file:///start/foo/bar/'),
+ languageVersion: LanguageVersion(1, 2))
+ ]);
+ testConfig(
+ 'string', PackageConfig.parseString(configText, baseUri), config);
+ testConfig(
+ 'bytes',
+ PackageConfig.parseBytes(
+ Uint8List.fromList(configText.codeUnits), baseUri),
+ config);
+ testConfig('json', PackageConfig.parseJson(jsonDecode(configText), baseUri),
+ config);
+
+ baseUri = Uri.parse('file:///start2/');
+ config = PackageConfig([
+ Package('foo', Uri.parse('file:///start2/foo/'),
+ packageUriRoot: Uri.parse('file:///start2/foo/bar/'),
+ languageVersion: LanguageVersion(1, 2))
+ ]);
+ testConfig(
+ 'string2', PackageConfig.parseString(configText, baseUri), config);
+ testConfig(
+ 'bytes2',
+ PackageConfig.parseBytes(
+ Uint8List.fromList(configText.codeUnits), baseUri),
+ config);
+ testConfig('json2',
+ PackageConfig.parseJson(jsonDecode(configText), baseUri), config);
+ });
+}
diff --git a/pkgs/package_config/test/src/util.dart b/pkgs/package_config/test/src/util.dart
new file mode 100644
index 0000000..780ee80
--- /dev/null
+++ b/pkgs/package_config/test/src/util.dart
@@ -0,0 +1,57 @@
+// 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 'dart:typed_data';
+
+import 'package:test/test.dart';
+
+/// Creates a package: URI.
+Uri pkg(String packageName, String packagePath) {
+ var path =
+ "$packageName${packagePath.startsWith('/') ? "" : "/"}$packagePath";
+ return Uri(scheme: 'package', path: path);
+}
+
+// Remove if not used.
+String configFromPackages(List<List<String>> packages) => """
+{
+ "configVersion": 2,
+ "packages": [
+${packages.map((nu) => """
+ {
+ "name": "${nu[0]}",
+ "rootUri": "${nu[1]}"
+ }""").join(",\n")}
+ ]
+}
+""";
+
+/// Mimics a directory structure of [description] and runs [loaderTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+void loaderTest(
+ String name,
+ Map<String, Object> description,
+ void Function(Uri root, Future<Uint8List?> Function(Uri) loader) loaderTest,
+) {
+ var root = Uri(scheme: 'test', path: '/');
+ Future<Uint8List?> loader(Uri uri) async {
+ var path = uri.path;
+ if (!uri.isScheme('test') || !path.startsWith('/')) return null;
+ var parts = path.split('/');
+ Object? value = description;
+ for (var i = 1; i < parts.length; i++) {
+ if (value is! Map<String, Object?>) return null;
+ value = value[parts[i]];
+ }
+ // ignore: unnecessary_cast
+ if (value is String) return utf8.encode(value) as Uint8List;
+ return null;
+ }
+
+ test(name, () => loaderTest(root, loader));
+}
diff --git a/pkgs/package_config/test/src/util_io.dart b/pkgs/package_config/test/src/util_io.dart
new file mode 100644
index 0000000..e032556
--- /dev/null
+++ b/pkgs/package_config/test/src/util_io.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2020, 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:io';
+
+import 'package:package_config/src/util_io.dart';
+import 'package:test/test.dart';
+
+/// Creates a directory structure from [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+/// Introduces a group to hold the [setUp]/[tearDown] logic.
+void fileTest(String name, Map<String, Object> description,
+ void Function(Directory directory) fileTest) {
+ group('file-test', () {
+ var tempDir = Directory.systemTemp.createTempSync('pkgcfgtest');
+ setUp(() {
+ _createFiles(tempDir, description);
+ });
+ tearDown(() {
+ tempDir.deleteSync(recursive: true);
+ });
+ test(name, () => fileTest(tempDir));
+ });
+}
+
+/// Creates a set of files under a new temporary directory.
+/// Returns the temporary directory.
+///
+/// The [description] is a map from file names to content.
+/// If the content is again a map, it represents a subdirectory
+/// with the content as description.
+/// Otherwise the content should be a string,
+/// which is written to the file as UTF-8.
+// Directory createTestFiles(Map<String, Object> description) {
+// var target = Directory.systemTemp.createTempSync("pkgcfgtest");
+// _createFiles(target, description);
+// return target;
+// }
+
+// Creates temporary files in the target directory.
+void _createFiles(Directory target, Map<Object?, Object?> description) {
+ description.forEach((name, content) {
+ var entryName = pathJoin(target.path, '$name');
+ if (content is Map<Object?, Object?>) {
+ _createFiles(Directory(entryName)..createSync(), content);
+ } else {
+ File(entryName).writeAsStringSync(content as String, flush: true);
+ }
+ });
+}
+
+/// Creates a [Directory] for a subdirectory of [parent].
+Directory subdir(Directory parent, String dirName) =>
+ Directory(pathJoinAll([parent.path, ...dirName.split('/')]));
+
+/// Creates a [File] for an entry in the [directory] directory.
+File dirFile(Directory directory, String fileName) =>
+ File(pathJoin(directory.path, fileName));