Further tweaks on the API. (dart-lang/package_config#57)

* Allow nested packages.
diff --git a/pkgs/package_config/lib/package_config.dart b/pkgs/package_config/lib/package_config.dart
index 4d6f346..77e8d61 100644
--- a/pkgs/package_config/lib/package_config.dart
+++ b/pkgs/package_config/lib/package_config.dart
@@ -23,10 +23,16 @@
 /// It is considered a `package_config.json` file if its first character
 /// is a `{`.
 ///
-/// If the file is a `.packages` file, also checks if there is a
-/// `.dart_tool/package_config.json` file next to the original file,
+/// If the file is a `.packages` file 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.
-Future<PackageConfig> loadPackageConfig(File file) => readAnyConfigFile(file);
+/// 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.
+Future<PackageConfig> loadPackageConfig(File file,
+        {bool preferNewest = true}) =>
+    readAnyConfigFile(file, preferNewest);
 
 /// Reads a specific package configuration URI.
 ///
@@ -36,19 +42,24 @@
 /// It is considered a `package_config.json` file if its first
 /// non-whitespace character is a `{`.
 ///
-/// If the file is a `.packages` file, first 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 true, the default, and the file is a `.packages` file,
+/// 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,
+/// 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,
@@ -58,8 +69,9 @@
 /// As such, it does not distinguish between a file not existing,
 /// and it being temporarily locked or unreachable.
 Future<PackageConfig> loadPackageConfigUri(Uri file,
-        {Future<Uint8List /*?*/ > loader(Uri uri) /*?*/}) =>
-    readAnyConfigFileUri(file, loader);
+        {Future<Uint8List /*?*/ > loader(Uri uri) /*?*/,
+        bool preferNewest = true}) =>
+    readAnyConfigFileUri(file, loader, preferNewest);
 
 /// Finds a package configuration relative to [directory].
 ///
@@ -123,14 +135,6 @@
 /// If the `.dart_tool/` directory does not exist, it is created.
 /// If it cannot be created, this operation fails.
 ///
-/// If [extraData] contains any entries, they are added to the JSON
-/// written to the `package_config.json` file. Entries with the names
-/// `"configVersion"` or `"packages"` are ignored, all other entries
-/// are added verbatim.
-/// This is intended for, e.g., the
-/// `"generator"`, `"generated"` and `"generatorVersion"`
-/// properties.
-///
 /// Also writes a `.packages` file in [directory].
 /// This will stop happening eventually as the `.packages` file becomes
 /// discontinued.
diff --git a/pkgs/package_config/lib/src/discovery.dart b/pkgs/package_config/lib/src/discovery.dart
index 6b0fc8f..5ad6ac5 100644
--- a/pkgs/package_config/lib/src/discovery.dart
+++ b/pkgs/package_config/lib/src/discovery.dart
@@ -5,7 +5,7 @@
 import "dart:io";
 import 'dart:typed_data';
 
-import "package:path/path.dart" as path;
+import "package:path/path.dart" as p;
 
 import "errors.dart";
 import "package_config_impl.dart";
@@ -110,23 +110,13 @@
 
 Future<File> /*?*/ checkForPackageConfigJsonFile(Directory directory) async {
   assert(directory.isAbsolute);
-  var file =
-      File(path.join(directory.path, ".dart_tool", "package_config.json"));
+  var file = File(p.join(directory.path, ".dart_tool", "package_config.json"));
   if (await file.exists()) return file;
   return null;
 }
 
 Future<File /*?*/ > checkForDotPackagesFile(Directory directory) async {
-  var file = File(path.join(directory.path, ".packages"));
+  var file = File(p.join(directory.path, ".packages"));
   if (await file.exists()) return file;
   return null;
 }
-
-Future<Uint8List /*?*/ > _loadFile(File file) async {
-  Uint8List bytes;
-  try {
-    return await file.readAsBytes();
-  } catch (_) {
-    return null;
-  }
-}
diff --git a/pkgs/package_config/lib/src/package_config_impl.dart b/pkgs/package_config/lib/src/package_config_impl.dart
index 0bbe18f..6257961 100644
--- a/pkgs/package_config/lib/src/package_config_impl.dart
+++ b/pkgs/package_config/lib/src/package_config_impl.dart
@@ -4,22 +4,26 @@
 
 import 'errors.dart';
 import "package_config.dart";
-export "package_config.dart";
 import "util.dart";
 
+export "package_config.dart";
+
 class SimplePackageConfig implements PackageConfig {
   final int version;
   final Map<String, Package> _packages;
+  final PackageTree _packageTree;
   final dynamic extraData;
 
-  SimplePackageConfig(int version, Iterable<Package> packages, [this.extraData])
-      : version = _validateVersion(version),
-        _packages = _validatePackages(packages);
+  SimplePackageConfig(int version, Iterable<Package> packages,
+      [dynamic extraData])
+      : this._(_validateVersion(version), packages,
+            [...packages]..sort(_compareRoot), extraData);
 
-  SimplePackageConfig._(
-      int version, Iterable<SimplePackage> packages, this.extraData)
-      : version = _validateVersion(version),
-        _packages = {for (var package in packages) package.name: package};
+  /// Expects a list of [packages] sorted on root path.
+  SimplePackageConfig._(this.version, Iterable<Package> originalPackages,
+      List<Package> packages, this.extraData)
+      : _packageTree = _validatePackages(originalPackages, packages),
+        _packages = {for (var p in packages) p.name: p};
 
   /// Creates empty configuration.
   ///
@@ -27,6 +31,7 @@
   /// found, but code expects a non-null configuration.
   const SimplePackageConfig.empty()
       : version = 1,
+        _packageTree = const EmptyPackageTree(),
         _packages = const <String, Package>{},
         extraData = null;
 
@@ -38,18 +43,28 @@
     return version;
   }
 
-  static Map<String, Package> _validatePackages(Iterable<Package> packages) {
+  static PackageTree _validatePackages(
+      Iterable<Package> originalPackages, List<Package> packages) {
+    // Assumes packages are sorted.
     Map<String, Package> result = {};
-    for (var package in packages) {
-      if (package is! SimplePackage) {
+    var tree = MutablePackageTree();
+    SimplePackage package;
+    for (var originalPackage in packages) {
+      if (originalPackage is! SimplePackage) {
         // SimplePackage validates these properties.
         try {
-          _validatePackageData(package.name, package.root,
-              package.packageUriRoot, package.languageVersion);
+          package = SimplePackage(
+              originalPackage.name,
+              originalPackage.root,
+              originalPackage.packageUriRoot,
+              originalPackage.languageVersion,
+              originalPackage.extraData);
         } catch (e) {
           throw PackageConfigArgumentError(
               packages, "packages", "Package ${package.name}: ${e.message}");
         }
+      } else {
+        package = originalPackage;
       }
       var name = package.name;
       if (result.containsKey(name)) {
@@ -57,33 +72,31 @@
             name, "packages", "Duplicate package name");
       }
       result[name] = package;
-    }
-
-    // Check that no root URI is a prefix of another.
-    if (result.length > 1) {
-      // Uris cache their toString, so this is not as bad as it looks.
-      var rootUris = [...result.values]
-        ..sort((a, b) => a.root.toString().compareTo(b.root.toString()));
-      var prev = rootUris[0];
-      var prevRoot = prev.root.toString();
-      for (int i = 1; i < rootUris.length; i++) {
-        var next = rootUris[i];
-        var nextRoot = next.root.toString();
-        // If one string is a prefix of another,
-        // the former sorts just before the latter.
-        if (nextRoot.startsWith(prevRoot)) {
+      try {
+        tree.add(0, package);
+      } on ConflictException catch (e) {
+        // There is a conflict with an existing package.
+        var existingPackage = e.existingPackage;
+        if (e.isRootConflict) {
           throw PackageConfigArgumentError(
-              packages,
+              originalPackages,
               "packages",
-              "Package ${next.name} root overlaps "
-                  "package ${prev.name} root.\n"
-                  "${prev.name} root: $prevRoot\n"
-                  "${next.name} root: $nextRoot\n");
+              "Packages ${package.name} and ${existingPackage.name}"
+                  "have the same root directory: ${package.root}.\n");
         }
-        prev = next;
+        assert(e.isPackageRootConflict);
+        // Or package is inside the package URI root of the existing package.
+        throw PackageConfigArgumentError(
+            originalPackages,
+            "packages",
+            "Package ${package.name} is inside the package URI root of "
+                "package ${existingPackage.name}.\n"
+                "${existingPackage.name} URI root: "
+                "${existingPackage.packageUriRoot}\n"
+                "${package.name} root: ${package.root}\n");
       }
     }
-    return result;
+    return tree;
   }
 
   Iterable<Package> get packages => _packages.values;
@@ -96,14 +109,7 @@
   /// That is, the [Package.rootUri] directory is a parent directory
   /// of the [file]'s location.
   /// Returns `null` if the file does not belong to any package.
-  Package /*?*/ packageOf(Uri file) {
-    String path = file.toString();
-    for (var package in _packages.values) {
-      var rootPath = package.root.toString();
-      if (path.startsWith(rootPath)) return package;
-    }
-    return null;
-  }
+  Package /*?*/ packageOf(Uri file) => _packageTree.packageOf(file);
 
   Uri /*?*/ resolve(Uri packageUri) {
     String packageName = checkValidPackageUri(packageUri, "packageUri");
@@ -120,12 +126,15 @@
       throw PackageConfigArgumentError(nonPackageUri, "nonPackageUri",
           "Must not have query or fragment part");
     }
-    for (var package in _packages.values) {
-      var root = package.packageUriRoot;
-      if (isUriPrefix(root, nonPackageUri)) {
-        var rest = nonPackageUri.toString().substring(root.toString().length);
-        return Uri(scheme: "package", path: "${package.name}/$rest");
-      }
+    // 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;
   }
@@ -142,6 +151,9 @@
   SimplePackage._(this.name, this.root, this.packageUriRoot,
       this.languageVersion, this.extraData);
 
+  /// Creates a [SimplePackage] with the provided content.
+  ///
+  /// The provided arguments must be valid.
   factory SimplePackage(String name, Uri root, Uri packageUriRoot,
       String /*?*/ languageVersion, dynamic extraData) {
     _validatePackageData(name, root, packageUriRoot, languageVersion);
@@ -179,3 +191,144 @@
         languageVersion, "languageVersion", "Invalid language version format");
   }
 }
+
+abstract class PackageTree {
+  SimplePackage /*?*/ packageOf(Uri file);
+}
+
+/// Packages of a package configuration ordered by root path.
+///
+/// A package is said to be inside another package if the root path URI of
+/// the latter is a prefix of the root path URI of the former.
+/// No two packages of a package may have the same root path, so this
+/// path prefix ordering defines a tree-like partial ordering on packages
+/// of a configuration.
+///
+/// The package tree contains an ordered mapping of unrelated packages
+/// (represented by their name) to their immediately nested packages' names.
+class MutablePackageTree implements PackageTree {
+  final List<SimplePackage> packages = [];
+  Map<String, MutablePackageTree /*?*/ > /*?*/ _packageChildren;
+
+  /// Tries to (add) `package` to the tree.
+  ///
+  /// Throws [ConflictException] if the added package conflicts with an
+  /// existing package.
+  /// It conflicts if it has the same root path, or if the new package
+  /// contains the existing package's package root.
+  void add(int start, SimplePackage package) {
+    var path = package.root.toString();
+    for (var childPackage in packages) {
+      var childPath = childPackage.root.toString();
+      assert(childPath.length > start);
+      assert(path.startsWith(childPath.substring(0, start)));
+      if (_beginsWith(start, childPath, path)) {
+        var childPathLength = childPath.length;
+        if (path.length == childPathLength) {
+          throw ConflictException.root(package, childPackage);
+        }
+        var childPackageRoot = childPackage.packageUriRoot.toString();
+        if (_beginsWith(childPathLength, childPackageRoot, path)) {
+          throw ConflictException.packageRoot(package, childPackage);
+        }
+        _treeOf(childPackage).add(childPathLength, package);
+        return;
+      }
+    }
+    packages.add(package);
+  }
+
+  SimplePackage /*?*/ packageOf(Uri file) {
+    return findPackageOf(0, file.toString());
+  }
+
+  /// Finds package containing [path] in this tree.
+  ///
+  /// Returns `null` if no such package is found.
+  ///
+  /// Assumes the first [start] characters of path agrees with all
+  /// the packages at this level of the tree.
+  SimplePackage /*?*/ findPackageOf(int start, String path) {
+    for (var childPackage in packages) {
+      var childPath = childPackage.root.toString();
+      if (_beginsWith(start, childPath, path)) {
+        // The [package] is inside [childPackage].
+        var childPathLength = childPath.length;
+        if (path.length == childPathLength) return childPackage;
+        var uriRoot = childPackage.packageUriRoot.toString();
+        // Is [package] is inside the URI root of [childPackage].
+        if (uriRoot.length == childPathLength ||
+            _beginsWith(childPathLength, uriRoot, path)) {
+          return childPackage;
+        }
+        // Otherwise add [package] as child of [childPackage].
+        // TODO(lrn): When NNBD comes, convert to:
+        // return _packageChildren?[childPackage.name]
+        //     ?.packageOf(childPathLength, path) ?? childPackage;
+        if (_packageChildren == null) return childPackage;
+        var childTree = _packageChildren[childPackage.name];
+        if (childTree == null) return childPackage;
+        return childTree.findPackageOf(childPathLength, path) ?? childPackage;
+      }
+    }
+    return null;
+  }
+
+  /// Returns the [PackageTree] of the children of [package].
+  ///
+  /// Ensures that the object is allocated if necessary.
+  MutablePackageTree _treeOf(SimplePackage package) {
+    var children = _packageChildren ??= {};
+    return children[package.name] ??= MutablePackageTree();
+  }
+}
+
+class EmptyPackageTree implements PackageTree {
+  const EmptyPackageTree();
+
+  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 (int i = start; i < parentPath.length; i++) {
+    if (longerPath.codeUnitAt(i) != parentPath.codeUnitAt(i)) return false;
+  }
+  return true;
+}
+
+/// Conflict between packages added to the same configuration.
+///
+/// The [package] conflicts with [existingPackage] if it has
+/// the same root path ([isRootConflict]) or the package URI root path
+/// of [existingPackage] is inside the root path of [package]
+/// ([isPackageRootConflict]).
+class ConflictException {
+  /// The existing package that [package] conflicts with.
+  final SimplePackage existingPackage;
+
+  /// The package that could not be added without a conflict.
+  final SimplePackage package;
+
+  /// Whether the conflict is with the package URI root of [existingPackage].
+  final bool isPackageRootConflict;
+
+  /// Creates a root conflict between [package] and [existingPackage].
+  ConflictException.root(this.package, this.existingPackage)
+      : isPackageRootConflict = false;
+
+  /// Creates a package root conflict between [package] and [existingPackage].
+  ConflictException.packageRoot(this.package, this.existingPackage)
+      : isPackageRootConflict = true;
+
+  /// WHether the conflict is with the root URI of [existingPackage].
+  bool get isRootConflict => !isPackageRootConflict;
+}
+
+/// Used for sorting packages by root path.
+int _compareRoot(Package p1, Package p2) =>
+    p1.root.toString().compareTo(p2.root.toString());
diff --git a/pkgs/package_config/lib/src/package_config_json.dart b/pkgs/package_config/lib/src/package_config_json.dart
index 8a6014c..8108102 100644
--- a/pkgs/package_config/lib/src/package_config_json.dart
+++ b/pkgs/package_config/lib/src/package_config_json.dart
@@ -7,7 +7,7 @@
 import "dart:typed_data";
 
 import 'package:charcode/ascii.dart';
-import "package:path/path.dart" as path;
+import "package:path/path.dart" as p;
 
 import "discovery.dart" show packageConfigJsonPath;
 import "errors.dart";
@@ -38,36 +38,42 @@
 /// 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, first checks whether there is an
-/// adjacent `.dart_tool/package_config.json` file, and if so,
-/// reads that instead.
+/// 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 [preferNewset] 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) async {
+Future<PackageConfig> readAnyConfigFile(File file, bool preferNewest) async {
   var bytes = await file.readAsBytes();
   int firstChar = firstNonWhitespaceChar(bytes);
   if (firstChar != $lbrace) {
     // Definitely not a JSON object, probably a .packages.
-    var alternateFile = File(path.join(
-        path.dirname(file.path), ".dart_tool", "package_config.json"));
-    if (!alternateFile.existsSync()) {
-      return packages_file.parse(bytes, file.uri);
+    if (preferNewest) {
+      var alternateFile = File(
+          p.join(p.dirname(file.path), ".dart_tool", "package_config.json"));
+      if (alternateFile.existsSync()) {
+        return parsePackageConfigBytes(
+            await alternateFile.readAsBytes(), alternateFile.uri);
+      }
     }
-    file = alternateFile;
-    bytes = await alternateFile.readAsBytes();
+    return packages_file.parse(bytes, file.uri);
   }
   return parsePackageConfigBytes(bytes, file.uri);
 }
 
 /// Like [readAnyConfigFile] but uses a URI and an optional loader.
-Future<PackageConfig> readAnyConfigFileUri(
-    Uri file, Future<Uint8List /*?*/ > loader(Uri uri) /*?*/) async {
+Future<PackageConfig> readAnyConfigFileUri(Uri file,
+    Future<Uint8List /*?*/ > loader(Uri uri) /*?*/, 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 readAnyConfigFile(File.fromUri(file));
+    if (file.isScheme("file")) {
+      return readAnyConfigFile(File.fromUri(file), preferNewest);
+    }
     loader = defaultLoader;
   }
   var bytes = await loader(file);
@@ -78,13 +84,15 @@
   int firstChar = firstNonWhitespaceChar(bytes);
   if (firstChar != $lbrace) {
     // Definitely not a JSON object, probably a .packages.
-    var alternateFile = file.resolveUri(packageConfigJsonPath);
-    var alternateBytes = await loader(alternateFile);
-    if (alternateBytes == null) {
-      return packages_file.parse(bytes, file);
+    if (preferNewest) {
+      // Check if there is a package_config.json file.
+      var alternateFile = file.resolveUri(packageConfigJsonPath);
+      var alternateBytes = await loader(alternateFile);
+      if (alternateBytes != null) {
+        return parsePackageConfigBytes(alternateBytes, alternateFile);
+      }
     }
-    bytes = alternateBytes;
-    file = alternateFile;
+    return packages_file.parse(bytes, file);
   }
   return parsePackageConfigBytes(bytes, file);
 }
@@ -242,8 +250,8 @@
 Future<void> writePackageConfigJson(
     PackageConfig config, Directory targetDirectory) async {
   // Write .dart_tool/package_config.json first.
-  var file = File(
-      path.join(targetDirectory.path, ".dart_tool", "package_config.json"));
+  var file =
+      File(p.join(targetDirectory.path, ".dart_tool", "package_config.json"));
   var baseUri = file.uri;
   var extraData = config.extraData;
   var data = <String, dynamic>{
@@ -275,7 +283,7 @@
           "${generated != null ? " on $generated" : ""}.";
     }
   }
-  file = File(path.join(targetDirectory.path, ".packages"));
+  file = File(p.join(targetDirectory.path, ".packages"));
   baseUri = file.uri;
   var buffer = StringBuffer();
   packages_file.write(buffer, config, baseUri: baseUri, comment: comment);
@@ -313,7 +321,7 @@
 
 /// Checks that the object is a valid JSON-like data structure.
 bool _validateJson(dynamic object) {
-  if (object == null || object == true || object == false) return true;
+  if (object == null || true == object || false == object) return true;
   if (object is num || object is String) return true;
   if (object is List<dynamic>) {
     for (var element in object) if (!_validateJson(element)) return false;
diff --git a/pkgs/package_config/pubspec.yaml b/pkgs/package_config/pubspec.yaml
index 0cd7ddc..781dc32 100644
--- a/pkgs/package_config/pubspec.yaml
+++ b/pkgs/package_config/pubspec.yaml
@@ -5,7 +5,7 @@
 homepage: https://github.com/dart-lang/package_config
 
 environment:
-  sdk: '>=2.5.0-dev <3.0.0'
+  sdk: '>=2.7.0 <3.0.0'
 
 dependencies:
   charcode: ^1.1.0
diff --git a/pkgs/package_config/test/discovery_test.dart b/pkgs/package_config/test/discovery_test.dart
index 5db24a1..4dd1504 100644
--- a/pkgs/package_config/test/discovery_test.dart
+++ b/pkgs/package_config/test/discovery_test.dart
@@ -170,6 +170,13 @@
         expect(config.version, 2);
         validatePackagesFile(config, directory);
       });
+      fileTest("prefer .packages", files, (Directory directory) async {
+        File file = dirFile(directory, ".packages");
+        PackageConfig config =
+            await loadPackageConfig(file, preferNewest: false);
+        expect(config.version, 1);
+        validatePackagesFile(config, directory);
+      });
     });
 
     fileTest("package_config.json non-default name", {
diff --git a/pkgs/package_config/test/discovery_uri_test.dart b/pkgs/package_config/test/discovery_uri_test.dart
index 414a43a..9558f58 100644
--- a/pkgs/package_config/test/discovery_uri_test.dart
+++ b/pkgs/package_config/test/discovery_uri_test.dart
@@ -4,7 +4,6 @@
 
 library package_config.discovery_test;
 
-import "dart:io";
 import "package:test/test.dart";
 import "package:package_config_2/package_config.dart";
 
diff --git a/pkgs/package_config/test/parse_test.dart b/pkgs/package_config/test/parse_test.dart
index 235f493..a07ef1d 100644
--- a/pkgs/package_config/test/parse_test.dart
+++ b/pkgs/package_config/test/parse_test.dart
@@ -2,7 +2,6 @@
 // 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:convert";
 
 import "package:test/test.dart";
@@ -199,9 +198,43 @@
       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/"},
+        ]
+      }));
+      var config = parsePackageConfigBytes(
+          configBytes, Uri.parse("file:///tmp/.dart_tool/file.dart"));
+      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"));
+    });
+
     group("invalid", () {
       testThrows(String name, String source) {
         test(name, () {
+          if (name == "inside lib") {
+            print(name);
+          }
           expect(
               () => parsePackageConfigBytes(utf8.encode(source),
                   Uri.parse("file:///tmp/.dart_tool/file.dart")),
@@ -304,9 +337,15 @@
       testThrows("same roots",
           '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
       testThrows(
-          "overlapping roots",
-          '{$cfg,"packages":[{$name,$root},'
-              '{"name":"bar","rootUri":"/foo/sub/"}]}');
+          // The roots of foo and bar are the same.
+          "same roots",
+          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
+      testThrows(
+          // The root of bar is inside the package root of foo.
+          "inside lib",
+          '{$cfg,"packages":['
+              '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},'
+              '{"name":"bar","rootUri":"/foo/lib/qux/"}]}');
     });
   });
 }