Add support for default package and metadata. (#53)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e802d7d..db4bb00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 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.
diff --git a/lib/discovery_analysis.dart b/lib/discovery_analysis.dart
index 67798e1..d623303 100644
--- a/lib/discovery_analysis.dart
+++ b/lib/discovery_analysis.dart
@@ -59,22 +59,22 @@
   static PackageContext findAll(Directory directory,
       {Packages root: Packages.noPackages}) {
     if (!directory.existsSync()) {
-      throw new ArgumentError("Directory not found: $directory");
+      throw ArgumentError("Directory not found: $directory");
     }
     var contexts = <PackageContext>[];
     void findRoots(Directory directory) {
       Packages packages;
       List<PackageContext> oldContexts;
-      File packagesFile = new File(path.join(directory.path, ".packages"));
+      File packagesFile = File(path.join(directory.path, ".packages"));
       if (packagesFile.existsSync()) {
         packages = _loadPackagesFile(packagesFile);
         oldContexts = contexts;
         contexts = [];
       } else {
         Directory packagesDir =
-            new Directory(path.join(directory.path, "packages"));
+            Directory(path.join(directory.path, "packages"));
         if (packagesDir.existsSync()) {
-          packages = new FilePackagesDirectoryPackages(packagesDir);
+          packages = FilePackagesDirectoryPackages(packagesDir);
           oldContexts = contexts;
           contexts = [];
         }
@@ -87,7 +87,7 @@
         }
       }
       if (packages != null) {
-        oldContexts.add(new _PackageContext(directory, packages, contexts));
+        oldContexts.add(_PackageContext(directory, packages, contexts));
         contexts = oldContexts;
       }
     }
@@ -97,7 +97,7 @@
     if (contexts.length == 1 && contexts[0].directory == directory) {
       return contexts[0];
     }
-    return new _PackageContext(directory, root, contexts);
+    return _PackageContext(directory, root, contexts);
   }
 }
 
@@ -106,10 +106,10 @@
   final Packages packages;
   final List<PackageContext> children;
   _PackageContext(this.directory, this.packages, List<PackageContext> children)
-      : children = new List<PackageContext>.unmodifiable(children);
+      : children = List<PackageContext>.unmodifiable(children);
 
   Map<Directory, Packages> asMap() {
-    var result = new HashMap<Directory, Packages>();
+    var result = HashMap<Directory, Packages>();
     recurse(_PackageContext current) {
       result[current.directory] = current.packages;
       for (var child in current.children) {
@@ -124,7 +124,7 @@
   PackageContext operator [](Directory directory) {
     String path = directory.path;
     if (!path.startsWith(this.directory.path)) {
-      throw new ArgumentError("Not inside $path: $directory");
+      throw ArgumentError("Not inside $path: $directory");
     }
     _PackageContext current = this;
     // The current path is know to agree with directory until deltaIndex.
@@ -160,8 +160,8 @@
 }
 
 Packages _loadPackagesFile(File file) {
-  var uri = new Uri.file(file.path);
+  var uri = Uri.file(file.path);
   var bytes = file.readAsBytesSync();
   var map = pkgfile.parse(bytes, uri);
-  return new MapPackages(map);
+  return MapPackages(map);
 }
diff --git a/lib/packages.dart b/lib/packages.dart
index 890f448..886fbc8 100644
--- a/lib/packages.dart
+++ b/lib/packages.dart
@@ -45,6 +45,32 @@
   /// and getting `packages` from such a `Packages` object will throw.
   Iterable<String> get packages;
 
+  /// Retrieve metadata associated with a package.
+  ///
+  /// Metadata have string keys and values, and are looked up by key.
+  ///
+  /// Returns `null` if the argument is not a valid package name,
+  /// or if the package is not one of the packages configured by
+  /// this packages object, or if the package does not have associated
+  /// metadata with the provided [key].
+  ///
+  /// Not all `Packages` objects can support metadata.
+  /// Those will always return `null`.
+  String packageMetadata(String packageName, String key);
+
+  /// Retrieve metadata associated with a library.
+  ///
+  /// If [libraryUri] is a `package:` URI, the returned value
+  /// is the same that would be returned by [packageMetadata] with
+  /// the package's name and the same key.
+  ///
+  /// If [libraryUri] is not a `package:` URI, and this [Packages]
+  /// object has a [defaultPackageName], then the [key] is looked
+  /// up on the default package instead.
+  ///
+  /// Otherwise the result is `null`.
+  String libraryMetadata(Uri libraryUri, String key);
+
   /// Return the names-to-base-URI mapping of the available packages.
   ///
   /// Returns a map from package name to a base URI.
@@ -55,4 +81,14 @@
   /// Some `Packages` objects are unable to find the package names,
   /// and calling `asMap` on such a `Packages` object will throw.
   Map<String, Uri> asMap();
+
+  /// The name of the "default package".
+  ///
+  /// A default package is a package that *non-package* libraries
+  /// may be considered part of for some purposes.
+  ///
+  /// The value is `null` if there is no default package.
+  /// Not all implementations of [Packages] supports a default package,
+  /// and will always have a `null` value for those.
+  String get defaultPackageName;
 }
diff --git a/lib/packages_file.dart b/lib/packages_file.dart
index 85de194..284d8e9 100644
--- a/lib/packages_file.dart
+++ b/lib/packages_file.dart
@@ -22,8 +22,15 @@
 /// If the content was read from a file, `baseLocation` should be the
 /// location of that file.
 ///
+/// If [allowDefaultPackage] is set to true, an entry with an empty package name
+/// is accepted. This entry does not correspond to a package, but instead
+/// represents a *default package* which non-package libraries may be considered
+/// part of in some cases. The value of that entry must be a valid package name.
+///
 /// Returns a simple mapping from package name to package location.
-Map<String, Uri> parse(List<int> source, Uri baseLocation) {
+/// If default package is allowed, the map maps the empty string to the default package's name.
+Map<String, Uri> parse(List<int> source, Uri baseLocation,
+    {bool allowDefaultPackage = false}) {
   int index = 0;
   Map<String, Uri> result = <String, Uri>{};
   while (index < source.length) {
@@ -36,7 +43,10 @@
       continue;
     }
     if (char == $colon) {
-      throw new FormatException("Missing package name", source, index - 1);
+      if (!allowDefaultPackage) {
+        throw FormatException("Missing package name", source, index - 1);
+      }
+      separatorIndex = index - 1;
     }
     isComment = char == $hash;
     while (index < source.length) {
@@ -50,22 +60,36 @@
     }
     if (isComment) continue;
     if (separatorIndex < 0) {
-      throw new FormatException("No ':' on line", source, index - 1);
+      throw FormatException("No ':' on line", source, index - 1);
     }
     var packageName = new String.fromCharCodes(source, start, separatorIndex);
-    if (!isValidPackageName(packageName)) {
-      throw new FormatException("Not a valid package name", packageName, 0);
+    if (packageName.isEmpty
+        ? !allowDefaultPackage
+        : !isValidPackageName(packageName)) {
+      throw FormatException("Not a valid package name", packageName, 0);
     }
-    var packageUri = new String.fromCharCodes(source, separatorIndex + 1, end);
-    var packageLocation = Uri.parse(packageUri);
-    packageLocation = baseLocation.resolveUri(packageLocation);
-    if (!packageLocation.path.endsWith('/')) {
-      packageLocation =
-          packageLocation.replace(path: packageLocation.path + "/");
+    var packageValue =
+        new String.fromCharCodes(source, separatorIndex + 1, end);
+    Uri packageLocation;
+    if (packageName.isEmpty) {
+      if (!isValidPackageName(packageValue)) {
+        throw FormatException(
+            "Default package entry value is not a valid package name");
+      }
+      packageLocation = Uri(path: packageValue);
+    } else {
+      packageLocation = baseLocation.resolve(packageValue);
+      if (!packageLocation.path.endsWith('/')) {
+        packageLocation =
+            packageLocation.replace(path: packageLocation.path + "/");
+      }
     }
     if (result.containsKey(packageName)) {
-      throw new FormatException(
-          "Same package name occured twice.", source, start);
+      if (packageName.isEmpty) {
+        throw FormatException(
+            "More than one default package entry", source, start);
+      }
+      throw FormatException("Same package name occured twice", source, start);
     }
     result[packageName] = packageLocation;
   }
diff --git a/lib/src/packages_impl.dart b/lib/src/packages_impl.dart
index e89d94d..817002f 100644
--- a/lib/src/packages_impl.dart
+++ b/lib/src/packages_impl.dart
@@ -26,6 +26,12 @@
   Iterable<String> get packages => new Iterable<String>.empty();
 
   Map<String, Uri> asMap() => const <String, Uri>{};
+
+  String get defaultPackageName => null;
+
+  String packageMetadata(String packageName, String key) => null;
+
+  String libraryMetadata(Uri libraryUri, String key) => null;
 }
 
 /// Base class for [Packages] implementations.
@@ -51,6 +57,12 @@
   /// Returns `null` if no package exists with that name, and that can be
   /// determined.
   Uri getBase(String packageName);
+
+  String get defaultPackageName => null;
+
+  String packageMetadata(String packageName, String key) => null;
+
+  String libraryMetadata(Uri libraryUri, String key) => null;
 }
 
 /// A [Packages] implementation based on an existing map.
@@ -58,11 +70,34 @@
   final Map<String, Uri> _mapping;
   MapPackages(this._mapping);
 
-  Uri getBase(String packageName) => _mapping[packageName];
+  Uri getBase(String packageName) =>
+      packageName.isEmpty ? null : _mapping[packageName];
 
   Iterable<String> get packages => _mapping.keys;
 
   Map<String, Uri> asMap() => new UnmodifiableMapView<String, Uri>(_mapping);
+
+  String get defaultPackageName => _mapping[""]?.toString();
+
+  String packageMetadata(String packageName, String key) {
+    if (packageName.isEmpty) return null;
+    Uri uri = _mapping[packageName];
+    if (uri == null || !uri.hasFragment) return null;
+    // This can be optimized, either by caching the map or by
+    // parsing incrementally instead of parsing the entire fragment.
+    return Uri.splitQueryString(uri.fragment)[key];
+  }
+
+  String libraryMetadata(Uri libraryUri, String key) {
+    if (libraryUri.isScheme("package")) {
+      return packageMetadata(libraryUri.pathSegments.first, key);
+    }
+    var defaultPackageNameUri = _mapping[""];
+    if (defaultPackageNameUri != null) {
+      return packageMetadata(defaultPackageNameUri.toString(), key);
+    }
+    return null;
+  }
 }
 
 /// A [Packages] implementation based on a remote (e.g., HTTP) directory.
diff --git a/pubspec.yaml b/pubspec.yaml
index 4bad1cf..a69096d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: package_config
-version: 1.0.5
+version: 1.1.0-pre
 description: Support for working with Package Resolution config files.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/package_config
diff --git a/test/parse_test.dart b/test/parse_test.dart
index fb3a6fa..b9b1bb5 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -116,6 +116,82 @@
         () => doParse(singleRelativeSample * 2, base), throwsFormatException);
   });
 
+  test("disallow default package", () {
+    expect(() => doParse(":foo", base, allowDefaultPackage: false),
+        throwsFormatException);
+  });
+
+  test("allow default package", () {
+    var packages = doParse(":foo", base, allowDefaultPackage: true);
+    expect(packages.defaultPackageName, "foo");
+  });
+
+  test("allow default package name with dot", () {
+    var packages = doParse(":foo.bar", base, allowDefaultPackage: true);
+    expect(packages.defaultPackageName, "foo.bar");
+  });
+
+  test("not two default packages", () {
+    expect(() => doParse(":foo\n:bar", base, allowDefaultPackage: true),
+        throwsFormatException);
+  });
+
+  test("default package invalid package name", () {
+    // Not a valid *package name*.
+    expect(() => doParse(":foo/bar", base, allowDefaultPackage: true),
+        throwsFormatException);
+  });
+
+  group("metadata", () {
+    var packages = doParse(
+        ":foo\n"
+        "foo:foo#metafoo=1\n"
+        "bar:bar#metabar=2\n"
+        "baz:baz\n"
+        "qux:qux#metaqux1=3&metaqux2=4\n",
+        base,
+        allowDefaultPackage: true);
+    test("non-existing", () {
+      // non-package name.
+      expect(packages.packageMetadata("///", "f"), null);
+      expect(packages.packageMetadata("", "f"), null);
+      // unconfigured package name.
+      expect(packages.packageMetadata("absent", "f"), null);
+      // package name without that metadata
+      expect(packages.packageMetadata("foo", "notfoo"), null);
+    });
+    test("lookup", () {
+      expect(packages.packageMetadata("foo", "metafoo"), "1");
+      expect(packages.packageMetadata("bar", "metabar"), "2");
+      expect(packages.packageMetadata("qux", "metaqux1"), "3");
+      expect(packages.packageMetadata("qux", "metaqux2"), "4");
+    });
+    test("by library URI", () {
+      expect(
+          packages.libraryMetadata(
+              Uri.parse("package:foo/index.dart"), "metafoo"),
+          "1");
+      expect(
+          packages.libraryMetadata(
+              Uri.parse("package:bar/index.dart"), "metabar"),
+          "2");
+      expect(
+          packages.libraryMetadata(
+              Uri.parse("package:qux/index.dart"), "metaqux1"),
+          "3");
+      expect(
+          packages.libraryMetadata(
+              Uri.parse("package:qux/index.dart"), "metaqux2"),
+          "4");
+    });
+    test("by default package", () {
+      expect(
+          packages.libraryMetadata(
+              Uri.parse("file:///whatever.dart"), "metafoo"),
+          "1");
+    });
+  });
+
   for (String invalidSample in invalid) {
     test("invalid '$invalidSample'", () {
       var result;
@@ -130,8 +206,10 @@
   }
 }
 
-Packages doParse(String sample, Uri baseUri) {
-  Map<String, Uri> map = parse(sample.codeUnits, baseUri);
+Packages doParse(String sample, Uri baseUri,
+    {bool allowDefaultPackage = false}) {
+  Map<String, Uri> map = parse(sample.codeUnits, baseUri,
+      allowDefaultPackage: allowDefaultPackage);
   return new MapPackages(map);
 }