Rename "pathos" package to "path".

R=ajohnsen@google.com

Review URL: https://codereview.chromium.org//18356011

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/path@24964 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/README.md b/README.md
index 66f40b1..dacacaa 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
 A comprehensive, cross-platform path manipulation library for Dart.
 
-The pathos library provides common operations for manipulating file paths:
+The path package provides common operations for manipulating file paths:
 joining, splitting, normalizing, etc.
 
 We've tried very hard to make this library do the "right" thing on whatever
-platform you run it on. When you use the top-level functions, it will assume
-the host OS's path style and work with that. If you want to specifically work
-with paths of a specific style, you can construct a `path.Builder` for that
+platform you run it on. When you use the top-level functions, it will assume the
+current platform's path style and work with that. If you want to specifically
+work with paths of a specific style, you can construct a `path.Builder` for that
 style.
 
 ## Using
@@ -14,22 +14,24 @@
 The path library was designed to be imported with a prefix, though you don't
 have to if you don't want to:
 
-    import 'package:pathos/path.dart' as path;
+    import 'package:path/path.dart' as path; // TODO(bob): ???
 
 ## Top-level functions
 
 The most common way to use the library is through the top-level functions.
 These manipulate path strings based on your current working directory and the
-path style (POSIX or Windows) of the host operating system.
+path style (POSIX, Windows, or URLs) of the host platform.
 
 ### String get current
 
-Gets the path to the current working directory.
+Gets the path to the current working directory. In the browser, this means the
+current URL. When using dart2js, this currently returns `.` due to technical
+constraints. In the future, it will return the current URL.
 
 ### String get separator
 
-Gets the path separator for the current platform. On Mac and Linux, this
-is `/`. On Windows, it's `\`.
+Gets the path separator for the current platform. On Mac, Linux, and the
+browser, this is `/`. On Windows, it's `\`.
 
 ### String absolute(String path)
 
@@ -100,12 +102,23 @@
     path.rootPrefix(r'path\to\foo'); // -> ''
     path.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
 
+    // URL
+    path.rootPrefix('path/to/foo'); // -> ''
+    path.rootPrefix('http://dartlang.org/path/to/foo');
+      // -> 'http://dartlang.org'
+
 ### bool isAbsolute(String path)
 
-Returns `true` if [path] is an absolute path and `false` if it is a
-relative path. On POSIX systems, absolute paths start with a `/` (forward
-slash). On Windows, an absolute path starts with `\\`, or a drive letter
-followed by `:/` or `:\`.
+Returns `true` if [path] is an absolute path and `false` if it is a relative
+path. On POSIX systems, absolute paths start with a `/` (forward slash). On
+Windows, an absolute path starts with `\\`, or a drive letter followed by `:/`
+or `:\`. For URLs, absolute paths either start with a protocol and optional
+hostname (e.g. `http://dartlang.org`, `file://`) or with a `/`.
+
+URLs that start with `/` are known as "root-relative", since they're relative to
+the root of the current URL. Since root-relative paths are still absolute in
+every other sense, [isAbsolute] will return true for them. They can be detected
+using [isRootRelative].
 
 ### bool isRelative(String path)
 
@@ -114,6 +127,16 @@
 Windows, an absolute path starts with `\\`, or a drive letter followed by
 `:/` or `:\`.
 
+### bool isRootRelative(String path)
+
+Returns `true` if [path] is a root-relative path and `false` if it's not. URLs
+that start with `/` are known as "root-relative", since they're relative to the
+root of the current URL. Since root-relative paths are still absolute in every
+other sense, [isAbsolute] will return true for them. They can be detected using
+[isRootRelative].
+
+No POSIX and Windows paths are root-relative.
+
 ### String join(String part1, [String part2, String part3, ...])
 
 Joins the given path parts into a single path using the current platform's
@@ -149,6 +172,10 @@
     // Windows
     path.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo']
 
+    // Browser
+    path.split('http://dartlang.org/path/to/foo');
+      // -> ['http://dartlang.org', 'path', 'to', 'foo']
+
 ### String normalize(String path)
 
 Normalizes [path], simplifying it by handling `..`, and `.`, and
@@ -176,14 +203,57 @@
 Since there is no relative path from one drive letter to another on Windows,
 this will return an absolute path in that case.
 
+    // Windows
     path.relative(r'D:\other', from: r'C:\home'); // -> 'D:\other'
 
+    // URL
+    path.relative('http://dartlang.org', from: 'http://pub.dartlang.org');
+      // -> 'http://dartlang.org'
+
 ### String withoutExtension(String path)
 
 Removes a trailing extension from the last part of [path].
 
     withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
 
+### String fromUri(Uri uri)
+
+Returns the path represented by [uri]. For POSIX and Windows styles, [uri] must
+be a `file:` URI. For the URL style, this will just convert [uri] to a string.
+
+    // POSIX
+    path.fromUri(Uri.parse('file:///path/to/foo'))
+      // -> '/path/to/foo'
+
+    // Windows
+    path.fromUri(Uri.parse('file:///C:/path/to/foo'))
+      // -> r'C:\path\to\foo'
+
+    // URL
+    path.fromUri(Uri.parse('http://dartlang.org/path/to/foo'))
+      // -> 'http://dartlang.org/path/to/foo'
+
+### Uri toUri(String path)
+
+Returns the URI that represents [path]. For POSIX and Windows styles, this will
+return a `file:` URI. For the URL style, this will just convert [path] to a
+[Uri].
+
+This will always convert relative paths to absolute ones before converting
+to a URI.
+
+    // POSIX
+    path.toUri('/path/to/foo')
+      // -> Uri.parse('file:///path/to/foo')
+
+    // Windows
+    path.toUri(r'C:\path\to\foo')
+      // -> Uri.parse('file:///C:/path/to/foo')
+
+    // URL
+    path.toUri('http://dartlang.org/path/to/foo')
+      // -> Uri.parse('http://dartlang.org/path/to/foo')
+
 ## The path.Builder class
 
 In addition to the functions, path exposes a `path.Builder` class. This lets
@@ -234,6 +304,11 @@
     builder.rootPrefix(r'path\to\foo'); // -> ''
     builder.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
 
+    // URL
+    builder.rootPrefix('path/to/foo'); // -> ''
+    builder.rootPrefix('http://dartlang.org/path/to/foo');
+      // -> 'http://dartlang.org'
+
 ### String resolve(String part1, [String part2, String part3, ...])
 
 Creates a new path by appending the given path parts to the [root].
@@ -244,9 +319,9 @@
 
 ## The path.Style class
 
-The path library can work with two different "flavors" of path: POSIX and
-Windows. The differences between these are encapsulated by the `path.Style`
-enum class. There are two instances of it:
+The path library can work with three different "flavors" of path: POSIX,
+Windows, and URLs. The differences between these are encapsulated by the
+`path.Style` enum class. There are three instances of it:
 
 ### path.Style.posix
 
@@ -259,16 +334,22 @@
 a drive letter followed by a colon (example, "C:") or two backslashes
 ("\\") for UNC paths.
 
+### path.Style.url
+
+URLs aren't filesystem paths, but they're supported by Pathos to make it easier
+to manipulate URL paths in the browser.
+
+URLs use "/" (forward slash) as separators. Absolute paths either start with a
+protocol and optional hostname (e.g. `http://dartlang.org`, `file://`) or with
+"/".
+
 ## FAQ
 
 ### Where can I use this?
 
-Currently, Dart has no way of encapsulating configuration-specific code.
-Ideally, this library would be able to import dart:io when that's available or
-dart:html when that is. That would let it seamlessly work on both.
-
-Until then, this only works on the standalone VM. It's API is not coupled to
-dart:io, but it uses it internally to determine the current working directory.
+Pathos runs on the Dart VM and in the browser under both dart2js and Dartium.
+Under dart2js, it currently returns "." as the current working directory, while
+under Dartium it returns the current URL.
 
 ### Why doesn't this make paths first-class objects?
 
diff --git a/lib/path.dart b/lib/path.dart
index 9e540b2..b425124 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -3,16 +3,77 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// A comprehensive, cross-platform path manipulation library.
+///
+/// ## Installing ##
+///
+/// Use [pub][] to install this package. Add the following to your
+/// `pubspec.yaml` file.
+///
+///     dependencies:
+///       path: any
+///
+/// Then run `pub install`.
+///
+/// For more information, see the [path package on pub.dartlang.org][pkg].
+///
+/// [pub]: http://pub.dartlang.org
+/// [pkg]: http://pub.dartlang.org/packages/path
 library path;
 
-import 'dart:io' as io;
+import 'dart:mirrors';
 
 /// An internal builder for the current OS so we can provide a straight
 /// functional interface and not require users to create one.
 final _builder = new Builder();
 
+/**
+ * Inserts [length] elements in front of the [list] and fills them with the
+ * [fillValue].
+ */
+void _growListFront(List list, int length, fillValue) =>
+  list.insertAll(0, new List.filled(length, fillValue));
+
+/// If we're running in the server-side Dart VM, this will return a
+/// [LibraryMirror] that gives access to the `dart:io` library.
+///
+/// If `dart:io` is not available, this returns null.
+LibraryMirror get _io {
+  try {
+    return currentMirrorSystem().libraries[Uri.parse('dart:io')];
+  } catch (_) {
+    return null;
+  }
+}
+
+// TODO(nweiz): when issue 6490 or 6943 are fixed, make this work under dart2js.
+/// If we're running in Dartium, this will return a [LibraryMirror] that gives
+/// access to the `dart:html` library.
+///
+/// If `dart:html` is not available, this returns null.
+LibraryMirror get _html {
+  try {
+    return currentMirrorSystem().libraries[Uri.parse('dart:html')];
+  } catch (_) {
+    return null;
+  }
+}
+
 /// Gets the path to the current working directory.
-String get current => new io.Directory.current().path;
+///
+/// In the browser, this means the current URL. When using dart2js, this
+/// currently returns `.` due to technical constraints. In the future, it will
+/// return the current URL.
+String get current {
+  if (_io != null) {
+    return _io.classes[const Symbol('Directory')]
+        .getField(const Symbol('current')).reflectee.path;
+  } else if (_html != null) {
+    return _html.getField(const Symbol('window'))
+        .reflectee.location.href;
+  } else {
+    return '.';
+  }
+}
 
 /// Gets the path separator for the current platform. On Mac and Linux, this
 /// is `/`. On Windows, it's `\`.
@@ -81,12 +142,25 @@
 ///     // Windows
 ///     path.rootPrefix(r'path\to\foo'); // -> ''
 ///     path.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
+///
+///     // URL
+///     path.rootPrefix('path/to/foo'); // -> ''
+///     path.rootPrefix('http://dartlang.org/path/to/foo');
+///       // -> 'http://dartlang.org'
 String rootPrefix(String path) => _builder.rootPrefix(path);
 
 /// Returns `true` if [path] is an absolute path and `false` if it is a
-/// relative path. On POSIX systems, absolute paths start with a `/` (forward
-/// slash). On Windows, an absolute path starts with `\\`, or a drive letter
-/// followed by `:/` or `:\`.
+/// relative path.
+///
+/// On POSIX systems, absolute paths start with a `/` (forward slash). On
+/// Windows, an absolute path starts with `\\`, or a drive letter followed by
+/// `:/` or `:\`. For URLs, absolute paths either start with a protocol and
+/// optional hostname (e.g. `http://dartlang.org`, `file://`) or with a `/`.
+///
+/// URLs that start with `/` are known as "root-relative", since they're
+/// relative to the root of the current URL. Since root-relative paths are still
+/// absolute in every other sense, [isAbsolute] will return true for them. They
+/// can be detected using [isRootRelative].
 bool isAbsolute(String path) => _builder.isAbsolute(path);
 
 /// Returns `true` if [path] is a relative path and `false` if it is absolute.
@@ -95,6 +169,16 @@
 /// `:/` or `:\`.
 bool isRelative(String path) => _builder.isRelative(path);
 
+/// Returns `true` if [path] is a root-relative path and `false` if it's not.
+///
+/// URLs that start with `/` are known as "root-relative", since they're
+/// relative to the root of the current URL. Since root-relative paths are still
+/// absolute in every other sense, [isAbsolute] will return true for them. They
+/// can be detected using [isRootRelative].
+///
+/// No POSIX and Windows paths are root-relative.
+bool isRootRelative(String path) => _builder.isRootRelative(path);
+
 /// Joins the given path parts into a single path using the current platform's
 /// [separator]. Example:
 ///
@@ -146,6 +230,10 @@
 ///
 ///     // Windows
 ///     path.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo']
+///
+///     // Browser
+///     path.split('http://dartlang.org/path/to/foo');
+///       // -> ['http://dartlang.org', 'path', 'to', 'foo']
 List<String> split(String path) => _builder.split(path);
 
 /// Normalizes [path], simplifying it by handling `..`, and `.`, and
@@ -169,9 +257,15 @@
 ///         from: '/root/path'); // -> '../other.dart'
 ///
 /// Since there is no relative path from one drive letter to another on Windows,
-/// this will return an absolute path in that case.
+/// or from one hostname to another for URLs, this will return an absolute path
+/// in those cases.
 ///
+///     // Windows
 ///     path.relative(r'D:\other', from: r'C:\home'); // -> 'D:\other'
+///
+///     // URL
+///     path.relative('http://dartlang.org', from: 'http://pub.dartlang.org');
+///       // -> 'http://dartlang.org'
 String relative(String path, {String from}) =>
     _builder.relative(path, from: from);
 
@@ -180,6 +274,45 @@
 ///     withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
 String withoutExtension(String path) => _builder.withoutExtension(path);
 
+/// Returns the path represented by [uri].
+///
+/// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL
+/// style, this will just convert [uri] to a string.
+///
+///     // POSIX
+///     path.fromUri(Uri.parse('file:///path/to/foo'))
+///       // -> '/path/to/foo'
+///
+///     // Windows
+///     path.fromUri(Uri.parse('file:///C:/path/to/foo'))
+///       // -> r'C:\path\to\foo'
+///
+///     // URL
+///     path.fromUri(Uri.parse('http://dartlang.org/path/to/foo'))
+///       // -> 'http://dartlang.org/path/to/foo'
+String fromUri(Uri uri) => _builder.fromUri(uri);
+
+/// Returns the URI that represents [path].
+///
+/// For POSIX and Windows styles, this will return a `file:` URI. For the URL
+/// style, this will just convert [path] to a [Uri].
+///
+/// This will always convert relative paths to absolute ones before converting
+/// to a URI.
+///
+///     // POSIX
+///     path.toUri('/path/to/foo')
+///       // -> Uri.parse('file:///path/to/foo')
+///
+///     // Windows
+///     path.toUri(r'C:\path\to\foo')
+///       // -> Uri.parse('file:///C:/path/to/foo')
+///
+///     // URL
+///     path.toUri('http://dartlang.org/path/to/foo')
+///       // -> Uri.parse('http://dartlang.org/path/to/foo')
+Uri toUri(String path) => _builder.toUri(path);
+
 /// Validates that there are no non-null arguments following a null one and
 /// throws an appropriate [ArgumentError] on failure.
 _validateArgList(String method, List<String> args) {
@@ -194,11 +327,11 @@
 
     // Show the arguments.
     var message = new StringBuffer();
-    message.add("$method(");
-    message.add(args.take(numArgs)
+    message.write("$method(");
+    message.write(args.take(numArgs)
         .map((arg) => arg == null ? "null" : '"$arg"')
         .join(", "));
-    message.add("): part ${i - 1} was null, but part $i was not.");
+    message.write("): part ${i - 1} was null, but part $i was not.");
     throw new ArgumentError(message.toString());
   }
 }
@@ -211,9 +344,16 @@
   /// If [style] is omitted, it uses the host operating system's path style. If
   /// [root] is omitted, it defaults to the current working directory. If [root]
   /// is relative, it is considered relative to the current working directory.
+  ///
+  /// On the browser, the path style is [Style.url]. In Dartium, [root] defaults
+  /// to the current URL. When using dart2js, it currently defaults to `.` due
+  /// to technical constraints.
   factory Builder({Style style, String root}) {
     if (style == null) {
-      if (io.Platform.operatingSystem == 'windows') {
+      if (_io == null) {
+        style = Style.url;
+      } else if (_io.classes[const Symbol('Platform')]
+          .getField(const Symbol('operatingSystem')).reflectee == 'windows') {
         style = Style.windows;
       } else {
         style = Style.posix;
@@ -245,7 +385,7 @@
   ///
   /// Trailing separators are ignored.
   ///
-  ///     builder.dirname('path/to/'); // -> 'to'
+  ///     builder.basename('path/to/'); // -> 'to'
   String basename(String path) => _parse(path).basename;
 
   /// Gets the part of [path] after the last separator on the builder's
@@ -255,7 +395,7 @@
   ///
   /// Trailing separators are ignored.
   ///
-  ///     builder.dirname('path/to/foo.dart/'); // -> 'foo'
+  ///     builder.basenameWithoutExtension('path/to/foo.dart/'); // -> 'foo'
   String basenameWithoutExtension(String path) =>
     _parse(path).basenameWithoutExtension;
 
@@ -306,22 +446,45 @@
   ///     // Windows
   ///     builder.rootPrefix(r'path\to\foo'); // -> ''
   ///     builder.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
+  ///
+  ///     // URL
+  ///     builder.rootPrefix('path/to/foo'); // -> ''
+  ///     builder.rootPrefix('http://dartlang.org/path/to/foo');
+  ///       // -> 'http://dartlang.org'
   String rootPrefix(String path) {
     var root = _parse(path).root;
     return root == null ? '' : root;
   }
 
   /// Returns `true` if [path] is an absolute path and `false` if it is a
-  /// relative path. On POSIX systems, absolute paths start with a `/` (forward
-  /// slash). On Windows, an absolute path starts with `\\`, or a drive letter
-  /// followed by `:/` or `:\`.
+  /// relative path.
+  ///
+  /// On POSIX systems, absolute paths start with a `/` (forward slash). On
+  /// Windows, an absolute path starts with `\\`, or a drive letter followed by
+  /// `:/` or `:\`. For URLs, absolute paths either start with a protocol and
+  /// optional hostname (e.g. `http://dartlang.org`, `file://`) or with a `/`.
+  ///
+  /// URLs that start with `/` are known as "root-relative", since they're
+  /// relative to the root of the current URL. Since root-relative paths are
+  /// still absolute in every other sense, [isAbsolute] will return true for
+  /// them. They can be detected using [isRootRelative].
   bool isAbsolute(String path) => _parse(path).isAbsolute;
 
   /// Returns `true` if [path] is a relative path and `false` if it is absolute.
   /// On POSIX systems, absolute paths start with a `/` (forward slash). On
   /// Windows, an absolute path starts with `\\`, or a drive letter followed by
   /// `:/` or `:\`.
-  bool isRelative(String path) => !isAbsolute(path);
+  bool isRelative(String path) => !this.isAbsolute(path);
+
+  /// Returns `true` if [path] is a root-relative path and `false` if it's not.
+  ///
+  /// URLs that start with `/` are known as "root-relative", since they're
+  /// relative to the root of the current URL. Since root-relative paths are
+  /// still absolute in every other sense, [isAbsolute] will return true for
+  /// them. They can be detected using [isRootRelative].
+  ///
+  /// No POSIX and Windows paths are root-relative.
+  bool isRootRelative(String path) => _parse(path).isRootRelative;
 
   /// Joins the given path parts into a single path. Example:
   ///
@@ -360,26 +523,34 @@
   String joinAll(Iterable<String> parts) {
     var buffer = new StringBuffer();
     var needsSeparator = false;
+    var isAbsoluteAndNotRootRelative = false;
 
     for (var part in parts) {
-      if (this.isAbsolute(part)) {
+      if (this.isRootRelative(part) && isAbsoluteAndNotRootRelative) {
+        // If the new part is root-relative, it preserves the previous root but
+        // replaces the path after it.
+        var oldRoot = this.rootPrefix(buffer.toString());
+        buffer.clear();
+        buffer.write(oldRoot);
+        buffer.write(part);
+      } else if (this.isAbsolute(part)) {
+        isAbsoluteAndNotRootRelative = !this.isRootRelative(part);
         // An absolute path discards everything before it.
         buffer.clear();
-        buffer.add(part);
+        buffer.write(part);
       } else {
         if (part.length > 0 && part[0].contains(style.separatorPattern)) {
           // The part starts with a separator, so we don't need to add one.
         } else if (needsSeparator) {
-          buffer.add(separator);
+          buffer.write(separator);
         }
 
-        buffer.add(part);
+        buffer.write(part);
       }
 
       // Unless this part ends with a separator, we'll need to add one before
       // the next part.
-      needsSeparator = part.length > 0 &&
-          !part[part.length - 1].contains(style.separatorPattern);
+      needsSeparator = part.contains(style.needsSeparatorPattern);
     }
 
     return buffer.toString();
@@ -406,8 +577,9 @@
   List<String> split(String path) {
     var parsed = _parse(path);
     // Filter out empty parts that exist due to multiple separators in a row.
-    parsed.parts = parsed.parts.where((part) => !part.isEmpty).toList();
-    if (parsed.root != null) parsed.parts.insertRange(0, 1, parsed.root);
+    parsed.parts = parsed.parts.where((part) => !part.isEmpty)
+                               .toList();
+    if (parsed.root != null) parsed.parts.insert(0, parsed.root);
     return parsed.parts;
   }
 
@@ -469,7 +641,9 @@
 
     // If the given path is relative, resolve it relative to the root of the
     // builder.
-    if (this.isRelative(path)) path = this.resolve(path);
+    if (this.isRelative(path) || this.isRootRelative(path)) {
+      path = this.resolve(path);
+    }
 
     // If the path is still relative and `from` is absolute, we're unable to
     // find a path from `from` to `path`.
@@ -480,6 +654,10 @@
     var fromParsed = _parse(from)..normalize();
     var pathParsed = _parse(path)..normalize();
 
+    if (fromParsed.parts.length > 0 && fromParsed.parts[0] == '.') {
+      return pathParsed.toString();
+    }
+
     // If the root prefixes don't match (for example, different drive letters
     // on Windows), then there is no relative path, so just return the absolute
     // one. In Windows, drive letters are case-insenstive and we allow
@@ -495,16 +673,17 @@
     while (fromParsed.parts.length > 0 && pathParsed.parts.length > 0 &&
            fromParsed.parts[0] == pathParsed.parts[0]) {
       fromParsed.parts.removeAt(0);
-      fromParsed.separators.removeAt(0);
+      fromParsed.separators.removeAt(1);
       pathParsed.parts.removeAt(0);
-      pathParsed.separators.removeAt(0);
+      pathParsed.separators.removeAt(1);
     }
 
     // If there are any directories left in the root path, we need to walk up
     // out of them.
-    pathParsed.parts.insertRange(0, fromParsed.parts.length, '..');
-    pathParsed.separators.insertRange(0, fromParsed.parts.length,
-        style.separator);
+    _growListFront(pathParsed.parts, fromParsed.parts.length, '..');
+    pathParsed.separators[0] = '';
+    pathParsed.separators.insertAll(1,
+        new List.filled(fromParsed.parts.length, style.separator));
 
     // Corner case: the paths completely collapsed.
     if (pathParsed.parts.length == 0) return '.';
@@ -532,16 +711,68 @@
     return parsed.toString();
   }
 
+  /// Returns the path represented by [uri].
+  ///
+  /// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL
+  /// style, this will just convert [uri] to a string.
+  ///
+  ///     // POSIX
+  ///     builder.fromUri(Uri.parse('file:///path/to/foo'))
+  ///       // -> '/path/to/foo'
+  ///
+  ///     // Windows
+  ///     builder.fromUri(Uri.parse('file:///C:/path/to/foo'))
+  ///       // -> r'C:\path\to\foo'
+  ///
+  ///     // URL
+  ///     builder.fromUri(Uri.parse('http://dartlang.org/path/to/foo'))
+  ///       // -> 'http://dartlang.org/path/to/foo'
+  String fromUri(Uri uri) => style.pathFromUri(uri);
+
+  /// Returns the URI that represents [path].
+  ///
+  /// For POSIX and Windows styles, this will return a `file:` URI. For the URL
+  /// style, this will just convert [path] to a [Uri].
+  ///
+  ///     // POSIX
+  ///     builder.toUri('/path/to/foo')
+  ///       // -> Uri.parse('file:///path/to/foo')
+  ///
+  ///     // Windows
+  ///     builder.toUri(r'C:\path\to\foo')
+  ///       // -> Uri.parse('file:///C:/path/to/foo')
+  ///
+  ///     // URL
+  ///     builder.toUri('http://dartlang.org/path/to/foo')
+  ///       // -> Uri.parse('http://dartlang.org/path/to/foo')
+  Uri toUri(String path) {
+    if (isRelative(path)) {
+      return Uri.parse(path.replaceAll(style.separatorPattern, '/'));
+    } else {
+      return style.pathToUri(join(root, path));
+    }
+  }
+
   _ParsedPath _parse(String path) {
     var before = path;
 
     // Remove the root prefix, if any.
     var root = style.getRoot(path);
+    var isRootRelative = style.getRelativeRoot(path) != null;
     if (root != null) path = path.substring(root.length);
 
     // Split the parts on path separators.
     var parts = [];
     var separators = [];
+
+    var firstSeparator = style.separatorPattern.firstMatch(path);
+    if (firstSeparator != null && firstSeparator.start == 0) {
+      separators.add(firstSeparator[0]);
+      path = path.substring(firstSeparator[0].length);
+    } else {
+      separators.add('');
+    }
+
     var start = 0;
     for (var match in style.separatorPattern.allMatches(path)) {
       parts.add(path.substring(start, match.start));
@@ -555,57 +786,216 @@
       separators.add('');
     }
 
-    return new _ParsedPath(style, root, parts, separators);
+    return new _ParsedPath(style, root, isRootRelative, parts, separators);
   }
 }
 
 /// An enum type describing a "flavor" of path.
-class Style {
+abstract class Style {
   /// POSIX-style paths use "/" (forward slash) as separators. Absolute paths
   /// start with "/". Used by UNIX, Linux, Mac OS X, and others.
-  static final posix = new Style._('posix', '/', '/', '/');
+  static final posix = new _PosixStyle();
 
   /// Windows paths use "\" (backslash) as separators. Absolute paths start with
   /// a drive letter followed by a colon (example, "C:") or two backslashes
   /// ("\\") for UNC paths.
   // TODO(rnystrom): The UNC root prefix should include the drive name too, not
   // just the "\\".
-  static final windows = new Style._('windows', '\\', r'[/\\]',
-      r'\\\\|[a-zA-Z]:[/\\]');
+  static final windows = new _WindowsStyle();
 
-  Style._(this.name, this.separator, String separatorPattern,
-      String rootPattern)
-    : separatorPattern = new RegExp(separatorPattern),
-      _rootPattern = new RegExp('^$rootPattern');
+  /// URLs aren't filesystem paths, but they're supported by Pathos to make it
+  /// easier to manipulate URL paths in the browser.
+  ///
+  /// URLs use "/" (forward slash) as separators. Absolute paths either start
+  /// with a protocol and optional hostname (e.g. `http://dartlang.org`,
+  /// `file://`) or with "/".
+  static final url = new _UrlStyle();
 
   /// The name of this path style. Will be "posix" or "windows".
-  final String name;
+  String get name;
 
   /// The path separator for this style. On POSIX, this is `/`. On Windows,
   /// it's `\`.
-  final String separator;
+  String get separator;
 
   /// The [Pattern] that can be used to match a separator for a path in this
-  /// style. Windows allows both "/" and "\" as path separators even though
-  /// "\" is the canonical one.
-  final Pattern separatorPattern;
+  /// style. Windows allows both "/" and "\" as path separators even though "\"
+  /// is the canonical one.
+  Pattern get separatorPattern;
 
-  // TODO(nweiz): make this a Pattern when issue 7080 is fixed.
-  /// The [RegExp] that can be used to match the root prefix of an absolute
+  /// The [Pattern] that matches path components that need a separator after
+  /// them.
+  ///
+  /// Windows and POSIX styles just need separators when the previous component
+  /// doesn't already end in a separator, but the URL always needs to place a
+  /// separator between the root and the first component, even if the root
+  /// already ends in a separator character. For example, to join "file://" and
+  /// "usr", an additional "/" is needed (making "file:///usr").
+  Pattern get needsSeparatorPattern;
+
+  /// The [Pattern] that can be used to match the root prefix of an absolute
   /// path in this style.
-  final RegExp _rootPattern;
+  Pattern get rootPattern;
+
+  /// The [Pattern] that can be used to match the root prefix of a root-relative
+  /// path in this style.
+  ///
+  /// This can be null to indicate that this style doesn't support root-relative
+  /// paths.
+  final Pattern relativeRootPattern = null;
 
   /// Gets the root prefix of [path] if path is absolute. If [path] is relative,
   /// returns `null`.
   String getRoot(String path) {
-    var match = _rootPattern.firstMatch(path);
+    var match = rootPattern.firstMatch(path);
+    if (match != null) return match[0];
+    return getRelativeRoot(path);
+  }
+
+  /// Gets the root prefix of [path] if it's root-relative.
+  ///
+  /// If [path] is relative or absolute and not root-relative, returns `null`.
+  String getRelativeRoot(String path) {
+    if (relativeRootPattern == null) return null;
+    var match = relativeRootPattern.firstMatch(path);
     if (match == null) return null;
     return match[0];
   }
 
+  /// Returns the path represented by [uri] in this style.
+  String pathFromUri(Uri uri);
+
+  /// Returns the URI that represents [path].
+  ///
+  /// Pathos will always path an absolute path for [path]. Relative paths are
+  /// handled automatically by [Builder].
+  Uri pathToUri(String path);
+
   String toString() => name;
 }
 
+/// The style for POSIX paths.
+class _PosixStyle extends Style {
+  _PosixStyle();
+
+  static final _builder = new Builder(style: Style.posix);
+
+  final name = 'posix';
+  final separator = '/';
+  final separatorPattern = new RegExp(r'/');
+  final needsSeparatorPattern = new RegExp(r'[^/]$');
+  final rootPattern = new RegExp(r'^/');
+
+  String pathFromUri(Uri uri) {
+    if (uri.scheme == '' || uri.scheme == 'file') {
+      return Uri.decodeComponent(uri.path);
+    }
+    throw new ArgumentError("Uri $uri must have scheme 'file:'.");
+  }
+
+  Uri pathToUri(String path) {
+    var parsed = _builder._parse(path);
+
+    if (parsed.parts.isEmpty) {
+      // If the path is a bare root (e.g. "/"), [components] will
+      // currently be empty. We add two empty components so the URL constructor
+      // produces "file:///", with a trailing slash.
+      parsed.parts.addAll(["", ""]);
+    } else if (parsed.hasTrailingSeparator) {
+      // If the path has a trailing slash, add a single empty component so the
+      // URI has a trailing slash as well.
+      parsed.parts.add("");
+    }
+
+    return new Uri(scheme: 'file', pathSegments: parsed.parts);
+  }
+}
+
+/// The style for Windows paths.
+class _WindowsStyle extends Style {
+  _WindowsStyle();
+
+  static final _builder = new Builder(style: Style.windows);
+
+  final name = 'windows';
+  final separator = '\\';
+  final separatorPattern = new RegExp(r'[/\\]');
+  final needsSeparatorPattern = new RegExp(r'[^/\\]$');
+  final rootPattern = new RegExp(r'^(\\\\|[a-zA-Z]:[/\\])');
+
+  String pathFromUri(Uri uri) {
+    if (uri.scheme != '' && uri.scheme != 'file') {
+      throw new ArgumentError("Uri $uri must have scheme 'file:'.");
+    }
+
+    var path = uri.path;
+    if (uri.host == '') {
+      // Drive-letter paths look like "file:///C:/path/to/file". The
+      // replaceFirst removes the extra initial slash.
+      if (path.startsWith('/')) path = path.replaceFirst("/", "");
+    } else {
+      // Network paths look like "file://hostname/path/to/file".
+      path = '\\\\${uri.host}$path';
+    }
+    return Uri.decodeComponent(path.replaceAll("/", "\\"));
+  }
+
+  Uri pathToUri(String path) {
+    var parsed = _builder._parse(path);
+    if (parsed.root == r'\\') {
+      // Network paths become "file://hostname/path/to/file".
+
+      var host = parsed.parts.removeAt(0);
+
+      if (parsed.parts.isEmpty) {
+        // If the path is a bare root (e.g. "\\hostname"), [parsed.parts] will
+        // currently be empty. We add two empty components so the URL
+        // constructor produces "file://hostname/", with a trailing slash.
+        parsed.parts.addAll(["", ""]);
+      } else if (parsed.hasTrailingSeparator) {
+        // If the path has a trailing slash, add a single empty component so the
+        // URI has a trailing slash as well.
+        parsed.parts.add("");
+      }
+
+      return new Uri(scheme: 'file', host: host, pathSegments: parsed.parts);
+    } else {
+      // Drive-letter paths become "file:///C:/path/to/file".
+
+      // If the path is a bare root (e.g. "C:\"), [parsed.parts] will currently
+      // be empty. We add an empty component so the URL constructor produces
+      // "file:///C:/", with a trailing slash. We also add an empty component if
+      // the URL otherwise has a trailing slash.
+      if (parsed.parts.length == 0 || parsed.hasTrailingSeparator) {
+        parsed.parts.add("");
+      }
+
+      // Get rid of the trailing "\" in "C:\" because the URI constructor will
+      // add a separator on its own.
+      parsed.parts.insert(0, parsed.root.replaceAll(separatorPattern, ""));
+
+      return new Uri(scheme: 'file', pathSegments: parsed.parts);
+    }
+  }
+}
+
+/// The style for URL paths.
+class _UrlStyle extends Style {
+  _UrlStyle();
+
+  final name = 'url';
+  final separator = '/';
+  final separatorPattern = new RegExp(r'/');
+  final needsSeparatorPattern = new RegExp(
+      r"(^[a-zA-Z][-+.a-zA-Z\d]*://|[^/])$");
+  final rootPattern = new RegExp(r"[a-zA-Z][-+.a-zA-Z\d]*://[^/]*");
+  final relativeRootPattern = new RegExp(r"^/");
+
+  String pathFromUri(Uri uri) => uri.toString();
+
+  Uri pathToUri(String path) => Uri.parse(path);
+}
+
 // TODO(rnystrom): Make this public?
 class _ParsedPath {
   /// The [Style] that was used to parse this path.
@@ -617,12 +1007,20 @@
   /// letters.
   String root;
 
+  /// Whether this path is root-relative.
+  ///
+  /// See [Builder.isRootRelative].
+  bool isRootRelative;
+
   /// The path-separated parts of the path. All but the last will be
   /// directories.
   List<String> parts;
 
-  /// The path separators following each part. The last one will be an empty
-  /// string unless the path ends with a trailing separator.
+  /// The path separators preceding each part.
+  ///
+  /// The first one will be an empty string unless the root requires a separator
+  /// between it and the path. The last one will be an empty string unless the
+  /// path ends with a trailing separator.
   List<String> separators;
 
   /// The file extension of the last part, or "" if it doesn't have one.
@@ -631,7 +1029,8 @@
   /// `true` if this is an absolute path.
   bool get isAbsolute => root != null;
 
-  _ParsedPath(this.style, this.root, this.parts, this.separators);
+  _ParsedPath(this.style, this.root, this.isRootRelative, this.parts,
+      this.separators);
 
   String get basename {
     var copy = this.clone();
@@ -647,6 +1046,8 @@
     return copy._splitExtension()[0];
   }
 
+  bool get hasTrailingSeparator => !parts.isEmpty && (parts.last == '' || separators.last != '');
+
   void removeTrailingSeparators() {
     while (!parts.isEmpty && parts.last == '') {
       parts.removeLast();
@@ -677,7 +1078,7 @@
 
     // A relative path can back out from the start directory.
     if (!isAbsolute) {
-      newParts.insertRange(0, leadingDoubles, '..');
+      _growListFront(newParts, leadingDoubles, '..');
     }
 
     // If we collapsed down to nothing, do ".".
@@ -686,8 +1087,12 @@
     }
 
     // Canonicalize separators.
-    var newSeparators = [];
-    newSeparators.insertRange(0, newParts.length, style.separator);
+    var newSeparators = new List.generate(
+        newParts.length, (_) => style.separator, growable: true);
+    newSeparators.insert(0,
+        isAbsolute && newParts.length > 0 &&
+                root.contains(style.needsSeparatorPattern) ?
+            style.separator : '');
 
     parts = newParts;
     separators = newSeparators;
@@ -701,11 +1106,12 @@
 
   String toString() {
     var builder = new StringBuffer();
-    if (root != null) builder.add(root);
+    if (root != null) builder.write(root);
     for (var i = 0; i < parts.length; i++) {
-      builder.add(parts[i]);
-      builder.add(separators[i]);
+      builder.write(separators[i]);
+      builder.write(parts[i]);
     }
+    builder.write(separators.last);
 
     return builder.toString();
   }
@@ -729,5 +1135,6 @@
   }
 
   _ParsedPath clone() => new _ParsedPath(
-      style, root, new List.from(parts), new List.from(separators));
+      style, root, isRootRelative,
+      new List.from(parts), new List.from(separators));
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 3372302..60fcec2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,10 +1,7 @@
-name: pathos
+name: path
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: >
  A string-based path manipulation library. All of the path operations you know
  and love, with solid support on both Windows and POSIX (Linux and Mac OS X)
  machines.
-
- Currently only runs on the standalone VM, but will run in a browser as soon as
- configuration-specific code is supported by Dart.
diff --git a/test/dart2js_test.dart b/test/dart2js_test.dart
new file mode 100644
index 0000000..997513d
--- /dev/null
+++ b/test/dart2js_test.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2013, 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:unittest/unittest.dart';
+import 'package:path/path.dart' as path;
+
+// In the future, the default root will be window.location.href, but right now
+// that's not possible.
+
+main() {
+  group('new Builder()', () {
+    test('uses the current working directory if root is omitted', () {
+      var builder = new path.Builder();
+      expect(builder.root, '.');
+    });
+
+    test('uses URL if style is omitted', () {
+      var builder = new path.Builder();
+      expect(builder.style, path.Style.url);
+    });
+  });
+
+  test('current', () {
+    expect(path.current, '.');
+  });
+}
diff --git a/test/dartium_test.dart b/test/dartium_test.dart
new file mode 100644
index 0000000..2a830c7
--- /dev/null
+++ b/test/dartium_test.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2013, 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:html';
+
+import 'package:unittest/unittest.dart';
+import 'package:unittest/html_config.dart';
+import 'package:path/path.dart' as path;
+
+main() {
+  useHtmlConfiguration();
+
+  group('new Builder()', () {
+    test('uses the current working directory if root is omitted', () {
+      var builder = new path.Builder();
+      expect(builder.root, window.location.href);
+    });
+
+    test('uses URL if style is omitted', () {
+      var builder = new path.Builder();
+      expect(builder.style, path.Style.url);
+    });
+  });
+
+  test('current', () {
+    expect(path.current, window.location.href);
+  });
+}
diff --git a/test/io_test.dart b/test/io_test.dart
new file mode 100644
index 0000000..fc29ced
--- /dev/null
+++ b/test/io_test.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2013, 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' as io;
+
+import 'package:unittest/unittest.dart';
+import 'package:path/path.dart' as path;
+
+main() {
+  group('new Builder()', () {
+    test('uses the current working directory if root is omitted', () {
+      var builder = new path.Builder();
+      expect(builder.root, io.Directory.current.path);
+    });
+
+    test('uses the host OS if style is omitted', () {
+      var builder = new path.Builder();
+      if (io.Platform.operatingSystem == 'windows') {
+        expect(builder.style, path.Style.windows);
+      } else {
+        expect(builder.style, path.Style.posix);
+      }
+    });
+  });
+
+  test('current', () {
+    expect(path.current, io.Directory.current.path);
+  });
+}
diff --git a/test/path_test.dart b/test/path_test.dart
index eea906a..ef3cf21 100644
--- a/test/path_test.dart
+++ b/test/path_test.dart
@@ -2,10 +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.
 
-library all_test;
-
-import 'dart:io' as io;
-
 import 'package:unittest/unittest.dart';
 import 'package:path/path.dart' as path;
 
@@ -37,23 +33,5 @@
       var builder = new path.Builder(style: path.Style.windows);
       expect(builder.style, path.Style.windows);
     });
-
-    test('uses the current working directory if root is omitted', () {
-      var builder = new path.Builder();
-      expect(builder.root, new io.Directory.current().path);
-    });
-
-    test('uses the host OS if style is omitted', () {
-      var builder = new path.Builder();
-      if (io.Platform.operatingSystem == 'windows') {
-        expect(builder.style, path.Style.windows);
-      } else {
-        expect(builder.style, path.Style.posix);
-      }
-    });
-  });
-
-  test('current', () {
-    expect(path.current, new io.Directory.current().path);
   });
 }
diff --git a/test/path_posix_test.dart b/test/posix_test.dart
similarity index 91%
rename from test/path_posix_test.dart
rename to test/posix_test.dart
index 763035f..f3232bc 100644
--- a/test/path_posix_test.dart
+++ b/test/posix_test.dart
@@ -2,9 +2,7 @@
 // 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.
 
-library path_test;
-
-import 'dart:io' as io;
+library path.test.posix_test;
 
 import 'package:unittest/unittest.dart';
 import 'package:path/path.dart' as path;
@@ -300,6 +298,12 @@
         expect(builder.relative('../a/b.txt'), '../a/b.txt');
         expect(builder.relative('a/./b/../c.txt'), 'a/c.txt');
       });
+
+      // Regression
+      test('from root-only path', () {
+        expect(builder.relative('/', from: '/'), '.');
+        expect(builder.relative('/root/path', from: '/'), 'root/path');
+      });
     });
 
     group('from relative root', () {
@@ -344,6 +348,12 @@
           equals('/foo/bar/baz'));
       expect(r.relative('..', from: 'foo/bar'), equals('../../..'));
     });
+
+    test('from a . root', () {
+      var r = new path.Builder(style: path.Style.posix, root: '.');
+      expect(r.relative('/foo/bar/baz'), equals('/foo/bar/baz'));
+      expect(r.relative('foo/bar/baz'), equals('foo/bar/baz'));
+    });
   });
 
   group('resolve', () {
@@ -389,4 +399,26 @@
     expect(builder.withoutExtension('a/b.c/'), 'a/b/');
     expect(builder.withoutExtension('a/b.c//'), 'a/b//');
   });
+
+  test('fromUri', () {
+    expect(builder.fromUri(Uri.parse('file:///path/to/foo')), '/path/to/foo');
+    expect(builder.fromUri(Uri.parse('file:///path/to/foo/')), '/path/to/foo/');
+    expect(builder.fromUri(Uri.parse('file:///')), '/');
+    expect(builder.fromUri(Uri.parse('foo/bar')), 'foo/bar');
+    expect(builder.fromUri(Uri.parse('/path/to/foo')), '/path/to/foo');
+    expect(builder.fromUri(Uri.parse('///path/to/foo')), '/path/to/foo');
+    expect(builder.fromUri(Uri.parse('file:///path/to/foo%23bar')),
+        '/path/to/foo#bar');
+    expect(() => builder.fromUri(Uri.parse('http://dartlang.org')),
+        throwsArgumentError);
+  });
+
+  test('toUri', () {
+    expect(builder.toUri('/path/to/foo'), Uri.parse('file:///path/to/foo'));
+    expect(builder.toUri('/path/to/foo/'), Uri.parse('file:///path/to/foo/'));
+    expect(builder.toUri('/'), Uri.parse('file:///'));
+    expect(builder.toUri('foo/bar'), Uri.parse('foo/bar'));
+    expect(builder.toUri('/path/to/foo#bar'),
+        Uri.parse('file:///path/to/foo%23bar'));
+  });
 }
diff --git a/test/url_test.dart b/test/url_test.dart
new file mode 100644
index 0000000..3e03d50
--- /dev/null
+++ b/test/url_test.dart
@@ -0,0 +1,653 @@
+// Copyright (c) 2012, 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:unittest/unittest.dart';
+import 'package:path/path.dart' as path;
+
+main() {
+  var builder = new path.Builder(style: path.Style.url,
+      root: 'http://dartlang.org/root/path');
+
+  test('separator', () {
+    expect(builder.separator, '/');
+  });
+
+  test('extension', () {
+    expect(builder.extension(''), '');
+    expect(builder.extension('foo.dart'), '.dart');
+    expect(builder.extension('foo.dart.js'), '.js');
+    expect(builder.extension('a.b/c'), '');
+    expect(builder.extension('a.b/c.d'), '.d');
+    expect(builder.extension(r'a.b\c'), r'.b\c');
+  });
+
+  test('rootPrefix', () {
+    expect(builder.rootPrefix(''), '');
+    expect(builder.rootPrefix('a'), '');
+    expect(builder.rootPrefix('a/b'), '');
+    expect(builder.rootPrefix('http://dartlang.org/a/c'),
+        'http://dartlang.org');
+    expect(builder.rootPrefix('file:///a/c'), 'file://');
+    expect(builder.rootPrefix('/a/c'), '/');
+    expect(builder.rootPrefix('http://dartlang.org/'), 'http://dartlang.org');
+    expect(builder.rootPrefix('file:///'), 'file://');
+    expect(builder.rootPrefix('http://dartlang.org'), 'http://dartlang.org');
+    expect(builder.rootPrefix('file://'), 'file://');
+    expect(builder.rootPrefix('/'), '/');
+  });
+
+  test('dirname', () {
+    expect(builder.dirname(''), '.');
+    expect(builder.dirname('a'), '.');
+    expect(builder.dirname('a/b'), 'a');
+    expect(builder.dirname('a/b/c'), 'a/b');
+    expect(builder.dirname('a/b.c'), 'a');
+    expect(builder.dirname('a/'), '.');
+    expect(builder.dirname('a/.'), 'a');
+    expect(builder.dirname(r'a\b/c'), r'a\b');
+    expect(builder.dirname('http://dartlang.org/a'), 'http://dartlang.org');
+    expect(builder.dirname('file:///a'), 'file://');
+    expect(builder.dirname('/a'), '/');
+    expect(builder.dirname('http://dartlang.org///a'), 'http://dartlang.org');
+    expect(builder.dirname('file://///a'), 'file://');
+    expect(builder.dirname('///a'), '/');
+    expect(builder.dirname('http://dartlang.org/'), 'http://dartlang.org');
+    expect(builder.dirname('http://dartlang.org'), 'http://dartlang.org');
+    expect(builder.dirname('file:///'), 'file://');
+    expect(builder.dirname('file://'), 'file://');
+    expect(builder.dirname('/'), '/');
+    expect(builder.dirname('http://dartlang.org///'), 'http://dartlang.org');
+    expect(builder.dirname('file://///'), 'file://');
+    expect(builder.dirname('///'), '/');
+    expect(builder.dirname('a/b/'), 'a');
+    expect(builder.dirname(r'a/b\c'), 'a');
+    expect(builder.dirname('a//'), '.');
+    expect(builder.dirname('a/b//'), 'a');
+    expect(builder.dirname('a//b'), 'a');
+  });
+
+  test('basename', () {
+    expect(builder.basename(''), '');
+    expect(builder.basename('a'), 'a');
+    expect(builder.basename('a/b'), 'b');
+    expect(builder.basename('a/b/c'), 'c');
+    expect(builder.basename('a/b.c'), 'b.c');
+    expect(builder.basename('a/'), 'a');
+    expect(builder.basename('a/.'), '.');
+    expect(builder.basename(r'a\b/c'), 'c');
+    expect(builder.basename('http://dartlang.org/a'), 'a');
+    expect(builder.basename('file:///a'), 'a');
+    expect(builder.basename('/a'), 'a');
+    expect(builder.basename('http://dartlang.org/'), 'http://dartlang.org');
+    expect(builder.basename('http://dartlang.org'), 'http://dartlang.org');
+    expect(builder.basename('file:///'), 'file://');
+    expect(builder.basename('file://'), 'file://');
+    expect(builder.basename('/'), '/');
+    expect(builder.basename('a/b/'), 'b');
+    expect(builder.basename(r'a/b\c'), r'b\c');
+    expect(builder.basename('a//'), 'a');
+    expect(builder.basename('a/b//'), 'b');
+    expect(builder.basename('a//b'), 'b');
+  });
+
+  test('basenameWithoutExtension', () {
+    expect(builder.basenameWithoutExtension(''), '');
+    expect(builder.basenameWithoutExtension('a'), 'a');
+    expect(builder.basenameWithoutExtension('a/b'), 'b');
+    expect(builder.basenameWithoutExtension('a/b/c'), 'c');
+    expect(builder.basenameWithoutExtension('a/b.c'), 'b');
+    expect(builder.basenameWithoutExtension('a/'), 'a');
+    expect(builder.basenameWithoutExtension('a/.'), '.');
+    expect(builder.basenameWithoutExtension(r'a/b\c'), r'b\c');
+    expect(builder.basenameWithoutExtension('a/.bashrc'), '.bashrc');
+    expect(builder.basenameWithoutExtension('a/b/c.d.e'), 'c.d');
+    expect(builder.basenameWithoutExtension('a//'), 'a');
+    expect(builder.basenameWithoutExtension('a/b//'), 'b');
+    expect(builder.basenameWithoutExtension('a//b'), 'b');
+    expect(builder.basenameWithoutExtension('a/b.c/'), 'b');
+    expect(builder.basenameWithoutExtension('a/b.c//'), 'b');
+  });
+
+  test('isAbsolute', () {
+    expect(builder.isAbsolute(''), false);
+    expect(builder.isAbsolute('a'), false);
+    expect(builder.isAbsolute('a/b'), false);
+    expect(builder.isAbsolute('http://dartlang.org/a'), true);
+    expect(builder.isAbsolute('file:///a'), true);
+    expect(builder.isAbsolute('/a'), true);
+    expect(builder.isAbsolute('http://dartlang.org/a/b'), true);
+    expect(builder.isAbsolute('file:///a/b'), true);
+    expect(builder.isAbsolute('/a/b'), true);
+    expect(builder.isAbsolute('http://dartlang.org/'), true);
+    expect(builder.isAbsolute('file:///'), true);
+    expect(builder.isAbsolute('http://dartlang.org'), true);
+    expect(builder.isAbsolute('file://'), true);
+    expect(builder.isAbsolute('/'), true);
+    expect(builder.isAbsolute('~'), false);
+    expect(builder.isAbsolute('.'), false);
+    expect(builder.isAbsolute('../a'), false);
+    expect(builder.isAbsolute('C:/a'), false);
+    expect(builder.isAbsolute(r'C:\a'), false);
+    expect(builder.isAbsolute(r'\\a'), false);
+  });
+
+  test('isRelative', () {
+    expect(builder.isRelative(''), true);
+    expect(builder.isRelative('a'), true);
+    expect(builder.isRelative('a/b'), true);
+    expect(builder.isRelative('http://dartlang.org/a'), false);
+    expect(builder.isRelative('file:///a'), false);
+    expect(builder.isRelative('/a'), false);
+    expect(builder.isRelative('http://dartlang.org/a/b'), false);
+    expect(builder.isRelative('file:///a/b'), false);
+    expect(builder.isRelative('/a/b'), false);
+    expect(builder.isRelative('http://dartlang.org/'), false);
+    expect(builder.isRelative('file:///'), false);
+    expect(builder.isRelative('http://dartlang.org'), false);
+    expect(builder.isRelative('file://'), false);
+    expect(builder.isRelative('/'), false);
+    expect(builder.isRelative('~'), true);
+    expect(builder.isRelative('.'), true);
+    expect(builder.isRelative('../a'), true);
+    expect(builder.isRelative('C:/a'), true);
+    expect(builder.isRelative(r'C:\a'), true);
+    expect(builder.isRelative(r'\\a'), true);
+  });
+
+  test('isRootRelative', () {
+    expect(builder.isRootRelative(''), false);
+    expect(builder.isRootRelative('a'), false);
+    expect(builder.isRootRelative('a/b'), false);
+    expect(builder.isRootRelative('http://dartlang.org/a'), false);
+    expect(builder.isRootRelative('file:///a'), false);
+    expect(builder.isRootRelative('/a'), true);
+    expect(builder.isRootRelative('http://dartlang.org/a/b'), false);
+    expect(builder.isRootRelative('file:///a/b'), false);
+    expect(builder.isRootRelative('/a/b'), true);
+    expect(builder.isRootRelative('http://dartlang.org/'), false);
+    expect(builder.isRootRelative('file:///'), false);
+    expect(builder.isRootRelative('http://dartlang.org'), false);
+    expect(builder.isRootRelative('file://'), false);
+    expect(builder.isRootRelative('/'), true);
+    expect(builder.isRootRelative('~'), false);
+    expect(builder.isRootRelative('.'), false);
+    expect(builder.isRootRelative('../a'), false);
+    expect(builder.isRootRelative('C:/a'), false);
+    expect(builder.isRootRelative(r'C:\a'), false);
+    expect(builder.isRootRelative(r'\\a'), false);
+  });
+
+  group('join', () {
+    test('allows up to eight parts', () {
+      expect(builder.join('a'), 'a');
+      expect(builder.join('a', 'b'), 'a/b');
+      expect(builder.join('a', 'b', 'c'), 'a/b/c');
+      expect(builder.join('a', 'b', 'c', 'd'), 'a/b/c/d');
+      expect(builder.join('a', 'b', 'c', 'd', 'e'), 'a/b/c/d/e');
+      expect(builder.join('a', 'b', 'c', 'd', 'e', 'f'), 'a/b/c/d/e/f');
+      expect(builder.join('a', 'b', 'c', 'd', 'e', 'f', 'g'), 'a/b/c/d/e/f/g');
+      expect(builder.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'),
+          'a/b/c/d/e/f/g/h');
+    });
+
+    test('does not add separator if a part ends in one', () {
+      expect(builder.join('a/', 'b', 'c/', 'd'), 'a/b/c/d');
+      expect(builder.join('a\\', 'b'), r'a\/b');
+    });
+
+    test('ignores parts before an absolute path', () {
+      expect(builder.join('a', 'http://dartlang.org', 'b', 'c'),
+          'http://dartlang.org/b/c');
+      expect(builder.join('a', 'file://', 'b', 'c'), 'file:///b/c');
+      expect(builder.join('a', '/', 'b', 'c'), '/b/c');
+      expect(builder.join('a', '/b', 'http://dartlang.org/c', 'd'),
+          'http://dartlang.org/c/d');
+      expect(builder.join(
+              'a', 'http://google.com/b', 'http://dartlang.org/c', 'd'),
+          'http://dartlang.org/c/d');
+      expect(builder.join('a', '/b', '/c', 'd'), '/c/d');
+      expect(builder.join('a', r'c:\b', 'c', 'd'), r'a/c:\b/c/d');
+      expect(builder.join('a', r'\\b', 'c', 'd'), r'a/\\b/c/d');
+    });
+
+    test('preserves roots before a root-relative path', () {
+      expect(builder.join('http://dartlang.org', 'a', '/b', 'c'),
+          'http://dartlang.org/b/c');
+      expect(builder.join('file://', 'a', '/b', 'c'), 'file:///b/c');
+      expect(builder.join('file://', 'a', '/b', 'c', '/d'), 'file:///d');
+    });
+
+    test('ignores trailing nulls', () {
+      expect(builder.join('a', null), equals('a'));
+      expect(builder.join('a', 'b', 'c', null, null), equals('a/b/c'));
+    });
+
+    test('disallows intermediate nulls', () {
+      expect(() => builder.join('a', null, 'b'), throwsArgumentError);
+      expect(() => builder.join(null, 'a'), throwsArgumentError);
+    });
+  });
+
+  group('joinAll', () {
+    test('allows more than eight parts', () {
+      expect(builder.joinAll(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']),
+          'a/b/c/d/e/f/g/h/i');
+    });
+
+    test('ignores parts before an absolute path', () {
+      expect(builder.joinAll(['a', 'http://dartlang.org', 'b', 'c']),
+          'http://dartlang.org/b/c');
+      expect(builder.joinAll(['a', 'file://', 'b', 'c']), 'file:///b/c');
+      expect(builder.joinAll(['a', '/', 'b', 'c']), '/b/c');
+      expect(builder.joinAll(['a', '/b', 'http://dartlang.org/c', 'd']),
+          'http://dartlang.org/c/d');
+      expect(builder.joinAll(
+              ['a', 'http://google.com/b', 'http://dartlang.org/c', 'd']),
+          'http://dartlang.org/c/d');
+      expect(builder.joinAll(['a', '/b', '/c', 'd']), '/c/d');
+      expect(builder.joinAll(['a', r'c:\b', 'c', 'd']), r'a/c:\b/c/d');
+      expect(builder.joinAll(['a', r'\\b', 'c', 'd']), r'a/\\b/c/d');
+    });
+
+    test('preserves roots before a root-relative path', () {
+      expect(builder.joinAll(['http://dartlang.org', 'a', '/b', 'c']),
+          'http://dartlang.org/b/c');
+      expect(builder.joinAll(['file://', 'a', '/b', 'c']), 'file:///b/c');
+      expect(builder.joinAll(['file://', 'a', '/b', 'c', '/d']), 'file:///d');
+    });
+  });
+
+  group('split', () {
+    test('simple cases', () {
+      expect(builder.split(''), []);
+      expect(builder.split('.'), ['.']);
+      expect(builder.split('..'), ['..']);
+      expect(builder.split('foo'), equals(['foo']));
+      expect(builder.split('foo/bar.txt'), equals(['foo', 'bar.txt']));
+      expect(builder.split('foo/bar/baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split('foo/../bar/./baz'),
+          equals(['foo', '..', 'bar', '.', 'baz']));
+      expect(builder.split('foo//bar///baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split('foo/\\/baz'), equals(['foo', '\\', 'baz']));
+      expect(builder.split('.'), equals(['.']));
+      expect(builder.split(''), equals([]));
+      expect(builder.split('foo/'), equals(['foo']));
+      expect(builder.split('http://dartlang.org//'),
+          equals(['http://dartlang.org']));
+      expect(builder.split('file:////'), equals(['file://']));
+      expect(builder.split('//'), equals(['/']));
+    });
+
+    test('includes the root for absolute paths', () {
+      expect(builder.split('http://dartlang.org/foo/bar/baz'),
+          equals(['http://dartlang.org', 'foo', 'bar', 'baz']));
+      expect(builder.split('file:///foo/bar/baz'),
+          equals(['file://', 'foo', 'bar', 'baz']));
+      expect(builder.split('/foo/bar/baz'), equals(['/', 'foo', 'bar', 'baz']));
+      expect(builder.split('http://dartlang.org/'),
+          equals(['http://dartlang.org']));
+      expect(builder.split('http://dartlang.org'),
+          equals(['http://dartlang.org']));
+      expect(builder.split('file:///'), equals(['file://']));
+      expect(builder.split('file://'), equals(['file://']));
+      expect(builder.split('/'), equals(['/']));
+    });
+  });
+
+  group('normalize', () {
+    test('simple cases', () {
+      expect(builder.normalize(''), '');
+      expect(builder.normalize('.'), '.');
+      expect(builder.normalize('..'), '..');
+      expect(builder.normalize('a'), 'a');
+      expect(builder.normalize('http://dartlang.org/'), 'http://dartlang.org');
+      expect(builder.normalize('http://dartlang.org'), 'http://dartlang.org');
+      expect(builder.normalize('file://'), 'file://');
+      expect(builder.normalize('file:///'), 'file://');
+      expect(builder.normalize('/'), '/');
+      expect(builder.normalize(r'\'), r'\');
+    });
+
+    test('collapses redundant separators', () {
+      expect(builder.normalize(r'a/b/c'), r'a/b/c');
+      expect(builder.normalize(r'a//b///c////d'), r'a/b/c/d');
+    });
+
+    test('does not collapse separators for other platform', () {
+      expect(builder.normalize(r'a\\b\\\c'), r'a\\b\\\c');
+    });
+
+    test('eliminates "." parts', () {
+      expect(builder.normalize('./'), '.');
+      expect(builder.normalize('http://dartlang.org/.'), 'http://dartlang.org');
+      expect(builder.normalize('file:///.'), 'file://');
+      expect(builder.normalize('/.'), '/');
+      expect(builder.normalize('http://dartlang.org/./'),
+          'http://dartlang.org');
+      expect(builder.normalize('file:///./'), 'file://');
+      expect(builder.normalize('/./'), '/');
+      expect(builder.normalize('./.'), '.');
+      expect(builder.normalize('a/./b'), 'a/b');
+      expect(builder.normalize('a/.b/c'), 'a/.b/c');
+      expect(builder.normalize('a/././b/./c'), 'a/b/c');
+      expect(builder.normalize('././a'), 'a');
+      expect(builder.normalize('a/./.'), 'a');
+    });
+
+    test('eliminates ".." parts', () {
+      expect(builder.normalize('..'), '..');
+      expect(builder.normalize('../'), '..');
+      expect(builder.normalize('../../..'), '../../..');
+      expect(builder.normalize('../../../'), '../../..');
+      expect(builder.normalize('http://dartlang.org/..'),
+          'http://dartlang.org');
+      expect(builder.normalize('file:///..'), 'file://');
+      expect(builder.normalize('/..'), '/');
+      expect(builder.normalize('http://dartlang.org/../../..'),
+          'http://dartlang.org');
+      expect(builder.normalize('file:///../../..'), 'file://');
+      expect(builder.normalize('/../../..'), '/');
+      expect(builder.normalize('http://dartlang.org/../../../a'),
+          'http://dartlang.org/a');
+      expect(builder.normalize('file:///../../../a'), 'file:///a');
+      expect(builder.normalize('/../../../a'), '/a');
+      expect(builder.normalize('a/..'), '.');
+      expect(builder.normalize('a/b/..'), 'a');
+      expect(builder.normalize('a/../b'), 'b');
+      expect(builder.normalize('a/./../b'), 'b');
+      expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
+      expect(builder.normalize('a/b/../../../../c'), '../../c');
+    });
+
+    test('does not walk before root on absolute paths', () {
+      expect(builder.normalize('..'), '..');
+      expect(builder.normalize('../'), '..');
+      expect(builder.normalize('http://dartlang.org/..'),
+          'http://dartlang.org');
+      expect(builder.normalize('file:///..'), 'file://');
+      expect(builder.normalize('/..'), '/');
+      expect(builder.normalize('a/..'), '.');
+      expect(builder.normalize('a/b/..'), 'a');
+      expect(builder.normalize('a/../b'), 'b');
+      expect(builder.normalize('a/./../b'), 'b');
+      expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
+      expect(builder.normalize('a/b/../../../../c'), '../../c');
+    });
+
+    test('removes trailing separators', () {
+      expect(builder.normalize('./'), '.');
+      expect(builder.normalize('.//'), '.');
+      expect(builder.normalize('a/'), 'a');
+      expect(builder.normalize('a/b/'), 'a/b');
+      expect(builder.normalize('a/b///'), 'a/b');
+    });
+  });
+
+  group('relative', () {
+    group('from absolute root', () {
+      test('given absolute path in root', () {
+        expect(builder.relative('http://dartlang.org'), '../..');
+        expect(builder.relative('http://dartlang.org/'), '../..');
+        expect(builder.relative('/'), '../..');
+        expect(builder.relative('http://dartlang.org/root'), '..');
+        expect(builder.relative('/root'), '..');
+        expect(builder.relative('http://dartlang.org/root/path'), '.');
+        expect(builder.relative('/root/path'), '.');
+        expect(builder.relative('http://dartlang.org/root/path/a'), 'a');
+        expect(builder.relative('/root/path/a'), 'a');
+        expect(builder.relative('http://dartlang.org/root/path/a/b.txt'),
+            'a/b.txt');
+        expect(builder.relative('/root/path/a/b.txt'), 'a/b.txt');
+        expect(builder.relative('http://dartlang.org/root/a/b.txt'),
+            '../a/b.txt');
+        expect(builder.relative('/root/a/b.txt'), '../a/b.txt');
+      });
+
+      test('given absolute path outside of root', () {
+        expect(builder.relative('http://dartlang.org/a/b'), '../../a/b');
+        expect(builder.relative('/a/b'), '../../a/b');
+        expect(builder.relative('http://dartlang.org/root/path/a'), 'a');
+        expect(builder.relative('/root/path/a'), 'a');
+        expect(builder.relative('http://dartlang.org/root/path/a/b.txt'),
+            'a/b.txt');
+        expect(builder.relative('http://dartlang.org/root/path/a/b.txt'),
+            'a/b.txt');
+        expect(builder.relative('http://dartlang.org/root/a/b.txt'),
+            '../a/b.txt');
+      });
+
+      test('given absolute path with different hostname/protocol', () {
+        expect(builder.relative(r'http://google.com/a/b'),
+            r'http://google.com/a/b');
+        expect(builder.relative(r'file:///a/b'),
+            r'file:///a/b');
+      });
+
+      test('given relative path', () {
+        // The path is considered relative to the root, so it basically just
+        // normalizes.
+        expect(builder.relative(''), '.');
+        expect(builder.relative('.'), '.');
+        expect(builder.relative('a'), 'a');
+        expect(builder.relative('a/b.txt'), 'a/b.txt');
+        expect(builder.relative('../a/b.txt'), '../a/b.txt');
+        expect(builder.relative('a/./b/../c.txt'), 'a/c.txt');
+      });
+
+      // Regression
+      test('from root-only path', () {
+        expect(builder.relative('http://dartlang.org',
+                from: 'http://dartlang.org'),
+            '.');
+        expect(builder.relative('http://dartlang.org/root/path',
+                from: 'http://dartlang.org'),
+            'root/path');
+      });
+    });
+
+    group('from relative root', () {
+      var r = new path.Builder(style: path.Style.url, root: 'foo/bar');
+
+      test('given absolute path', () {
+        expect(r.relative('http://google.com/'), equals('http://google.com'));
+        expect(r.relative('http://google.com'), equals('http://google.com'));
+        expect(r.relative('file:///'), equals('file://'));
+        expect(r.relative('file://'), equals('file://'));
+        expect(r.relative('/'), equals('/'));
+        expect(r.relative('/a/b'), equals('/a/b'));
+      });
+
+      test('given relative path', () {
+        // The path is considered relative to the root, so it basically just
+        // normalizes.
+        expect(r.relative(''), '.');
+        expect(r.relative('.'), '.');
+        expect(r.relative('..'), '..');
+        expect(r.relative('a'), 'a');
+        expect(r.relative('a/b.txt'), 'a/b.txt');
+        expect(r.relative('../a/b.txt'), '../a/b.txt');
+        expect(r.relative('a/./b/../c.txt'), 'a/c.txt');
+      });
+    });
+
+    group('from root-relative root', () {
+      var r = new path.Builder(style: path.Style.url, root: '/foo/bar');
+
+      test('given absolute path', () {
+        expect(r.relative('http://google.com/'), equals('http://google.com'));
+        expect(r.relative('http://google.com'), equals('http://google.com'));
+        expect(r.relative('file:///'), equals('file://'));
+        expect(r.relative('file://'), equals('file://'));
+        expect(r.relative('/'), equals('../..'));
+        expect(r.relative('/a/b'), equals('../../a/b'));
+      });
+
+      test('given relative path', () {
+        // The path is considered relative to the root, so it basically just
+        // normalizes.
+        expect(r.relative(''), '.');
+        expect(r.relative('.'), '.');
+        expect(r.relative('..'), '..');
+        expect(r.relative('a'), 'a');
+        expect(r.relative('a/b.txt'), 'a/b.txt');
+        expect(r.relative('../a/b.txt'), '../a/b.txt');
+        expect(r.relative('a/./b/../c.txt'), 'a/c.txt');
+      });
+    });
+
+    test('from a root with extension', () {
+      var r = new path.Builder(style: path.Style.url, root: '/dir.ext');
+      expect(r.relative('/dir.ext/file'), 'file');
+    });
+
+    test('with a root parameter', () {
+      expect(builder.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz'));
+      expect(
+          builder.relative('/foo/bar/baz', from: 'http://dartlang.org/foo/bar'),
+          equals('baz'));
+      expect(
+          builder.relative('http://dartlang.org/foo/bar/baz', from: '/foo/bar'),
+          equals('baz'));
+      expect(builder.relative('http://dartlang.org/foo/bar/baz',
+              from: 'file:///foo/bar'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(builder.relative('http://dartlang.org/foo/bar/baz',
+          from: 'http://dartlang.org/foo/bar'), equals('baz'));
+      expect(
+          builder.relative('/foo/bar/baz', from: 'file:///foo/bar'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(
+          builder.relative('file:///foo/bar/baz', from: '/foo/bar'),
+          equals('file:///foo/bar/baz'));
+
+      expect(builder.relative('..', from: '/foo/bar'), equals('../../root'));
+      expect(builder.relative('..', from: 'http://dartlang.org/foo/bar'),
+          equals('../../root'));
+      expect(builder.relative('..', from: 'file:///foo/bar'),
+          equals('http://dartlang.org/root'));
+      expect(builder.relative('..', from: '/foo/bar'), equals('../../root'));
+
+      expect(builder.relative('http://dartlang.org/foo/bar/baz',
+              from: 'foo/bar'),
+          equals('../../../../foo/bar/baz'));
+      expect(builder.relative('file:///foo/bar/baz', from: 'foo/bar'),
+          equals('file:///foo/bar/baz'));
+      expect(builder.relative('/foo/bar/baz', from: 'foo/bar'),
+          equals('../../../../foo/bar/baz'));
+
+      expect(builder.relative('..', from: 'foo/bar'), equals('../../..'));
+    });
+
+    test('with a root parameter and a relative root', () {
+      var r = new path.Builder(style: path.Style.url, root: 'relative/root');
+      expect(r.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz'));
+      expect(
+          r.relative('/foo/bar/baz', from: 'http://dartlang.org/foo/bar'),
+          equals('/foo/bar/baz'));
+      expect(
+          r.relative('http://dartlang.org/foo/bar/baz', from: '/foo/bar'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(r.relative('http://dartlang.org/foo/bar/baz',
+              from: 'file:///foo/bar'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(r.relative('http://dartlang.org/foo/bar/baz',
+          from: 'http://dartlang.org/foo/bar'), equals('baz'));
+
+      expect(r.relative('http://dartlang.org/foo/bar/baz', from: 'foo/bar'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(r.relative('file:///foo/bar/baz', from: 'foo/bar'),
+          equals('file:///foo/bar/baz'));
+      expect(r.relative('/foo/bar/baz', from: 'foo/bar'),
+          equals('/foo/bar/baz'));
+
+      expect(r.relative('..', from: 'foo/bar'), equals('../../..'));
+    });
+
+    test('from a . root', () {
+      var r = new path.Builder(style: path.Style.url, root: '.');
+      expect(r.relative('http://dartlang.org/foo/bar/baz'),
+          equals('http://dartlang.org/foo/bar/baz'));
+      expect(r.relative('file:///foo/bar/baz'), equals('file:///foo/bar/baz'));
+      expect(r.relative('/foo/bar/baz'), equals('/foo/bar/baz'));
+      expect(r.relative('foo/bar/baz'), equals('foo/bar/baz'));
+    });
+  });
+
+  group('resolve', () {
+    test('allows up to seven parts', () {
+      expect(builder.resolve('a'), 'http://dartlang.org/root/path/a');
+      expect(builder.resolve('a', 'b'), 'http://dartlang.org/root/path/a/b');
+      expect(builder.resolve('a', 'b', 'c'),
+          'http://dartlang.org/root/path/a/b/c');
+      expect(builder.resolve('a', 'b', 'c', 'd'),
+          'http://dartlang.org/root/path/a/b/c/d');
+      expect(builder.resolve('a', 'b', 'c', 'd', 'e'),
+          'http://dartlang.org/root/path/a/b/c/d/e');
+      expect(builder.resolve('a', 'b', 'c', 'd', 'e', 'f'),
+          'http://dartlang.org/root/path/a/b/c/d/e/f');
+      expect(builder.resolve('a', 'b', 'c', 'd', 'e', 'f', 'g'),
+          'http://dartlang.org/root/path/a/b/c/d/e/f/g');
+    });
+
+    test('does not add separator if a part ends in one', () {
+      expect(builder.resolve('a/', 'b', 'c/', 'd'),
+          'http://dartlang.org/root/path/a/b/c/d');
+      expect(builder.resolve(r'a\', 'b'),
+          r'http://dartlang.org/root/path/a\/b');
+    });
+
+    test('ignores parts before an absolute path', () {
+      expect(builder.resolve('a', '/b', '/c', 'd'), 'http://dartlang.org/c/d');
+      expect(builder.resolve('a', '/b', 'file:///c', 'd'), 'file:///c/d');
+      expect(builder.resolve('a', r'c:\b', 'c', 'd'),
+          r'http://dartlang.org/root/path/a/c:\b/c/d');
+      expect(builder.resolve('a', r'\\b', 'c', 'd'),
+          r'http://dartlang.org/root/path/a/\\b/c/d');
+    });
+  });
+
+  test('withoutExtension', () {
+    expect(builder.withoutExtension(''), '');
+    expect(builder.withoutExtension('a'), 'a');
+    expect(builder.withoutExtension('.a'), '.a');
+    expect(builder.withoutExtension('a.b'), 'a');
+    expect(builder.withoutExtension('a/b.c'), 'a/b');
+    expect(builder.withoutExtension('a/b.c.d'), 'a/b.c');
+    expect(builder.withoutExtension('a/'), 'a/');
+    expect(builder.withoutExtension('a/b/'), 'a/b/');
+    expect(builder.withoutExtension('a/.'), 'a/.');
+    expect(builder.withoutExtension('a/.b'), 'a/.b');
+    expect(builder.withoutExtension('a.b/c'), 'a.b/c');
+    expect(builder.withoutExtension(r'a.b\c'), r'a');
+    expect(builder.withoutExtension(r'a/b\c'), r'a/b\c');
+    expect(builder.withoutExtension(r'a/b\c.d'), r'a/b\c');
+    expect(builder.withoutExtension('a/b.c/'), 'a/b/');
+    expect(builder.withoutExtension('a/b.c//'), 'a/b//');
+  });
+
+
+  test('fromUri', () {
+    expect(builder.fromUri(Uri.parse('http://dartlang.org/path/to/foo')),
+        'http://dartlang.org/path/to/foo');
+    expect(builder.fromUri(Uri.parse('http://dartlang.org/path/to/foo/')),
+        'http://dartlang.org/path/to/foo/');
+    expect(builder.fromUri(Uri.parse('file:///path/to/foo')),
+        'file:///path/to/foo');
+    expect(builder.fromUri(Uri.parse('foo/bar')), 'foo/bar');
+    expect(builder.fromUri(Uri.parse('http://dartlang.org/path/to/foo%23bar')),
+        'http://dartlang.org/path/to/foo%23bar');
+  });
+
+  test('toUri', () {
+    expect(builder.toUri('http://dartlang.org/path/to/foo'),
+        Uri.parse('http://dartlang.org/path/to/foo'));
+    expect(builder.toUri('http://dartlang.org/path/to/foo/'),
+        Uri.parse('http://dartlang.org/path/to/foo/'));
+    expect(builder.toUri('file:///path/to/foo'),
+        Uri.parse('file:///path/to/foo'));
+    expect(builder.toUri('foo/bar'), Uri.parse('foo/bar'));
+    expect(builder.toUri('http://dartlang.org/path/to/foo%23bar'),
+        Uri.parse('http://dartlang.org/path/to/foo%23bar'));
+  });
+}
diff --git a/test/path_windows_test.dart b/test/windows_test.dart
similarity index 89%
rename from test/path_windows_test.dart
rename to test/windows_test.dart
index 8164224..7b3cf60 100644
--- a/test/path_windows_test.dart
+++ b/test/windows_test.dart
@@ -2,9 +2,7 @@
 // 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.
 
-library path_test;
-
-import 'dart:io' as io;
+library path.test.windows_test;
 
 import 'package:unittest/unittest.dart';
 import 'package:path/path.dart' as path;
@@ -333,6 +331,12 @@
         expect(builder.relative(r'..\a\b.txt'), r'..\a\b.txt');
         expect(builder.relative(r'a\.\b\..\c.txt'), r'a\c.txt');
       });
+
+      // Regression
+      test('from root-only path', () {
+        expect(builder.relative(r'C:\', from: r'C:\'), '.');
+        expect(builder.relative(r'C:\root\path', from: r'C:\'), r'root\path');
+      });
     });
 
     group('from relative root', () {
@@ -385,6 +389,12 @@
       expect(builder.relative(r'D:\a\b'), r'D:\a\b');
       expect(builder.relative(r'\\a\b'), r'\\a\b');
     });
+
+    test('from a . root', () {
+      var r = new path.Builder(style: path.Style.windows, root: '.');
+      expect(r.relative(r'C:\foo\bar\baz'), equals(r'C:\foo\bar\baz'));
+      expect(r.relative(r'foo\bar\baz'), equals(r'foo\bar\baz'));
+    });
   });
 
   group('resolve', () {
@@ -431,4 +441,38 @@
     expect(builder.withoutExtension(r'a.b/c'), r'a.b/c');
     expect(builder.withoutExtension(r'a\b.c\'), r'a\b\');
   });
+
+  test('fromUri', () {
+    expect(builder.fromUri(Uri.parse('file:///C:/path/to/foo')),
+        r'C:\path\to\foo');
+    expect(builder.fromUri(Uri.parse('file://hostname/path/to/foo')),
+        r'\\hostname\path\to\foo');
+    expect(builder.fromUri(Uri.parse('file:///C:/')), r'C:\');
+    expect(builder.fromUri(Uri.parse('file://hostname/')), r'\\hostname\');
+    expect(builder.fromUri(Uri.parse('foo/bar')), r'foo\bar');
+    expect(builder.fromUri(Uri.parse('/C:/path/to/foo')), r'C:\path\to\foo');
+    expect(builder.fromUri(Uri.parse('///C:/path/to/foo')), r'C:\path\to\foo');
+    expect(builder.fromUri(Uri.parse('//hostname/path/to/foo')),
+        r'\\hostname\path\to\foo');
+    expect(builder.fromUri(Uri.parse('file:///C:/path/to/foo%23bar')),
+        r'C:\path\to\foo#bar');
+    expect(builder.fromUri(Uri.parse('file://hostname/path/to/foo%23bar')),
+        r'\\hostname\path\to\foo#bar');
+    expect(() => builder.fromUri(Uri.parse('http://dartlang.org')),
+        throwsArgumentError);
+  });
+
+  test('toUri', () {
+    expect(builder.toUri(r'C:\path\to\foo'),
+        Uri.parse('file:///C:/path/to/foo'));
+    expect(builder.toUri(r'C:\path\to\foo\'),
+        Uri.parse('file:///C:/path/to/foo/'));
+    expect(builder.toUri(r'C:\'), Uri.parse('file:///C:/'));
+    expect(builder.toUri(r'\\hostname\'), Uri.parse('file://hostname/'));
+    expect(builder.toUri(r'foo\bar'), Uri.parse('foo/bar'));
+    expect(builder.toUri(r'C:\path\to\foo#bar'),
+        Uri.parse('file:///C:/path/to/foo%23bar'));
+    expect(builder.toUri(r'\\hostname\path\to\foo#bar'),
+        Uri.parse('file://hostname/path/to/foo%23bar'));
+  });
 }