Add functions to help with --packages/--package-root command line parameters.

R=pquitslund@google.com, sgjesse@google.com

Review URL: https://codereview.chromium.org//1166443005.
diff --git a/pkgs/package_config/lib/discovery.dart b/pkgs/package_config/lib/discovery.dart
index 0dd7f55..35c3ece 100644
--- a/pkgs/package_config/lib/discovery.dart
+++ b/pkgs/package_config/lib/discovery.dart
@@ -15,6 +15,55 @@
 import "src/packages_impl.dart";
 import "src/packages_io_impl.dart";
 
+/// Reads a package resolution file and creates a [Packages] object from it.
+///
+/// The [packagesFile] must exist and be loadable.
+/// Currently that means the URI must have a `file`, `http` or `https` scheme,
+/// and that the file can be loaded and its contents parsed correctly.
+///
+/// If the [loader] is provided, it is used to fetch non-`file` URIs, and
+/// it can support other schemes or set up more complex HTTP requests.
+///
+/// This function can be used to load an explicitly configured package
+/// resolution file, for example one specified using a `--packages`
+/// command-line parameter.
+Future<Packages> loadPackagesFile(Uri packagesFile,
+                                  {Future<List<int>> loader(Uri uri)}) {
+  Packages parseBytes(List<int> bytes) {
+    Map<String, Uri> packageMap = pkgfile.parse(bytes, packagesFile);
+    return new MapPackages(packageMap);
+  }
+  if (packagesFile.scheme == "file") {
+    File file = new File.fromUri(packagesFile);
+    return file.readAsBytes().then(parseBytes);
+  }
+  if (loader == null) {
+    return http.readBytes(packagesFile).then(parseBytes);
+  }
+  return loader(packagesFile).then(parseBytes);
+}
+
+
+/// Create a [Packages] object for a package directory.
+///
+/// The [packagesDir] URI should refer to a directory.
+/// Package names are resolved as relative to sub-directories of the
+/// package directory.
+///
+/// This function can be used for explicitly configured package directories,
+/// for example one specified using a `--package-root` comand-line parameter.
+Packages getPackagesDirectory(Uri packagesDir) {
+  if (packagesDir.scheme == "file") {
+    Directory directory = new Directory.fromUri(packagesDir);
+    return new FilePackagesDirectoryPackages(directory);
+  }
+  if (!packagesDir.path.endsWith('/')) {
+    packagesDir = packagesDir.replace(path: packagesDir.path + '/');
+  }
+  return new NonFilePackagesDirectoryPackages(packagesDir);
+}
+
+
 /// Discover the package configuration for a Dart script.
 ///
 /// The [baseUri] points to either the Dart script or its directory.
@@ -36,19 +85,21 @@
 /// It needs to be able to load a `.packages` file from the URI, so only
 /// recognized schemes are accepted.
 ///
-/// To support other schemes, an optional [loader] function can be supplied.
-/// It's called to load the `.packages` file for any unsupported scheme.
-/// It must return the *contents* of the file identified by the URI it's given,
-/// which should be a UTF-8 encoded `.packages` file, and must return an
+/// To support other schemes, or more complex HTTP requests,
+/// an optional [loader] function can be supplied.
+/// It's called to load the `.packages` file for a non-`file` scheme.
+/// The loader function returns the *contents* of the file
+/// identified by the URI it's given.
+/// The content should be a UTF-8 encoded `.packages` file, and must return an
 /// error future if loading fails for any reason.
 Future<Packages> findPackages(Uri baseUri,
-    {Future<List<int>> loader(Uri unsupportedUri)}) {
+                              {Future<List<int>> loader(Uri unsupportedUri)}) {
   if (baseUri.scheme == "file") {
     return new Future<Packages>.sync(() => findPackagesFromFile(baseUri));
-  } else if (baseUri.scheme == "http" || baseUri.scheme == "https") {
-    return findPackagesFromNonFile(baseUri, loader: _httpGet);
   } else if (loader != null) {
     return findPackagesFromNonFile(baseUri, loader: loader);
+  } else if (baseUri.scheme == "http" || baseUri.scheme == "https") {
+    return findPackagesFromNonFile(baseUri, loader: http.readBytes);
   } else {
     return new Future<Packages>.value(Packages.noPackages);
   }
@@ -140,8 +191,8 @@
 /// of the requested `.packages` file as bytes, which will be assumed to be
 /// UTF-8 encoded.
 Future<Packages> findPackagesFromNonFile(Uri nonFileUri,
-    {Future<List<int>> loader(Uri name)}) {
-  if (loader == null) loader = _httpGet;
+                                         {Future<List<int>> loader(Uri name)}) {
+  if (loader == null) loader = http.readBytes;
   Uri packagesFileUri = nonFileUri.resolve(".packages");
   return loader(packagesFileUri).then((List<int> fileBytes) {
     Map<String, Uri> map = pkgfile.parse(fileBytes, packagesFileUri);
@@ -152,30 +203,3 @@
     return new NonFilePackagesDirectoryPackages(packagesDirectoryUri);
   });
 }
-
-/// Fetches a file over http.
-Future<List<int>> _httpGet(Uri uri) {
-  HttpClient client = new HttpClient();
-  return client
-      .getUrl(uri)
-      .then((HttpClientRequest request) => request.close())
-      .then((HttpClientResponse response) {
-    if (response.statusCode != HttpStatus.OK) {
-      String msg = 'Failure getting $uri: '
-          '${response.statusCode} ${response.reasonPhrase}';
-      throw msg;
-    }
-    return response.toList();
-  }).then((List<List<int>> splitContent) {
-    int totalLength = splitContent.fold(0, (int old, List list) {
-      return old + list.length;
-    });
-    Uint8List result = new Uint8List(totalLength);
-    int offset = 0;
-    for (List<int> contentPart in splitContent) {
-      result.setRange(offset, offset + contentPart.length, contentPart);
-      offset += contentPart.length;
-    }
-    return result;
-  });
-}
diff --git a/pkgs/package_config/test/discovery_test.dart b/pkgs/package_config/test/discovery_test.dart
index 16544e4..d89cc2f 100644
--- a/pkgs/package_config/test/discovery_test.dart
+++ b/pkgs/package_config/test/discovery_test.dart
@@ -205,6 +205,52 @@
                                              loader:loader);
     validatePackagesDir(resolver, location);
   });
+
+  generalTest("loadPackagesFile",
+              {".packages": packagesFile},
+              (Uri directory) async {
+    Uri file = directory.resolve(".packages");
+    Packages resolver = await loadPackagesFile(file);
+    validatePackagesFile(resolver, file);
+  });
+
+  generalTest("loadPackagesFile non-default name",
+              {"pheldagriff": packagesFile},
+              (Uri directory) async {
+    Uri file = directory.resolve("pheldagriff");
+    Packages resolver = await loadPackagesFile(file);
+    validatePackagesFile(resolver, file);
+  });
+
+  test("loadPackagesFile w/ loader", () async {
+    loader(Uri uri) async => packagesFile.codeUnits;
+    Uri file = Uri.parse("krutz://example.com/.packages");
+    Packages resolver = await loadPackagesFile(file, loader: loader);
+    validatePackagesFile(resolver, file);
+  });
+
+  generalTest("loadPackagesFile not found",
+               {},
+               (Uri directory) async {
+    Uri file = directory.resolve(".packages");
+    expect(loadPackagesFile(file), throws);
+  });
+
+  generalTest("loadPackagesFile syntax error",
+               {".packages": "syntax error"},
+               (Uri directory) async {
+    Uri file = directory.resolve(".packages");
+    expect(loadPackagesFile(file), throws);
+  });
+
+  generalTest("getPackagesDir",
+              {"packages": {"foo": {}, "bar": {}, "baz": {}}},
+              (Uri directory) async {
+    Uri packages = directory.resolve("packages/");
+    Packages resolver = getPackagesDirectory(packages);
+    Uri resolved = resolver.resolve(pkg("foo","flip/flop"));
+    expect(resolved, packages.resolve("foo/flip/flop"));
+  });
 }
 
 /// Create a directory structure from [description] and run [fileTest].
@@ -285,5 +331,3 @@
     }
   });
 }
-
-