Add package implementation.

See dart-lang/test#327

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//2132443003 .
diff --git a/lib/package_resolver.dart b/lib/package_resolver.dart
new file mode 100644
index 0000000..c0984d3
--- /dev/null
+++ b/lib/package_resolver.dart
@@ -0,0 +1,6 @@
+// Copyright (c) 2016, 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.
+
+export 'src/package_resolver.dart';
+export 'src/sync_package_resolver.dart';
diff --git a/lib/src/async_package_resolver.dart b/lib/src/async_package_resolver.dart
new file mode 100644
index 0000000..06c61e7
--- /dev/null
+++ b/lib/src/async_package_resolver.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2016, 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:async';
+
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+
+/// An implementation of [PackageResolver] that wraps a [SyncPackageResolver].
+class AsyncPackageResolver implements PackageResolver {
+  /// The wrapped [SyncPackageResolver].
+  final SyncPackageResolver _inner;
+
+  AsyncPackageResolver(this._inner);
+
+  Future<Map<String, Uri>> get packageConfigMap async =>
+      _inner.packageConfigMap;
+
+  Future<Uri> get packageConfigUri async => _inner.packageConfigUri;
+  Future<Uri> get packageRoot async => _inner.packageRoot;
+  Future<SyncPackageResolver> get asSync async => _inner;
+  Future<String> get processArgument async => _inner.processArgument;
+
+  Future<Uri> resolveUri(packageUri) async => _inner.resolveUri(packageUri);
+  Future<Uri> urlFor(String package, [String path]) async =>
+      _inner.urlFor(package, path);
+  Future<Uri> packageUriFor(url) async => _inner.packageUriFor(url);
+  Future<String> packagePath(String package) async =>
+      _inner.packagePath(package);
+}
diff --git a/lib/src/current_isolate_resolver.dart b/lib/src/current_isolate_resolver.dart
new file mode 100644
index 0000000..eb4dc9b
--- /dev/null
+++ b/lib/src/current_isolate_resolver.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2016, 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:async';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as p;
+
+import 'package_config_resolver.dart';
+import 'package_resolver.dart';
+import 'package_root_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// The package resolution strategy used by the current isolate.
+class CurrentIsolateResolver implements PackageResolver {
+  Future<Map<String, Uri>> get packageConfigMap async {
+    if (_packageConfigMap != null) return _packageConfigMap;
+
+    var url = await Isolate.packageConfig;
+    if (url == null) return null;
+
+    return await loadConfigMap(url);
+  }
+  Map<String, Uri> _packageConfigMap;
+
+  Future<Uri> get packageConfigUri => Isolate.packageConfig;
+
+  Future<Uri> get packageRoot => Isolate.packageRoot;
+
+  Future<SyncPackageResolver> get asSync async {
+    var root = await packageRoot;
+    if (root != null) return new PackageRootResolver(root);
+
+    var map = await packageConfigMap;
+
+    // It's hard to imagine how there would be no package resolution strategy
+    // for an Isolate that can load the package_resolver package, but it's easy
+    // to handle that case so we do.
+    if (map == null) return SyncPackageResolver.none;
+
+    return new PackageConfigResolver(map, uri: await packageConfigUri);
+  }
+
+  Future<String> get processArgument async {
+    var configUri = await packageConfigUri;
+    if (configUri != null) return "--packages=$configUri";
+
+    var root = await packageRoot;
+    if (root != null) return "--package-root=$root";
+
+    return null;
+  }
+
+  Future<Uri> resolveUri(packageUri) =>
+      Isolate.resolvePackageUri(asPackageUri(packageUri, "packageUri"));
+
+  Future<Uri> urlFor(String package, [String path]) =>
+      Isolate.resolvePackageUri(Uri.parse("package:$package/${path ?? ''}"));
+
+  Future<Uri> packageUriFor(url) async => (await asSync).packageUriFor(url);
+
+  Future<String> packagePath(String package) async {
+    var root = await packageRoot;
+    if (root != null) return new PackageRootResolver(root).packagePath(package);
+
+    return p.dirname(p.fromUri(await urlFor(package)));
+  }
+}
diff --git a/lib/src/no_package_resolver.dart b/lib/src/no_package_resolver.dart
new file mode 100644
index 0000000..9d324d4
--- /dev/null
+++ b/lib/src/no_package_resolver.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2016, 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 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy that is unable to resolve any `package:` URIs.
+class NoPackageResolver implements SyncPackageResolver {
+  Map<String, Uri> get packageConfigMap => null;
+  Uri get packageConfigUri => null;
+  Uri get packageRoot => null;
+  String get processArgument => null;
+
+  PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+  Uri resolveUri(packageUri) {
+    // Verify that the URI is valid.
+    asPackageUri(packageUri, "packageUri");
+    return null;
+  }
+
+  Uri urlFor(String package, [String path]) => null;
+
+  Uri packageUriFor(url) {
+    // Verify that the URI is a valid type.
+    asUri(url, "url");
+    return null;
+  }
+
+  String packagePath(String package) => null;
+}
diff --git a/lib/src/package_config_resolver.dart b/lib/src/package_config_resolver.dart
new file mode 100644
index 0000000..defd683
--- /dev/null
+++ b/lib/src/package_config_resolver.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2016, 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:collection';
+
+import 'package:collection/collection.dart';
+import 'package:package_config/packages_file.dart' as packages_file;
+import 'package:path/path.dart' as p;
+
+import 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy based on a package config map.
+class PackageConfigResolver implements SyncPackageResolver {
+  final packageRoot = null;
+
+  final Map<String, Uri> packageConfigMap;
+
+  Uri get packageConfigUri {
+    if (_uri != null) return _uri;
+
+    var buffer = new StringBuffer();
+    packages_file.write(buffer, packageConfigMap, comment: "");
+    _uri = new UriData.fromString(buffer.toString(),
+            parameters: {"charset": "utf-8"})
+        .uri;
+    return _uri;
+  }
+  Uri _uri;
+
+  PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+  String get processArgument => "--packages=$packageConfigUri";
+
+  PackageConfigResolver(Map<String, Uri> packageConfigMap, {uri})
+      : packageConfigMap = _normalizeMap(packageConfigMap),
+        _uri = uri == null ? null : asUri(uri, "uri");
+
+  /// Normalizes the URIs in [map] to ensure that they all end in a trailing
+  /// slash.
+  static Map<String, Uri> _normalizeMap(Map<String, Uri> map) =>
+      new UnmodifiableMapView(
+          mapMap(map, value: (_, uri) => ensureTrailingSlash(uri)));
+
+  Uri resolveUri(packageUri) {
+    var uri = asPackageUri(packageUri, "packageUri");
+
+    var baseUri = packageConfigMap[uri.pathSegments.first];
+    if (baseUri == null) return null;
+
+    var segments = baseUri.pathSegments.toList()
+      ..removeLast(); // Remove the trailing slash.
+
+    // Following [Isolate.resolvePackageUri], "package:foo" resolves to null.
+    if (uri.pathSegments.length == 1) return null;
+
+    segments.addAll(uri.pathSegments.skip(1));
+    return baseUri.replace(pathSegments: segments);        
+  }
+
+  Uri urlFor(String package, [String path]) {
+    var baseUri = packageConfigMap[package];
+    if (baseUri == null) return null;
+    if (path == null) return baseUri;
+    return baseUri.resolve(path);
+  }
+
+  Uri packageUriFor(url) {
+    url = asUri(url, "url").toString();
+
+    // Make sure isWithin works if [url] is exactly the base.
+    var nested = p.url.join(url, "_");
+    for (var package in packageConfigMap.keys) {
+      var base = packageConfigMap[package].toString();
+      if (!p.url.isWithin(base, nested)) continue;
+
+      var relative = p.url.relative(url, from: base);
+      if (relative == '.') relative = '';
+      return Uri.parse("package:$package/$relative");
+    }
+
+    return null;
+  }
+
+  String packagePath(String package) {
+    var lib = packageConfigMap[package];
+    if (lib == null) return null;
+    if (lib.scheme != 'file') return null;
+    return p.dirname(p.fromUri(lib));
+  }
+}
diff --git a/lib/src/package_resolver.dart b/lib/src/package_resolver.dart
new file mode 100644
index 0000000..c72fc9d
--- /dev/null
+++ b/lib/src/package_resolver.dart
@@ -0,0 +1,161 @@
+// Copyright (c) 2016, 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:async';
+
+import 'package:http/http.dart' as http;
+
+import 'current_isolate_resolver.dart';
+import 'package_config_resolver.dart';
+import 'package_root_resolver.dart';
+import 'sync_package_resolver.dart';
+
+/// A class that defines how to resolve `package:` URIs.
+///
+/// This includes the information necessary to resolve `package:` URIs using
+/// either a package config or a package root. It can be used both as a standard
+/// cross-package representation of the user's configuration, and as a means of
+/// concretely locating packages and the assets they contain.
+///
+/// Unlike [SyncPackageResolver], this does not provide synchronous APIs. This
+/// is necessary when dealing with the current Isolate's package resolution
+/// strategy.
+///
+/// This class should not be implemented by user code.
+abstract class PackageResolver {
+  /// The map contained in the parsed package config.
+  ///
+  /// This maps package names to the base URIs for those packages. These are
+  /// already resolved relative to [packageConfigUri], so if they're relative
+  /// they should be considered relative to [Uri.base].
+  ///
+  /// [urlFor] should generally be used rather than looking up package URLs in
+  /// this map, to ensure that code works with a package root as well as a
+  /// package config.
+  ///
+  /// Note that for some implementations, loading the map may require IO
+  /// operations that could fail.
+  ///
+  /// Completes to `null` when using a [packageRoot] for resolution, or when no
+  /// package resolution is being used.
+  Future<Map<String, Uri>> get packageConfigMap;
+
+  /// The URI for the package config.
+  ///
+  /// This is the URI from which [packageConfigMap] was parsed, if that's
+  /// available. Otherwise, it's a `data:` URI containing a serialized
+  /// representation of [packageConfigMap]. This `data:` URI should be accepted
+  /// by all Dart tools.
+  ///
+  /// Note that if this is a `data:` URI, it may not be safe to pass as a
+  /// parameter to a Dart process due to length limits.
+  ///
+  /// Completes to `null` when using a [packageRoot] for resolution, or when no
+  /// package resolution is being used.
+  Future<Uri> get packageConfigUri;
+
+  /// The base URL for resolving `package:` URLs.
+  ///
+  /// Completes to `null` when using a [packageConfigMap] for resolution, or
+  /// when no package resolution is being used.
+  Future<Uri> get packageRoot;
+
+  /// Fetches the package resolution for [this] and returns an object that
+  /// provides synchronous access.
+  ///
+  /// This may throw exceptions if loading or parsing the package map fails.
+  Future<SyncPackageResolver> get asSync;
+
+  /// Returns the argument to pass to a subprocess to get it to use this package
+  /// resolution strategy when resolving `package:` URIs.
+  ///
+  /// This uses the `--package-root` or `--package` flags, which are the
+  /// conventions supported by the Dart VM and dart2js.
+  ///
+  /// Note that if [packageConfigUri] is a `data:` URI, it may be too large to
+  /// pass on the command line.
+  ///
+  /// Returns `null` if no package resolution is in use.
+  Future<String> get processArgument;
+
+  /// Returns package resolution strategy describing how the current isolate
+  /// resolves `package:` URIs.
+  static final PackageResolver current = new CurrentIsolateResolver();
+
+  /// Returns a package resolution strategy that is unable to resolve any
+  /// `package:` URIs.
+  static final PackageResolver none = SyncPackageResolver.none.asAsync;
+
+  /// Loads a package config file from [uri] and returns its package resolution
+  /// strategy.
+  ///
+  /// This supports `file:`, `http:`, `data:` and `package:` URIs. It throws an
+  /// [UnsupportedError] for any other schemes. If [client] is passed and an
+  /// HTTP request is needed, it's used to make that request; otherwise, a
+  /// default client is used.
+  ///
+  /// [uri] may be a [String] or a [Uri].
+  static Future<PackageResolver> loadConfig(uri, {http.Client client}) async {
+    var resolver = await SyncPackageResolver.loadConfig(uri, client: client);
+    return resolver.asAsync;
+  }
+
+  /// Returns the package resolution strategy for the given [packageConfigMap].
+  ///
+  /// If passed, [uri] specifies the URI from which [packageConfigMap] was
+  /// loaded. It may be a [String] or a [Uri].
+  ///
+  /// Whether or not [uri] is passed, [packageConfigMap] is expected to be
+  /// fully-resolved. That is, any relative URIs in the original package config
+  /// source should be resolved relative to its location.
+  factory PackageResolver.config(Map<String, Uri> packageConfigMap, {uri}) =>
+      new PackageConfigResolver(packageConfigMap, uri: uri).asAsync;
+
+  /// Returns the package resolution strategy for the given [packageRoot], which
+  /// may be a [String] or a [Uri].
+  factory PackageResolver.root(packageRoot) =>
+      new PackageRootResolver(packageRoot).asAsync;
+
+  /// Resolves [packageUri] according to this package resolution strategy.
+  ///
+  /// [packageUri] may be a [String] or a [Uri]. This throws a [FormatException]
+  /// if [packageUri] isn't a `package:` URI or doesn't have at least one path
+  /// segment.
+  ///
+  /// If [packageUri] refers to a package that's not in the package spec, this
+  /// returns `null`.
+  Future<Uri> resolveUri(packageUri);
+
+  /// Returns the resolved URL for [package] and [path].
+  ///
+  /// This is equivalent to `resolveUri("package:$package/")` or
+  /// `resolveUri("package:$package/$path")`, depending on whether [path] was
+  /// passed.
+  ///
+  /// If [package] refers to a package that's not in the package spec, this
+  /// returns `null`.
+  Future<Uri> urlFor(String package, [String path]);
+
+  /// Returns the `package:` URI for [uri].
+  ///
+  /// If [uri] can't be referred to using a `package:` URI, returns `null`.
+  ///
+  /// [uri] may be a [String] or a [Uri].
+  Future<Uri> packageUriFor(uri);
+
+  /// Returns the path on the local filesystem to the root of [package], or
+  /// `null` if the root cannot be found.
+  ///
+  /// **Note**: this assumes a pub-style package layout. In particular:
+  ///
+  /// * If a package root is being used, this assumes that it contains symlinks
+  ///   to packages' lib/ directories.
+  ///
+  /// * If a package config is being used, this assumes that each entry points
+  ///   to a package's lib/ directory.
+  ///
+  /// Returns `null` if the package root is not a `file:` URI, or if the package
+  /// config entry for [package] is not a `file:` URI.
+  Future<String> packagePath(String package);
+}
diff --git a/lib/src/package_root_resolver.dart b/lib/src/package_root_resolver.dart
new file mode 100644
index 0000000..c9a06ff
--- /dev/null
+++ b/lib/src/package_root_resolver.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2016, 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:path/path.dart' as p;
+
+import 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy based on a package root URI.
+class PackageRootResolver implements SyncPackageResolver {
+  final packageConfigMap = null;
+  final packageConfigUri = null;
+
+  final Uri packageRoot;
+
+  PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+  String get processArgument => "--package-root=$packageRoot";
+
+  PackageRootResolver(packageRoot)
+      : packageRoot = ensureTrailingSlash(asUri(packageRoot, "packageRoot"));
+
+  Uri resolveUri(packageUri) {
+    packageUri = asPackageUri(packageUri, "packageUri");
+
+    // Following [Isolate.resolvePackageUri], "package:foo" resolves to null.
+    if (packageUri.pathSegments.length == 1) return null;
+    return packageRoot.resolve(packageUri.path);
+  }
+
+  Uri urlFor(String package, [String path]) {
+    var result = packageRoot.resolve("$package/");
+    return path == null ? result : result.resolve(path);
+  }
+
+  Uri packageUriFor(url) {
+    var packageRootString = packageRoot.toString();
+    url = asUri(url, "url").toString();
+    if (!p.url.isWithin(packageRootString, url)) return null;
+
+    var relative = p.url.relative(url, from: packageRootString);
+    if (!relative.contains("/")) relative += "/";
+    return Uri.parse("package:$relative");
+  }
+
+  String packagePath(String package) {
+    if (packageRoot.scheme != 'file') return null;
+
+    var libLink = p.join(p.fromUri(packageRoot), package);
+    if (!new Link(libLink).existsSync()) return null;
+
+    return p.dirname(new Link(libLink).resolveSymbolicLinksSync());
+  }
+}
diff --git a/lib/src/sync_package_resolver.dart b/lib/src/sync_package_resolver.dart
new file mode 100644
index 0000000..1c71dfd
--- /dev/null
+++ b/lib/src/sync_package_resolver.dart
@@ -0,0 +1,170 @@
+// Copyright (c) 2016, 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:async';
+
+import 'package:http/http.dart' as http;
+
+import 'no_package_resolver.dart';
+import 'package_config_resolver.dart';
+import 'package_resolver.dart';
+import 'package_root_resolver.dart';
+import 'utils.dart';
+
+/// A class that defines how to resolve `package:` URIs.
+///
+/// This includes the information necessary to resolve `package:` URIs using
+/// either a package config or a package root. It can be used both as a standard
+/// cross-package representation of the user's configuration, and as a means of
+/// concretely locating packages and the assets they contain.
+///
+/// Unlike [PackageResolver], all members on this are synchronous, which may
+/// require that more data be loaded up front. This is useful when primarily
+/// dealing with user-created package resolution strategies, whereas
+/// [PackageInfo] is usually preferable when the current Isolate's package
+/// resolution logic may be used.
+///
+/// This class should not be implemented by user code.
+abstract class SyncPackageResolver {
+  /// The map contained in the parsed package config.
+  ///
+  /// This maps package names to the base URIs for those packages. These are
+  /// already resolved relative to [packageConfigUri], so if they're relative
+  /// they should be considered relative to [Uri.base]. They're normalized to
+  /// ensure that all URLs end with a trailing slash.
+  ///
+  /// [urlFor] should generally be used rather than looking up package URLs in
+  /// this map, to ensure that code works with a package root as well as a
+  /// package config.
+  ///
+  /// Returns `null` when using a [packageRoot] for resolution, or when no
+  /// package resolution is being used.
+  Map<String, Uri> get packageConfigMap;
+
+  /// The URI for the package config.
+  ///
+  /// This is the URI from which [packageConfigMap] was parsed, if that's
+  /// available. Otherwise, it's a `data:` URI containing a serialized
+  /// representation of [packageConfigMap]. This `data:` URI should be accepted
+  /// by all Dart tools.
+  ///
+  /// Note that if this is a `data:` URI, it's likely not safe to pass as a
+  /// parameter to a Dart process due to length limits.
+  ///
+  /// Returns `null` when using a [packageRoot] for resolution, or when no
+  /// package resolution is being used.
+  Uri get packageConfigUri;
+
+  /// The base URL for resolving `package:` URLs.
+  ///
+  /// This is normalized so that it always ends with a trailing slash.
+  ///
+  /// Returns `null` when using a [packageConfigMap] for resolution, or when no
+  /// package resolution is being used.
+  Uri get packageRoot;
+
+  /// Returns a wrapper for [this] that implements [PackageResolver].
+  PackageResolver get asAsync;
+
+  /// Returns the argument to pass to a subprocess to get it to use this package
+  /// resolution strategy when resolving `package:` URIs.
+  ///
+  /// This uses the `--package-root` or `--package` flags, which are the
+  /// convention supported by the Dart VM and dart2js.
+  ///
+  /// Note that if [packageConfigUri] is a `data:` URI, it may be too large to
+  /// pass on the command line.
+  ///
+  /// Returns `null` if no package resolution is in use.
+  String get processArgument;
+
+  /// Returns a package resolution strategy describing how the current isolate
+  /// resolves `package:` URIs.
+  ///
+  /// This may throw exceptions if loading or parsing the isolate's package map
+  /// fails.
+  static final Future<SyncPackageResolver> current =
+      PackageResolver.current.asSync;
+
+  /// Returns a package resolution strategy that is unable to resolve any
+  /// `package:` URIs.
+  static final SyncPackageResolver none = new NoPackageResolver();
+
+  /// Loads a package config file from [uri] and returns its package resolution
+  /// strategy.
+  ///
+  /// This supports `file:`, `http:`, `data:` and `package:` URIs. It throws an
+  /// [UnsupportedError] for any other schemes. If [client] is passed and an
+  /// HTTP request is needed, it's used to make that request; otherwise, a
+  /// default client is used.
+  ///
+  /// [uri] may be a [String] or a [Uri].
+  static Future<SyncPackageResolver> loadConfig(uri, {http.Client client})
+      async {
+    uri = asUri(uri, "uri");
+    return new SyncPackageResolver.config(
+        await loadConfigMap(uri, client: client),
+        uri: uri);
+  }
+
+  /// Returns the package resolution strategy for the given [packageConfigMap].
+  ///
+  /// If passed, [uri] specifies the URI from which [packageConfigMap] was
+  /// loaded. It may be a [String] or a [Uri].
+  ///
+  /// Whether or not [uri] is passed, [packageConfigMap] is expected to be
+  /// fully-resolved. That is, any relative URIs in the original package config
+  /// source should be resolved relative to its location.
+  factory SyncPackageResolver.config(Map<String, Uri> packageConfigMap, {uri}) =
+      PackageConfigResolver;
+
+  /// Returns the package resolution strategy for the given [packageRoot], which
+  /// may be a [String] or a [Uri].
+  factory SyncPackageResolver.root(packageRoot) = PackageRootResolver;
+
+  /// Resolves [packageUri] according to this package resolution strategy.
+  ///
+  /// [packageUri] may be a [String] or a [Uri]. This throws a [FormatException]
+  /// if [packageUri] isn't a `package:` URI or doesn't have at least one path
+  /// segment.
+  ///
+  /// If [packageUri] refers to a package that's not in the package spec, this
+  /// returns `null`.
+  Uri resolveUri(packageUri);
+
+  /// Returns the resolved URL for [package] and [path].
+  ///
+  /// This is equivalent to `resolveUri("package:$package/")` or
+  /// `resolveUri("package:$package/$path")`, depending on whether [path] was
+  /// passed.
+  ///
+  /// If [package] refers to a package that's not in the package spec, this
+  /// returns `null`.
+  Uri urlFor(String package, [String path]);
+
+  /// Returns the `package:` URI for [url].
+  ///
+  /// If [url] can't be referred to using a `package:` URI, returns `null`.
+  ///
+  /// [url] may be a [String] or a [Uri].
+  Uri packageUriFor(url);
+
+  /// Returns the path on the local filesystem to the root of [package], or
+  /// `null` if the root cannot be found.
+  ///
+  /// **Note**: this assumes a pub-style package layout. In particular:
+  ///
+  /// * If a package root is being used, this assumes that it contains symlinks
+  ///   to packages' lib/ directories.
+  ///
+  /// * If a package config is being used, this assumes that each entry points
+  ///   to a package's lib/ directory.
+  ///
+  /// If these assumptions are broken, this may return `null` or it may return
+  /// an invalid result.
+  ///
+  /// Returns `null` if the package root is not a `file:` URI, or if the package
+  /// config entry for [package] is not a `file:` URI.
+  String packagePath(String package);
+}
diff --git a/lib/src/test_package_config b/lib/src/test_package_config
new file mode 100644
index 0000000..31aaa5e
--- /dev/null
+++ b/lib/src/test_package_config
@@ -0,0 +1,3 @@
+# This needs to be in src/ so the tests can access it using a package: URI.
+foo:file:///foo/bar/
+bar:http://dartlang.org/bar/
\ No newline at end of file
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..77de03d
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2016, 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.
+
+// TODO(nweiz): Avoid importing dart:io directly when cross-platform libraries
+// exist.
+import 'dart:io';
+import 'dart:isolate';
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:package_config/packages_file.dart' as packages_file;
+
+/// Loads the configuration map from [uri].
+///
+/// This supports `http`, `file`, `data`, and `package` URIs. If [client] is
+/// passed and an HTTP request is needed, it's used to make that request;
+/// otherwise, a default client is used.
+Future<Map<String, Uri>> loadConfigMap(Uri uri, {http.Client client}) async {
+  var resolved = Uri.base.resolveUri(uri);
+
+  var text;
+  if (resolved.scheme == 'http') {
+    text = await (client == null
+        ? http.read(resolved)
+        : client.read(resolved));
+  } else if (resolved.scheme == 'file') {
+    var path = resolved.toFilePath(windows: Platform.isWindows);
+    text = await new File(path).readAsString();
+  } else if (resolved.scheme == 'data') {
+    text = resolved.data.contentAsString();
+  } else if (resolved.scheme == 'package') {
+    return loadConfigMap(await Isolate.resolvePackageUri(uri),
+        client: client);
+  } else {
+    throw new UnsupportedError(
+        'PackageInfo.loadConfig doesn\'t support URI scheme "${uri.scheme}:".');
+  }
+
+  return packages_file.parse(UTF8.encode(text), resolved);
+}
+
+/// Converts [uri] to a [Uri] and verifies that it's a valid `package:` URI.
+///
+/// Throws an [ArgumentError] if [uri] isn't a [String] or a [Uri]. [name] is
+/// used as the argument name in that error.
+///
+/// Throws a [FormatException] if [uri] isn't a `package:` URI or doesn't have
+/// at least one path segment.
+Uri asPackageUri(uri, String name) {
+  uri = asUri(uri, name);
+
+  if (uri.scheme != 'package') {
+    throw new FormatException("Can only resolve a package: URI.",
+        uri.toString(), 0);
+  } else if (uri.pathSegments.isEmpty) {
+    throw new FormatException("Expected package name.",
+        uri.toString(), "package:".length);
+  }
+
+  return uri;
+}
+
+/// Converts [uri] to a [Uri].
+///
+/// Throws an [ArgumentError] if [uri] isn't a [String] or a [Uri]. [name] is
+/// used as the argument name in that error.
+Uri asUri(uri, String name) {
+  if (uri is Uri) return uri;
+  if (uri is String) return Uri.parse(uri);
+
+  throw new ArgumentError.value(uri, name, "Must be a String or a Uri.");
+}
+
+/// Returns a copy of [uri] with a trailing slash.
+///
+/// If [uri] already ends in a slash, returns it as-is.
+Uri ensureTrailingSlash(Uri uri) {
+  if (uri.pathSegments.last.isEmpty) return uri;
+  return uri.replace(pathSegments: uri.pathSegments.toList()..add(""));
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 751eb44..d33cd97 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,5 +7,11 @@
 environment:
   sdk: '>=1.14.0 <2.0.0'
 
+dependencies:
+  collection: '^1.9.0'
+  http: '^0.11.0'
+  package_config: '>=0.1.0 <2.0.0'
+  path: '^1.0.0'
 dev_dependencies:
+  shelf: '^0.6.0'
   test: '^0.12.0'
diff --git a/test/current_isolate_info_test.dart b/test/current_isolate_info_test.dart
new file mode 100644
index 0000000..bb6f4f7
--- /dev/null
+++ b/test/current_isolate_info_test.dart
@@ -0,0 +1,233 @@
+// Copyright (c) 2016, 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:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+  // It's important to test these, because they use PackageConfig.current and
+  // they're used to verify the output of the inner isolate's
+  // PackageConfig.current.
+  test("_packageResolverLibUri is correct", () async {
+    var libPath = p.fromUri(await _packageResolverLibUri);
+    expect(new File(p.join(libPath, 'package_resolver.dart')).exists(),
+        completion(isTrue));
+  });
+
+  test("_pathLibUri is correct", () async {
+    var libPath = p.fromUri(await _pathLibUri);
+    expect(new File(p.join(libPath, 'path.dart')).exists(), completion(isTrue));
+  });
+
+  group("with a package config", () {
+    var resolver;
+    setUp(() async {
+      var map;
+      var currentIsolateMap = await PackageResolver.current.packageConfigMap;
+      if (currentIsolateMap != null) {
+        map = new Map.from(currentIsolateMap);
+      } else {
+        // If the isolate running this test isn't using package config, create
+        // one from scratch with the same resolution semantics.
+        var map = {};
+        var root = p.fromUri(await PackageResolver.current.packageRoot);
+        await for (var link in new Directory(root).list(followLinks: false)) {
+          assert(link is Link);
+          map[p.basename(link.path)] =
+              p.toUri(await link.resolveSymbolicLinks());
+        }
+      }
+
+      // Ensure that we have at least one URI that ends with "/" and one that
+      // doesn't. Both of these cases need to be tested.
+      expect(map["package_resolver"].path, endsWith("/"));
+      map["path"] = Uri.parse(p.url.normalize(map["path"].toString()));
+      expect(map["path"].path, isNot(endsWith("/")));
+
+      resolver = new PackageResolver.config(map);
+    });
+
+    test("exposes the config map", () async {
+      expect(await _spawn("""() async {
+        var serializable = {};
+        (await PackageResolver.current.packageConfigMap)
+            .forEach((package, uri) {
+          serializable[package] = uri.toString();
+        });
+        return serializable;
+      }()""", resolver),
+          containsPair("package_resolver", await _packageResolverLibUri));
+    });
+
+    test("exposes the config URI", () async {
+      expect(
+          await _spawn(
+              "(await PackageResolver.current.packageConfigUri).toString()",
+              resolver),
+          equals((await resolver.packageConfigUri).toString()));
+    });
+
+    test("exposes a null package root", () async {
+      expect(
+          // Use "== null" because if it *is* a URI, it'll crash the isolate
+          // when we try to send it over the port.
+          await _spawn(
+              "(await PackageResolver.current.packageRoot) == null", resolver),
+          isTrue);
+    });
+
+    test("processArgument uses --packages", () async {
+      expect(
+          await _spawn("PackageResolver.current.processArgument", resolver),
+          equals(await resolver.processArgument));
+    });
+
+    group("resolveUri", () {
+      test("with a matching package", () async {
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              'package:package_resolver/foo/bar.dart');
+          return uri.toString();
+        }()""", resolver),
+            equals(p.url.join(await _packageResolverLibUri, "foo/bar.dart")));
+
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              'package:path/foo/bar.dart');
+          return uri.toString();
+        }()""", resolver),
+            equals(p.url.join(await _pathLibUri, "foo/bar.dart")));
+      });
+
+      test("with a matching package with no path", () async {
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              'package:package_resolver');
+          return uri == null;
+        }()""", resolver), isTrue);
+
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri('package:path');
+          return uri == null;
+        }()""", resolver), isTrue);
+      });
+
+      test("with a matching package with an empty path",
+          () async {
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              'package:package_resolver/');
+          return uri.toString();
+        }()""", resolver), (await _packageResolverLibUri).toString());
+
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri('package:path/');
+          return uri.toString();
+        }()""", resolver), (await _pathLibUri).toString());
+      });
+
+      test("with a URI object", () async {
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              Uri.parse('package:package_resolver/foo/bar.dart'));
+          return uri.toString();
+        }()""", resolver),
+            equals(p.url.join(await _packageResolverLibUri, 'foo/bar.dart')));
+      });
+
+      test("with a non-matching package", () async {
+        expect(await _spawn("""() async {
+          var uri = await PackageResolver.current.resolveUri(
+              Uri.parse('package:not-a-package/foo/bar.dart'));
+          return uri == null;
+        }()""", resolver), isTrue);
+      });
+
+      test("with an invalid argument type", () async {
+        expect(await _spawn("""() async {
+          try {
+            await PackageResolver.current.resolveUri(12);
+            return false;
+          } on ArgumentError catch (_) {
+            return true;
+          }
+        }()""", resolver), isTrue);
+      });
+
+      test("with a non-package URI", () async {
+        expect(await _spawn("""() async {
+          try {
+            await PackageResolver.current.resolveUri('file:///zip/zap');
+            return false;
+          } on FormatException catch (_) {
+            return true;
+          }
+        }()""", resolver), isTrue);
+      });
+
+      test("with an invalid package URI", () async {
+        expect(await _spawn("""() async {
+          try {
+            await PackageResolver.current.resolveUri("package:");
+            return false;
+          } on FormatException catch (_) {
+            return true;
+          }
+        }()""", resolver), isTrue);
+      });
+    });
+  });
+}
+
+Future<String> get _packageResolverLibUri async =>
+    (await PackageResolver.current.urlFor("package_resolver")).toString();
+
+Future<String> get _pathLibUri async =>
+    (await PackageResolver.current.urlFor("path")).toString();
+
+Future _spawn(String expression, PackageResolver packageResolver) async {
+  var data = new UriData.fromString("""
+    import 'dart:convert';
+    import 'dart:isolate';
+
+    import 'package:package_resolver/package_resolver.dart';
+
+    main(_, SendPort message) async {
+      message.send(await ($expression));
+    }
+  """, mimeType: "application/dart", parameters: {"charset": "utf-8"});
+
+  var receivePort = new ReceivePort();
+  var errorPort = new ReceivePort();
+  try {
+    var isolate = await Isolate.spawnUri(data.uri, [], receivePort.sendPort,
+        packageRoot: await packageResolver.packageRoot,
+        packageConfig: await packageResolver.packageConfigUri,
+        paused: true);
+
+    isolate.addErrorListener(errorPort.sendPort);
+    errorPort.listen((message) {
+      registerException(message[0],
+          message[1] == null ? null : new Trace.parse(message[1]));
+      errorPort.close();
+      receivePort.close();
+    });
+    isolate.resume(isolate.pauseCapability);
+
+    var value = await receivePort.first;
+    isolate.kill();
+    return value;
+  } finally {
+    errorPort.close();
+    receivePort.close();
+  }
+}
diff --git a/test/no_package_info_test.dart b/test/no_package_info_test.dart
new file mode 100644
index 0000000..fddb011
--- /dev/null
+++ b/test/no_package_info_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2016, 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 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+  var resolver;
+  setUp(() {
+    resolver = SyncPackageResolver.none;
+  });
+
+  test("exposes everything as null", () {
+    expect(resolver.packageConfigMap, isNull);
+    expect(resolver.packageConfigUri, isNull);
+    expect(resolver.packageRoot, isNull);
+    expect(resolver.processArgument, isNull);
+    expect(resolver.resolveUri("package:foo/bar.dart"), isNull);
+    expect(resolver.urlFor("foo"), isNull);
+    expect(resolver.urlFor("foo", "bar.dart"), isNull);
+    expect(resolver.packageUriFor("file:///foo/bar.dart"), isNull);
+    expect(resolver.packagePath("foo"), isNull);
+  });
+
+  group("resolveUri", () {
+    test("with an invalid argument type", () {
+      expect(() => resolver.resolveUri(12), throwsArgumentError);
+    });
+
+    test("with a non-package URI", () {
+      expect(() => resolver.resolveUri("file:///zip/zap"),
+          throwsFormatException);
+    });
+
+    test("with an invalid package URI", () {
+      expect(() => resolver.resolveUri("package:"), throwsFormatException);
+    });
+  });
+
+  test("packageUriFor with an invalid argument type", () {
+    expect(() => resolver.packageUriFor(12), throwsArgumentError);
+  });
+}
diff --git a/test/package_config_info_test.dart b/test/package_config_info_test.dart
new file mode 100644
index 0000000..5dcc417
--- /dev/null
+++ b/test/package_config_info_test.dart
@@ -0,0 +1,249 @@
+// Copyright (c) 2016, 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:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+  var resolver;
+  setUp(() {
+    resolver = new SyncPackageResolver.config({
+      "foo": Uri.parse("file:///foo/bar/"),
+      "bar": Uri.parse("http://dartlang.org/bar")
+    }, uri: "file:///myapp/.packages");
+  });
+
+  group("constructor", () {
+    test("with a URI object", () {
+      var resolver = new SyncPackageResolver.config({},
+          uri: Uri.parse("file:///myapp/.packages"));
+      expect(resolver.packageConfigUri,
+          equals(Uri.parse("file:///myapp/.packages")));
+    });
+
+    test("with an invalid URI type", () {
+      expect(() => new SyncPackageResolver.config({}, uri: 12),
+          throwsArgumentError);
+    });
+  });
+
+  test("exposes the config map", () {
+    expect(resolver.packageConfigMap, equals({
+      "foo": Uri.parse("file:///foo/bar/"),
+      "bar": Uri.parse("http://dartlang.org/bar/")
+    }));
+  });
+
+  test("exposes the config URI if passed", () {
+    expect(resolver.packageConfigUri,
+        equals(Uri.parse("file:///myapp/.packages")));
+  });
+
+  test("exposes a data: config URI if none is passed", () {
+    resolver = new SyncPackageResolver.config(resolver.packageConfigMap);
+    expect(resolver.packageConfigUri, equals(Uri.parse(
+        "data:;charset=utf-8,"
+        "foo:file:///foo/bar/%0A"
+        "bar:http://dartlang.org/bar/%0A")));
+  });
+
+  test("exposes a null root", () {
+    expect(resolver.packageRoot, isNull);
+  });
+
+  test("processArgument uses --packages", () {
+    expect(resolver.processArgument,
+        equals("--packages=file:///myapp/.packages"));
+  });
+
+  group("resolveUri", () {
+    test("with a matching package", () {
+      expect(resolver.resolveUri("package:foo/bang/qux.dart"),
+          equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+      expect(resolver.resolveUri("package:bar/bang/qux.dart"),
+          equals(Uri.parse("http://dartlang.org/bar/bang/qux.dart")));
+    });
+
+    test("with a matching package with no path", () {
+      expect(resolver.resolveUri("package:foo"), isNull);
+    });
+
+    test("with a matching package with an empty path", () {
+      expect(resolver.resolveUri("package:bar/"),
+          equals(Uri.parse("http://dartlang.org/bar/")));
+    });
+
+    test("with a URI object", () {
+      expect(resolver.resolveUri(Uri.parse("package:foo/bang/qux.dart")),
+          equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+    });
+
+    test("with a non-matching package", () {
+      expect(resolver.resolveUri("package:zap/bang/qux.dart"), isNull);
+    });
+
+    test("with an invalid argument type", () {
+      expect(() => resolver.resolveUri(12), throwsArgumentError);
+    });
+
+    test("with a non-package URI", () {
+      expect(() => resolver.resolveUri("file:///zip/zap"),
+          throwsFormatException);
+    });
+
+    test("with an invalid package URI", () {
+      expect(() => resolver.resolveUri("package:"), throwsFormatException);
+    });
+  });
+
+  group("urlFor", () {
+    test("with a matching package and no path", () {
+      expect(resolver.urlFor("foo"), equals(Uri.parse("file:///foo/bar/")));
+      expect(resolver.urlFor("bar"),
+          equals(Uri.parse("http://dartlang.org/bar/")));
+    });
+
+    test("with a matching package and a path", () {
+      expect(resolver.urlFor("foo", "bang/qux.dart"),
+          equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+      expect(resolver.urlFor("bar", "bang/qux.dart"),
+          equals(Uri.parse("http://dartlang.org/bar/bang/qux.dart")));
+    });
+
+    test("with a non-matching package and no path", () {
+      expect(resolver.urlFor("zap"), isNull);
+    });
+  });
+
+  group("packageUriFor", () {
+    test("converts matching URIs to package:", () {
+      expect(resolver.packageUriFor("file:///foo/bar/bang/qux.dart"),
+          equals(Uri.parse("package:foo/bang/qux.dart")));
+      expect(resolver.packageUriFor("http://dartlang.org/bar/bang/qux.dart"),
+          equals(Uri.parse("package:bar/bang/qux.dart")));
+    });
+
+    test("converts URIs with no paths", () {
+      expect(resolver.packageUriFor("file:///foo/bar"),
+          equals(Uri.parse("package:foo/")));
+      expect(resolver.packageUriFor("http://dartlang.org/bar/"),
+          equals(Uri.parse("package:bar/")));
+    });
+
+    test("with a URI object", () {
+      expect(resolver.packageUriFor(Uri.parse("file:///foo/bar/bang/qux.dart")),
+          equals(Uri.parse("package:foo/bang/qux.dart")));
+    });
+
+    test("with an invalid argument type", () {
+      expect(() => resolver.packageUriFor(12), throwsArgumentError);
+    });
+  });
+
+  group("packagePath", () {
+    setUp(() {
+      resolver = new SyncPackageResolver.config({
+        "foo": p.toUri(p.join(p.current, 'lib')),
+        "bar": Uri.parse("http://dartlang.org/bar")
+      });
+    });
+
+    test("with a matching package", () {
+      expect(resolver.packagePath("foo"), equals(p.current));
+    });
+
+    test("with a package with a non-file scheme", () {
+      expect(resolver.packagePath("bar"), isNull);
+    });
+
+    test("with a non-matching", () {
+      expect(resolver.packagePath("baz"), isNull);
+    });
+  });
+
+  group("loadConfig", () {
+    var server;
+    var sandbox;
+    setUp(() async {
+      sandbox = (await Directory.systemTemp.createTemp("package_resolver_test"))
+          .path;
+    });
+
+    tearDown(() async {
+      if (server != null) await server.close();
+      await new Directory(sandbox).delete(recursive: true);
+    });
+
+    test("with an http: URI", () async {
+      server = await shelf_io.serve((request) {
+        return new shelf.Response.ok(
+            "foo:file:///foo/bar/\n"
+            "bar:http://dartlang.org/bar/");
+      }, 'localhost', 0);
+
+      var resolver = await SyncPackageResolver.loadConfig(
+          "http://localhost:${server.port}");
+
+      expect(resolver.packageConfigMap, equals({
+        "foo": Uri.parse("file:///foo/bar/"),
+        "bar": Uri.parse("http://dartlang.org/bar/")
+      }));
+      expect(resolver.packageConfigUri,
+          equals(Uri.parse("http://localhost:${server.port}")));
+    });
+
+    test("with a file: URI", () async {
+      var packagesPath = p.join(sandbox, ".packages");
+      new File(packagesPath).writeAsStringSync(
+          "foo:file:///foo/bar/\n"
+          "bar:http://dartlang.org/bar/");
+
+      var resolver =
+          await SyncPackageResolver.loadConfig(p.toUri(packagesPath));
+
+      expect(resolver.packageConfigMap, equals({
+        "foo": Uri.parse("file:///foo/bar/"),
+        "bar": Uri.parse("http://dartlang.org/bar/")
+      }));
+      expect(resolver.packageConfigUri, equals(p.toUri(packagesPath)));
+    });
+
+    test("with a data: URI", () async {
+      var data = Uri.parse(
+          "data:;charset=utf-8,"
+          "foo:file:///foo/bar/%0A"
+          "bar:http://dartlang.org/bar/%0A");
+      var resolver = await SyncPackageResolver.loadConfig(data);
+
+      expect(resolver.packageConfigMap, equals({
+        "foo": Uri.parse("file:///foo/bar/"),
+        "bar": Uri.parse("http://dartlang.org/bar/")
+      }));
+      expect(resolver.packageConfigUri, equals(data));
+    });
+
+    test("with a package: URI", () async {
+      var resolver = await SyncPackageResolver.loadConfig(
+          "package:package_resolver/src/test_package_config");
+
+      expect(resolver.packageConfigMap, equals({
+        "foo": Uri.parse("file:///foo/bar/"),
+        "bar": Uri.parse("http://dartlang.org/bar/")
+      }));
+      expect(resolver.packageConfigUri, equals(Uri.parse(
+          "package:package_resolver/src/test_package_config")));
+    });
+
+    test("with an unsupported scheme", () {
+      expect(SyncPackageResolver.loadConfig("asdf:foo/bar"),
+          throwsUnsupportedError);
+    });
+  });
+}
diff --git a/test/package_root_info_test.dart b/test/package_root_info_test.dart
new file mode 100644
index 0000000..c0edba6
--- /dev/null
+++ b/test/package_root_info_test.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2016, 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:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+  var resolver;
+  setUp(() {
+    resolver = new SyncPackageResolver.root("file:///foo/bar");
+  });
+
+  group("constructor", () {
+    test("with a URI object", () {
+      var resolver =
+          new SyncPackageResolver.root(Uri.parse("file:///foo/bar/"));
+      expect(resolver.packageRoot, equals(Uri.parse("file:///foo/bar/")));
+    });
+
+    test("with an invalid URI type", () {
+      expect(() => new SyncPackageResolver.root(12), throwsArgumentError);
+    });
+  });
+
+  test("exposes a null config map", () {
+    expect(resolver.packageConfigMap, isNull);
+  });
+
+  test("exposes a null config URI", () {
+    expect(resolver.packageConfigUri, isNull);
+  });
+
+  test("exposes the root root", () {
+    expect(resolver.packageRoot, equals(Uri.parse("file:///foo/bar/")));
+  });
+
+  test("processArgument uses --package-root", () {
+    expect(resolver.processArgument, equals("--package-root=file:///foo/bar/"));
+  });
+
+  group("resolveUri", () {
+    test("with a package", () {
+      expect(resolver.resolveUri("package:baz/bang/qux.dart"),
+          equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+    });
+
+    test("with a package with no path", () {
+      expect(resolver.resolveUri("package:baz"), isNull);
+    });
+
+    test("with a package with an empty path", () {
+      expect(resolver.resolveUri("package:baz/"),
+          equals(Uri.parse("file:///foo/bar/baz/")));
+    });
+
+    test("with a URI object", () {
+      expect(resolver.resolveUri(Uri.parse("package:baz/bang/qux.dart")),
+          equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+    });
+
+    test("with an invalid argument type", () {
+      expect(() => resolver.resolveUri(12), throwsArgumentError);
+    });
+
+    test("with a non-package URI", () {
+      expect(() => resolver.resolveUri("file:///zip/zap"),
+          throwsFormatException);
+    });
+
+    test("with an invalid package URI", () {
+      expect(() => resolver.resolveUri("package:"), throwsFormatException);
+    });
+  });
+
+  group("urlFor", () {
+    test("with no path", () {
+      expect(resolver.urlFor("baz"), equals(Uri.parse("file:///foo/bar/baz/")));
+    });
+
+    test("with a path", () {
+      expect(resolver.urlFor("baz", "bang/qux.dart"),
+          equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+    });
+  });
+
+  group("packageUriFor", () {
+    test("converts a matching URI to a package:", () {
+      expect(resolver.packageUriFor("file:///foo/bar/bang/qux.dart"),
+          equals(Uri.parse("package:bang/qux.dart")));
+    });
+
+    test("converts a matching URI with no path", () {
+      expect(resolver.packageUriFor("file:///foo/bar/baz"),
+          equals(Uri.parse("package:baz/")));
+      expect(resolver.packageUriFor("file:///foo/bar/baz/"),
+          equals(Uri.parse("package:baz/")));
+    });
+
+    test("with a URI object", () {
+      expect(resolver.packageUriFor(Uri.parse("file:///foo/bar/bang/qux.dart")),
+          equals(Uri.parse("package:bang/qux.dart")));
+    });
+
+    test("with an invalid argument type", () {
+      expect(() => resolver.packageUriFor(12), throwsArgumentError);
+    });
+  });
+
+  group("packagePath", () {
+    var sandbox;
+    setUp(() async {
+      sandbox = (await Directory.systemTemp.createTemp("package_resolver_test"))
+          .path;
+    });
+
+    tearDown(() async {
+      await new Directory(sandbox).delete(recursive: true);
+    });
+
+    test("with a file: scheme", () async {
+      var packageLib = p.join(sandbox, "foo/lib");
+      await new Directory(packageLib).create(recursive: true);
+
+      var packagesDir = p.join(sandbox, "packages");
+      var fooLink = p.join(packagesDir, "foo");
+      await new Link(fooLink).create(packageLib, recursive: true);
+
+      var packagesLink = p.join(sandbox, "foo/packages");
+      await new Link(packagesLink).create(packagesDir);
+
+      var resolver = new SyncPackageResolver.root(p.toUri(packagesLink));
+
+      expect(resolver.packagePath("foo"), equals(p.join(sandbox, "foo")));
+      expect(resolver.packagePath("bar"), isNull);
+    });
+
+    test("without a file: scheme", () {
+      var resolver = new SyncPackageResolver.root("http://dartlang.org/bar");
+      expect(resolver.packagePath("foo"), isNull);
+    });
+  });
+}