| Index: sdk/lib/core/uri.dart | 
| diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart | 
| index 456bcb7589769a20d1e84d41f915517caa11b7f0..51d66cfa8052a64fb1ad6235f4afee31e1c4a918 100644 | 
| --- a/sdk/lib/core/uri.dart | 
| +++ b/sdk/lib/core/uri.dart | 
| @@ -4,6 +4,24 @@ | 
|  | 
| part of dart.core; | 
|  | 
| +// Frequently used character codes. | 
| +const int _SPACE = 0x20; | 
| +const int _PERCENT = 0x25; | 
| +const int _PLUS = 0x2B; | 
| +const int _DOT = 0x2E; | 
| +const int _SLASH = 0x2F; | 
| +const int _COLON = 0x3A; | 
| +const int _UPPER_CASE_A = 0x41; | 
| +const int _UPPER_CASE_Z = 0x5A; | 
| +const int _LEFT_BRACKET = 0x5B; | 
| +const int _BACKSLASH = 0x5C; | 
| +const int _RIGHT_BRACKET = 0x5D; | 
| +const int _LOWER_CASE_A = 0x61; | 
| +const int _LOWER_CASE_F = 0x66; | 
| +const int _LOWER_CASE_Z = 0x7A; | 
| + | 
| +const String _hexDigits = "0123456789ABCDEF"; | 
| + | 
| /** | 
| * A parsed URI, such as a URL. | 
| * | 
| @@ -15,77 +33,17 @@ part of dart.core; | 
| * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris | 
| * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html | 
| */ | 
| -class Uri { | 
| -  /** | 
| -   * The scheme component of the URI. | 
| -   * | 
| -   * Returns the empty string if there is no scheme component. | 
| -   * | 
| -   * A URI scheme is case insensitive. | 
| -   * The returned scheme is canonicalized to lowercase letters. | 
| -   */ | 
| -  // We represent the missing scheme as an empty string. | 
| -  // A valid scheme cannot be empty. | 
| -  final String scheme; | 
| - | 
| -  /** | 
| -   * The user-info part of the authority. | 
| -   * | 
| -   * Does not distinguish between an empty user-info and an absent one. | 
| -   * The value is always non-null. | 
| -   * Is considered absent if [_host] is `null`. | 
| -   */ | 
| -  final String _userInfo; | 
| - | 
| -  /** | 
| -   * The host name of the URI. | 
| -   * | 
| -   * Set to `null` if there is no authority in the URI. | 
| -   * The host name is the only mandatory part of an authority, so we use | 
| -   * it to mark whether an authority part was present or not. | 
| -   */ | 
| -  final String _host; | 
| - | 
| +abstract class Uri { | 
| /** | 
| -   * The port number part of the authority. | 
| +   * Returns the natural base URI for the current platform. | 
| * | 
| -   * The port. Set to null if there is no port. Normalized to null if | 
| -   * the port is the default port for the scheme. | 
| -   */ | 
| -  int _port; | 
| - | 
| -  /** | 
| -   * The path of the URI. | 
| +   * When running in a browser this is the current URL of the current page | 
| +   * (from `window.location.href`). | 
| * | 
| -   * Always non-null. | 
| -   */ | 
| -  String _path; | 
| - | 
| -  // The query content, or null if there is no query. | 
| -  final String _query; | 
| - | 
| -  // The fragment content, or null if there is no fragment. | 
| -  final String _fragment; | 
| - | 
| -  /** | 
| -   * Cache the computed return value of [pathSegements]. | 
| -   */ | 
| -  List<String> _pathSegments; | 
| - | 
| -  /** | 
| -   * Cache the computed return value of [queryParameters]. | 
| +   * When not running in a browser this is the file URI referencing | 
| +   * the current working directory. | 
| */ | 
| -  Map<String, String> _queryParameters; | 
| -  Map<String, List<String>> _queryParameterLists; | 
| - | 
| -  /// Internal non-verifying constructor. Only call with validated arguments. | 
| -  Uri._internal(this.scheme, | 
| -                this._userInfo, | 
| -                this._host, | 
| -                this._port, | 
| -                this._path, | 
| -                this._query, | 
| -                this._fragment); | 
| +  external static Uri get base; | 
|  | 
| /** | 
| * Creates a new URI from its components. | 
| @@ -158,39 +116,15 @@ class Uri { | 
| * general delimiters, are escaped if necessary. | 
| * If `fragment` is omitted or `null`, the URI has no fragment part. | 
| */ | 
| -  factory Uri({String scheme : "", | 
| -               String userInfo : "", | 
| +  factory Uri({String scheme, | 
| +               String userInfo, | 
| String host, | 
| int port, | 
| String path, | 
| Iterable<String> pathSegments, | 
| String query, | 
| Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| -               String fragment}) { | 
| -    scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | 
| -    userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | 
| -    host = _makeHost(host, 0, _stringOrNullLength(host), false); | 
| -    // Special case this constructor for backwards compatibility. | 
| -    if (query == "") query = null; | 
| -    query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 
| -    fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | 
| -    port = _makePort(port, scheme); | 
| -    bool isFile = (scheme == "file"); | 
| -    if (host == null && | 
| -        (userInfo.isNotEmpty || port != null || isFile)) { | 
| -      host = ""; | 
| -    } | 
| -    bool hasAuthority = (host != null); | 
| -    path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 
| -                     scheme, hasAuthority); | 
| -    if (scheme.isEmpty && host == null && !path.startsWith('/')) { | 
| -      path = _normalizeRelativePath(path); | 
| -    } else { | 
| -      path = _removeDotSegments(path); | 
| -    } | 
| -    return new Uri._internal(scheme, userInfo, host, port, | 
| -                             path, query, fragment); | 
| -  } | 
| +               String fragment}) = _Uri; | 
|  | 
| /** | 
| * Creates a new `http` URI from authority, path and query. | 
| @@ -227,9 +161,7 @@ class Uri { | 
| */ | 
| factory Uri.http(String authority, | 
| String unencodedPath, | 
| -                   [Map<String, String> queryParameters]) { | 
| -    return _makeHttpUri("http", authority, unencodedPath, queryParameters); | 
| -  } | 
| +                   [Map<String, String> queryParameters]) = _Uri.http; | 
|  | 
| /** | 
| * Creates a new `https` URI from authority, path and query. | 
| @@ -239,531 +171,106 @@ class Uri { | 
| */ | 
| factory Uri.https(String authority, | 
| String unencodedPath, | 
| -                    [Map<String, String> queryParameters]) { | 
| -    return _makeHttpUri("https", authority, unencodedPath, queryParameters); | 
| -  } | 
| +                    [Map<String, String> queryParameters]) = _Uri.https; | 
|  | 
| /** | 
| -   * Returns the authority component. | 
| +   * Creates a new file URI from an absolute or relative file path. | 
| * | 
| -   * The authority is formatted from the [userInfo], [host] and [port] | 
| -   * parts. | 
| +   * The file path is passed in [path]. | 
| * | 
| -   * Returns the empty string if there is no authority component. | 
| -   */ | 
| -  String get authority { | 
| -    if (!hasAuthority) return ""; | 
| -    var sb = new StringBuffer(); | 
| -    _writeAuthority(sb); | 
| -    return sb.toString(); | 
| -  } | 
| - | 
| -  /** | 
| -   * Returns the user info part of the authority component. | 
| +   * This path is interpreted using either Windows or non-Windows | 
| +   * semantics. | 
| * | 
| -   * Returns the empty string if there is no user info in the | 
| -   * authority component. | 
| -   */ | 
| -  String get userInfo => _userInfo; | 
| - | 
| -  /** | 
| -   * Returns the host part of the authority component. | 
| +   * With non-Windows semantics the slash ("/") is used to separate | 
| +   * path segments. | 
| * | 
| -   * Returns the empty string if there is no authority component and | 
| -   * hence no host. | 
| +   * With Windows semantics, backslash ("\\") and forward-slash ("/") | 
| +   * are used to separate path segments, except if the path starts | 
| +   * with "\\\\?\\" in which case, only backslash ("\\") separates path | 
| +   * segments. | 
| * | 
| -   * If the host is an IP version 6 address, the surrounding `[` and `]` is | 
| -   * removed. | 
| +   * If the path starts with a path separator an absolute URI is | 
| +   * created. Otherwise a relative URI is created. One exception from | 
| +   * this rule is that when Windows semantics is used and the path | 
| +   * starts with a drive letter followed by a colon (":") and a | 
| +   * path separator then an absolute URI is created. | 
| * | 
| -   * The host string is case-insensitive. | 
| -   * The returned host name is canonicalized to lower-case | 
| -   * with upper-case percent-escapes. | 
| -   */ | 
| -  String get host { | 
| -    if (_host == null) return ""; | 
| -    if (_host.startsWith('[')) { | 
| -      return _host.substring(1, _host.length - 1); | 
| -    } | 
| -    return _host; | 
| -  } | 
| - | 
| -  /** | 
| -   * Returns the port part of the authority component. | 
| +   * The default for whether to use Windows or non-Windows semantics | 
| +   * determined from the platform Dart is running on. When running in | 
| +   * the standalone VM this is detected by the VM based on the | 
| +   * operating system. When running in a browser non-Windows semantics | 
| +   * is always used. | 
| * | 
| -   * Returns the defualt port if there is no port number in the authority | 
| -   * component. That's 80 for http, 443 for https, and 0 for everything else. | 
| -   */ | 
| -  int get port { | 
| -    if (_port == null) return _defaultPort(scheme); | 
| -    return _port; | 
| -  } | 
| - | 
| -  // The default port for the scheme of this Uri.. | 
| -  static int _defaultPort(String scheme) { | 
| -    if (scheme == "http") return 80; | 
| -    if (scheme == "https") return 443; | 
| -    return 0; | 
| -  } | 
| - | 
| -  /** | 
| -   * Returns the path component. | 
| +   * To override the automatic detection of which semantics to use pass | 
| +   * a value for [windows]. Passing `true` will use Windows | 
| +   * semantics and passing `false` will use non-Windows semantics. | 
| * | 
| -   * The returned path is encoded. To get direct access to the decoded | 
| -   * path use [pathSegments]. | 
| +   * Examples using non-Windows semantics: | 
| * | 
| -   * Returns the empty string if there is no path component. | 
| -   */ | 
| -  String get path => _path; | 
| - | 
| -  /** | 
| -   * Returns the query component. The returned query is encoded. To get | 
| -   * direct access to the decoded query use [queryParameters]. | 
| +   * ``` | 
| +   * // xxx/yyy | 
| +   * new Uri.file("xxx/yyy", windows: false); | 
| * | 
| -   * Returns the empty string if there is no query component. | 
| +   * // xxx/yyy/ | 
| +   * new Uri.file("xxx/yyy/", windows: false); | 
| +   * | 
| +   * // file:///xxx/yyy | 
| +   * new Uri.file("/xxx/yyy", windows: false); | 
| +   * | 
| +   * // file:///xxx/yyy/ | 
| +   * new Uri.file("/xxx/yyy/", windows: false); | 
| +   * | 
| +   * // C: | 
| +   * new Uri.file("C:", windows: false); | 
| +   * ``` | 
| +   * | 
| +   * Examples using Windows semantics: | 
| +   * | 
| +   * ``` | 
| +   * // xxx/yyy | 
| +   * new Uri.file(r"xxx\yyy", windows: true); | 
| +   * | 
| +   * // xxx/yyy/ | 
| +   * new Uri.file(r"xxx\yyy\", windows: true); | 
| +   * | 
| +   * file:///xxx/yyy | 
| +   * new Uri.file(r"\xxx\yyy", windows: true); | 
| +   * | 
| +   * file:///xxx/yyy/ | 
| +   * new Uri.file(r"\xxx\yyy/", windows: true); | 
| +   * | 
| +   * // file:///C:/xxx/yyy | 
| +   * new Uri.file(r"C:\xxx\yyy", windows: true); | 
| +   * | 
| +   * // This throws an error. A path with a drive letter is not absolute. | 
| +   * new Uri.file(r"C:", windows: true); | 
| +   * | 
| +   * // This throws an error. A path with a drive letter is not absolute. | 
| +   * new Uri.file(r"C:xxx\yyy", windows: true); | 
| +   * | 
| +   * // file://server/share/file | 
| +   * new Uri.file(r"\\server\share\file", windows: true); | 
| +   * ``` | 
| +   * | 
| +   * If the path passed is not a legal file path [ArgumentError] is thrown. | 
| */ | 
| -  String get query => (_query == null) ? "" : _query; | 
| +  factory Uri.file(String path, {bool windows}) = _Uri.file; | 
|  | 
| /** | 
| -   * Returns the fragment identifier component. | 
| +   * Like [Uri.file] except that a non-empty URI path ends in a slash. | 
| * | 
| -   * Returns the empty string if there is no fragment identifier | 
| -   * component. | 
| +   * If [path] is not empty, and it doesn't end in a directory separator, | 
| +   * then a slash is added to the returned URI's path. | 
| +   * In all other cases, the result is the same as returned by `Uri.file`. | 
| */ | 
| -  String get fragment => (_fragment == null) ? "" : _fragment; | 
| +  factory Uri.directory(String path, {bool windows}) = _Uri.directory; | 
|  | 
| /** | 
| -   * Creates a new `Uri` object by parsing a URI string. | 
| +   * Creates a `data:` URI containing the [content] string. | 
| * | 
| -   * If [start] and [end] are provided, only the substring from `start` | 
| -   * to `end` is parsed as a URI. | 
| -   * | 
| -   * If the string is not valid as a URI or URI reference, | 
| -   * a [FormatException] is thrown. | 
| -   */ | 
| -  static Uri parse(String uri, [int start = 0, int end]) { | 
| -    // This parsing will not validate percent-encoding, IPv6, etc. | 
| -    // When done splitting into parts, it will call, e.g., [_makeFragment] | 
| -    // to do the final parsing. | 
| -    // | 
| -    // Important parts of the RFC 3986 used here: | 
| -    // URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ] | 
| -    // | 
| -    // hier-part     = "//" authority path-abempty | 
| -    //               / path-absolute | 
| -    //               / path-rootless | 
| -    //               / path-empty | 
| -    // | 
| -    // URI-reference = URI / relative-ref | 
| -    // | 
| -    // absolute-URI  = scheme ":" hier-part [ "?" query ] | 
| -    // | 
| -    // relative-ref  = relative-part [ "?" query ] [ "#" fragment ] | 
| -    // | 
| -    // relative-part = "//" authority path-abempty | 
| -    //               / path-absolute | 
| -    //               / path-noscheme | 
| -    //               / path-empty | 
| -    // | 
| -    // scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) | 
| -    // | 
| -    // authority     = [ userinfo "@" ] host [ ":" port ] | 
| -    // userinfo      = *( unreserved / pct-encoded / sub-delims / ":" ) | 
| -    // host          = IP-literal / IPv4address / reg-name | 
| -    // port          = *DIGIT | 
| -    // reg-name      = *( unreserved / pct-encoded / sub-delims ) | 
| -    // | 
| -    // path          = path-abempty    ; begins with "/" or is empty | 
| -    //               / path-absolute   ; begins with "/" but not "//" | 
| -    //               / path-noscheme   ; begins with a non-colon segment | 
| -    //               / path-rootless   ; begins with a segment | 
| -    //               / path-empty      ; zero characters | 
| -    // | 
| -    // path-abempty  = *( "/" segment ) | 
| -    // path-absolute = "/" [ segment-nz *( "/" segment ) ] | 
| -    // path-noscheme = segment-nz-nc *( "/" segment ) | 
| -    // path-rootless = segment-nz *( "/" segment ) | 
| -    // path-empty    = 0<pchar> | 
| -    // | 
| -    // segment       = *pchar | 
| -    // segment-nz    = 1*pchar | 
| -    // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 
| -    //               ; non-zero-length segment without any colon ":" | 
| -    // | 
| -    // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@" | 
| -    // | 
| -    // query         = *( pchar / "/" / "?" ) | 
| -    // | 
| -    // fragment      = *( pchar / "/" / "?" ) | 
| -    const int EOI = -1; | 
| - | 
| -    String scheme = ""; | 
| -    String userinfo = ""; | 
| -    String host = null; | 
| -    int port = null; | 
| -    String path = null; | 
| -    String query = null; | 
| -    String fragment = null; | 
| -    if (end == null) end = uri.length; | 
| - | 
| -    int index = start; | 
| -    int pathStart = start; | 
| -    // End of input-marker. | 
| -    int char = EOI; | 
| - | 
| -    void parseAuth() { | 
| -      if (index == end) { | 
| -        char = EOI; | 
| -        return; | 
| -      } | 
| -      int authStart = index; | 
| -      int lastColon = -1; | 
| -      int lastAt = -1; | 
| -      char = uri.codeUnitAt(index); | 
| -      while (index < end) { | 
| -        char = uri.codeUnitAt(index); | 
| -        if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { | 
| -          break; | 
| -        } | 
| -        if (char == _AT_SIGN) { | 
| -          lastAt = index; | 
| -          lastColon = -1; | 
| -        } else if (char == _COLON) { | 
| -          lastColon = index; | 
| -        } else if (char == _LEFT_BRACKET) { | 
| -          lastColon = -1; | 
| -          int endBracket = uri.indexOf(']', index + 1); | 
| -          if (endBracket == -1) { | 
| -            index = end; | 
| -            char = EOI; | 
| -            break; | 
| -          } else { | 
| -            index = endBracket; | 
| -          } | 
| -        } | 
| -        index++; | 
| -        char = EOI; | 
| -      } | 
| -      int hostStart = authStart; | 
| -      int hostEnd = index; | 
| -      if (lastAt >= 0) { | 
| -        userinfo = _makeUserInfo(uri, authStart, lastAt); | 
| -        hostStart = lastAt + 1; | 
| -      } | 
| -      if (lastColon >= 0) { | 
| -        int portNumber; | 
| -        if (lastColon + 1 < index) { | 
| -          portNumber = 0; | 
| -          for (int i = lastColon + 1; i < index; i++) { | 
| -            int digit = uri.codeUnitAt(i); | 
| -            if (_ZERO > digit || _NINE < digit) { | 
| -              _fail(uri, i, "Invalid port number"); | 
| -            } | 
| -            portNumber = portNumber * 10 + (digit - _ZERO); | 
| -          } | 
| -        } | 
| -        port = _makePort(portNumber, scheme); | 
| -        hostEnd = lastColon; | 
| -      } | 
| -      host = _makeHost(uri, hostStart, hostEnd, true); | 
| -      if (index < end) { | 
| -        char = uri.codeUnitAt(index); | 
| -      } | 
| -    } | 
| - | 
| -    // When reaching path parsing, the current character is known to not | 
| -    // be part of the path. | 
| -    const int NOT_IN_PATH = 0; | 
| -    // When reaching path parsing, the current character is part | 
| -    // of the a non-empty path. | 
| -    const int IN_PATH = 1; | 
| -    // When reaching authority parsing, authority is possible. | 
| -    // This is only true at start or right after scheme. | 
| -    const int ALLOW_AUTH = 2; | 
| - | 
| -    // Current state. | 
| -    // Initialized to the default value that is used when exiting the | 
| -    // scheme loop by reaching the end of input. | 
| -    // All other breaks set their own state. | 
| -    int state = NOT_IN_PATH; | 
| -    int i = index;  // Temporary alias for index to avoid bug 19550 in dart2js. | 
| -    while (i < end) { | 
| -      char = uri.codeUnitAt(i); | 
| -      if (char == _QUESTION || char == _NUMBER_SIGN) { | 
| -        state = NOT_IN_PATH; | 
| -        break; | 
| -      } | 
| -      if (char == _SLASH) { | 
| -        state = (i == start) ? ALLOW_AUTH : IN_PATH; | 
| -        break; | 
| -      } | 
| -      if (char == _COLON) { | 
| -        if (i == start) _fail(uri, start, "Invalid empty scheme"); | 
| -        scheme = _makeScheme(uri, start, i); | 
| -        i++; | 
| -        if (scheme == "data") { | 
| -          // This generates a URI that is (potentially) not path normalized. | 
| -          // Applying part normalization to a non-hierarchial URI isn't | 
| -          // meaningful. | 
| -          return UriData._parse(uri, i, null).uri; | 
| -        } | 
| -        pathStart = i; | 
| -        if (i == end) { | 
| -          char = EOI; | 
| -          state = NOT_IN_PATH; | 
| -        } else { | 
| -          char = uri.codeUnitAt(i); | 
| -          if (char == _QUESTION || char == _NUMBER_SIGN) { | 
| -            state = NOT_IN_PATH; | 
| -          } else if (char == _SLASH) { | 
| -            state = ALLOW_AUTH; | 
| -          } else { | 
| -            state = IN_PATH; | 
| -          } | 
| -        } | 
| -        break; | 
| -      } | 
| -      i++; | 
| -      char = EOI; | 
| -    } | 
| -    index = i;  // Remove alias when bug is fixed. | 
| - | 
| -    if (state == ALLOW_AUTH) { | 
| -      assert(char == _SLASH); | 
| -      // Have seen one slash either at start or right after scheme. | 
| -      // If two slashes, it's an authority, otherwise it's just the path. | 
| -      index++; | 
| -      if (index == end) { | 
| -        char = EOI; | 
| -        state = NOT_IN_PATH; | 
| -      } else { | 
| -        char = uri.codeUnitAt(index); | 
| -        if (char == _SLASH) { | 
| -          index++; | 
| -          parseAuth(); | 
| -          pathStart = index; | 
| -        } | 
| -        if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { | 
| -          state = NOT_IN_PATH; | 
| -        } else { | 
| -          state = IN_PATH; | 
| -        } | 
| -      } | 
| -    } | 
| - | 
| -    assert(state == IN_PATH || state == NOT_IN_PATH); | 
| -    if (state == IN_PATH) { | 
| -      // Characters from pathStart to index (inclusive) are known | 
| -      // to be part of the path. | 
| -      while (++index < end) { | 
| -        char = uri.codeUnitAt(index); | 
| -        if (char == _QUESTION || char == _NUMBER_SIGN) { | 
| -          break; | 
| -        } | 
| -        char = EOI; | 
| -      } | 
| -      state = NOT_IN_PATH; | 
| -    } | 
| - | 
| -    assert(state == NOT_IN_PATH); | 
| -    bool hasAuthority = (host != null); | 
| -    path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); | 
| - | 
| -    if (char == _QUESTION) { | 
| -      int numberSignIndex = -1; | 
| -      for (int i = index + 1; i < end; i++) { | 
| -        if (uri.codeUnitAt(i) == _NUMBER_SIGN) { | 
| -          numberSignIndex = i; | 
| -          break; | 
| -        } | 
| -      } | 
| -      if (numberSignIndex < 0) { | 
| -        query = _makeQuery(uri, index + 1, end, null); | 
| -      } else { | 
| -        query = _makeQuery(uri, index + 1, numberSignIndex, null); | 
| -        fragment = _makeFragment(uri, numberSignIndex + 1, end); | 
| -      } | 
| -    } else if (char == _NUMBER_SIGN) { | 
| -      fragment = _makeFragment(uri, index + 1, end); | 
| -    } | 
| -    return new Uri._internal(scheme, | 
| -                             userinfo, | 
| -                             host, | 
| -                             port, | 
| -                             path, | 
| -                             query, | 
| -                             fragment); | 
| -  } | 
| - | 
| -  // Report a parse failure. | 
| -  static void _fail(String uri, int index, String message) { | 
| -    throw new FormatException(message, uri, index); | 
| -  } | 
| - | 
| -  static Uri _makeHttpUri(String scheme, | 
| -                          String authority, | 
| -                          String unencodedPath, | 
| -                          Map<String, String> queryParameters) { | 
| -    var userInfo = ""; | 
| -    var host = null; | 
| -    var port = null; | 
| - | 
| -    if (authority != null && authority.isNotEmpty) { | 
| -      var hostStart = 0; | 
| -      // Split off the user info. | 
| -      bool hasUserInfo = false; | 
| -      for (int i = 0; i < authority.length; i++) { | 
| -        if (authority.codeUnitAt(i) == _AT_SIGN) { | 
| -          hasUserInfo = true; | 
| -          userInfo = authority.substring(0, i); | 
| -          hostStart = i + 1; | 
| -          break; | 
| -        } | 
| -      } | 
| -      var hostEnd = hostStart; | 
| -      if (hostStart < authority.length && | 
| -          authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | 
| -        // IPv6 host. | 
| -        for (; hostEnd < authority.length; hostEnd++) { | 
| -          if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | 
| -        } | 
| -        if (hostEnd == authority.length) { | 
| -          throw new FormatException("Invalid IPv6 host entry.", | 
| -                                    authority, hostStart); | 
| -        } | 
| -        parseIPv6Address(authority, hostStart + 1, hostEnd); | 
| -        hostEnd++;  // Skip the closing bracket. | 
| -        if (hostEnd != authority.length && | 
| -            authority.codeUnitAt(hostEnd) != _COLON) { | 
| -          throw new FormatException("Invalid end of authority", | 
| -                                    authority, hostEnd); | 
| -        } | 
| -      } | 
| -      // Split host and port. | 
| -      bool hasPort = false; | 
| -      for (; hostEnd < authority.length; hostEnd++) { | 
| -        if (authority.codeUnitAt(hostEnd) == _COLON) { | 
| -          var portString = authority.substring(hostEnd + 1); | 
| -          // We allow the empty port - falling back to initial value. | 
| -          if (portString.isNotEmpty) port = int.parse(portString); | 
| -          break; | 
| -        } | 
| -      } | 
| -      host = authority.substring(hostStart, hostEnd); | 
| -    } | 
| -    return new Uri(scheme: scheme, | 
| -                   userInfo: userInfo, | 
| -                   host: host, | 
| -                   port: port, | 
| -                   pathSegments: unencodedPath.split("/"), | 
| -                   queryParameters: queryParameters); | 
| -  } | 
| - | 
| -  /** | 
| -   * Creates a new file URI from an absolute or relative file path. | 
| -   * | 
| -   * The file path is passed in [path]. | 
| -   * | 
| -   * This path is interpreted using either Windows or non-Windows | 
| -   * semantics. | 
| -   * | 
| -   * With non-Windows semantics the slash ("/") is used to separate | 
| -   * path segments. | 
| -   * | 
| -   * With Windows semantics, backslash ("\\") and forward-slash ("/") | 
| -   * are used to separate path segments, except if the path starts | 
| -   * with "\\\\?\\" in which case, only backslash ("\\") separates path | 
| -   * segments. | 
| -   * | 
| -   * If the path starts with a path separator an absolute URI is | 
| -   * created. Otherwise a relative URI is created. One exception from | 
| -   * this rule is that when Windows semantics is used and the path | 
| -   * starts with a drive letter followed by a colon (":") and a | 
| -   * path separator then an absolute URI is created. | 
| -   * | 
| -   * The default for whether to use Windows or non-Windows semantics | 
| -   * determined from the platform Dart is running on. When running in | 
| -   * the standalone VM this is detected by the VM based on the | 
| -   * operating system. When running in a browser non-Windows semantics | 
| -   * is always used. | 
| -   * | 
| -   * To override the automatic detection of which semantics to use pass | 
| -   * a value for [windows]. Passing `true` will use Windows | 
| -   * semantics and passing `false` will use non-Windows semantics. | 
| -   * | 
| -   * Examples using non-Windows semantics: | 
| -   * | 
| -   * ``` | 
| -   * // xxx/yyy | 
| -   * new Uri.file("xxx/yyy", windows: false); | 
| -   * | 
| -   * // xxx/yyy/ | 
| -   * new Uri.file("xxx/yyy/", windows: false); | 
| -   * | 
| -   * // file:///xxx/yyy | 
| -   * new Uri.file("/xxx/yyy", windows: false); | 
| -   * | 
| -   * // file:///xxx/yyy/ | 
| -   * new Uri.file("/xxx/yyy/", windows: false); | 
| -   * | 
| -   * // C: | 
| -   * new Uri.file("C:", windows: false); | 
| -   * ``` | 
| -   * | 
| -   * Examples using Windows semantics: | 
| -   * | 
| -   * ``` | 
| -   * // xxx/yyy | 
| -   * new Uri.file(r"xxx\yyy", windows: true); | 
| -   * | 
| -   * // xxx/yyy/ | 
| -   * new Uri.file(r"xxx\yyy\", windows: true); | 
| -   * | 
| -   * file:///xxx/yyy | 
| -   * new Uri.file(r"\xxx\yyy", windows: true); | 
| -   * | 
| -   * file:///xxx/yyy/ | 
| -   * new Uri.file(r"\xxx\yyy/", windows: true); | 
| -   * | 
| -   * // file:///C:/xxx/yyy | 
| -   * new Uri.file(r"C:\xxx\yyy", windows: true); | 
| -   * | 
| -   * // This throws an error. A path with a drive letter is not absolute. | 
| -   * new Uri.file(r"C:", windows: true); | 
| -   * | 
| -   * // This throws an error. A path with a drive letter is not absolute. | 
| -   * new Uri.file(r"C:xxx\yyy", windows: true); | 
| -   * | 
| -   * // file://server/share/file | 
| -   * new Uri.file(r"\\server\share\file", windows: true); | 
| -   * ``` | 
| -   * | 
| -   * If the path passed is not a legal file path [ArgumentError] is thrown. | 
| -   */ | 
| -  factory Uri.file(String path, {bool windows}) { | 
| -    windows = (windows == null) ? Uri._isWindows : windows; | 
| -    return windows ? _makeWindowsFileUrl(path, false) | 
| -                   : _makeFileUri(path, false); | 
| -  } | 
| - | 
| -  /** | 
| -   * Like [Uri.file] except that a non-empty URI path ends in a slash. | 
| -   * | 
| -   * If [path] is not empty, and it doesn't end in a directory separator, | 
| -   * then a slash is added to the returned URI's path. | 
| -   * In all other cases, the result is the same as returned by `Uri.file`. | 
| -   */ | 
| -  factory Uri.directory(String path, {bool windows}) { | 
| -    windows = (windows == null) ? Uri._isWindows : windows; | 
| -    return windows ? _makeWindowsFileUrl(path, true) | 
| -                   : _makeFileUri(path, true); | 
| -  } | 
| - | 
| -  /** | 
| -   * Creates a `data:` URI containing the [content] string. | 
| -   * | 
| -   * Converts the content to a bytes using [encoding] or the charset specified | 
| -   * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), | 
| -   * then encodes the bytes into the resulting data URI. | 
| +   * Converts the content to a bytes using [encoding] or the charset specified | 
| +   * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), | 
| +   * then encodes the bytes into the resulting data URI. | 
| * | 
| * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid | 
| * bytes is replaced by a percent encoding). If [base64] is true, the bytes | 
| @@ -827,1599 +334,2340 @@ class Uri { | 
| } | 
|  | 
| /** | 
| -   * Returns the natural base URI for the current platform. | 
| +   * The scheme component of the URI. | 
| * | 
| -   * When running in a browser this is the current URL (from | 
| -   * `window.location.href`). | 
| +   * Returns the empty string if there is no scheme component. | 
| * | 
| -   * When not running in a browser this is the file URI referencing | 
| -   * the current working directory. | 
| +   * A URI scheme is case insensitive. | 
| +   * The returned scheme is canonicalized to lowercase letters. | 
| */ | 
| -  external static Uri get base; | 
| +  String get scheme; | 
|  | 
| -  external static bool get _isWindows; | 
| +  /** | 
| +   * Returns the authority component. | 
| +   * | 
| +   * The authority is formatted from the [userInfo], [host] and [port] | 
| +   * parts. | 
| +   * | 
| +   * Returns the empty string if there is no authority component. | 
| +   */ | 
| +  String get authority; | 
|  | 
| -  static _checkNonWindowsPathReservedCharacters(List<String> segments, | 
| -                                                bool argumentError) { | 
| -    segments.forEach((segment) { | 
| -      if (segment.contains("/")) { | 
| -        if (argumentError) { | 
| -          throw new ArgumentError("Illegal path character $segment"); | 
| -        } else { | 
| -          throw new UnsupportedError("Illegal path character $segment"); | 
| -        } | 
| -      } | 
| -    }); | 
| -  } | 
| +  /** | 
| +   * Returns the user info part of the authority component. | 
| +   * | 
| +   * Returns the empty string if there is no user info in the | 
| +   * authority component. | 
| +   */ | 
| +  String get userInfo; | 
|  | 
| -  static _checkWindowsPathReservedCharacters(List<String> segments, | 
| -                                             bool argumentError, | 
| -                                             [int firstSegment = 0]) { | 
| -    for (var segment in segments.skip(firstSegment)) { | 
| -      if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { | 
| -        if (argumentError) { | 
| -          throw new ArgumentError("Illegal character in path"); | 
| -        } else { | 
| -          throw new UnsupportedError("Illegal character in path"); | 
| -        } | 
| -      } | 
| -    } | 
| -  } | 
| +  /** | 
| +   * Returns the host part of the authority component. | 
| +   * | 
| +   * Returns the empty string if there is no authority component and | 
| +   * hence no host. | 
| +   * | 
| +   * If the host is an IP version 6 address, the surrounding `[` and `]` is | 
| +   * removed. | 
| +   * | 
| +   * The host string is case-insensitive. | 
| +   * The returned host name is canonicalized to lower-case | 
| +   * with upper-case percent-escapes. | 
| +   */ | 
| +  String get host; | 
|  | 
| -  static _checkWindowsDriveLetter(int charCode, bool argumentError) { | 
| -    if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) || | 
| -        (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) { | 
| -      return; | 
| -    } | 
| -    if (argumentError) { | 
| -      throw new ArgumentError("Illegal drive letter " + | 
| -                              new String.fromCharCode(charCode)); | 
| -    } else { | 
| -      throw new UnsupportedError("Illegal drive letter " + | 
| -                              new String.fromCharCode(charCode)); | 
| -    } | 
| -  } | 
| +  /** | 
| +   * Returns the port part of the authority component. | 
| +   * | 
| +   * Returns the defualt port if there is no port number in the authority | 
| +   * component. That's 80 for http, 443 for https, and 0 for everything else. | 
| +   */ | 
| +  int get port; | 
|  | 
| -  static _makeFileUri(String path, bool slashTerminated) { | 
| -    const String sep = "/"; | 
| -    var segments = path.split(sep); | 
| -    if (slashTerminated && | 
| -        segments.isNotEmpty && | 
| -        segments.last.isNotEmpty) { | 
| -      segments.add("");  // Extra separator at end. | 
| -    } | 
| -    if (path.startsWith(sep)) { | 
| -      // Absolute file:// URI. | 
| -      return new Uri(scheme: "file", pathSegments: segments); | 
| -    } else { | 
| -      // Relative URI. | 
| -      return new Uri(pathSegments: segments); | 
| -    } | 
| -  } | 
| +  /** | 
| +   * Returns the path component. | 
| +   * | 
| +   * The returned path is encoded. To get direct access to the decoded | 
| +   * path use [pathSegments]. | 
| +   * | 
| +   * Returns the empty string if there is no path component. | 
| +   */ | 
| +  String get path; | 
|  | 
| -  static _makeWindowsFileUrl(String path, bool slashTerminated) { | 
| -    if (path.startsWith(r"\\?\")) { | 
| -      if (path.startsWith(r"UNC\", 4)) { | 
| -        path = path.replaceRange(0, 7, r'\'); | 
| -      } else { | 
| -        path = path.substring(4); | 
| -        if (path.length < 3 || | 
| -            path.codeUnitAt(1) != _COLON || | 
| -            path.codeUnitAt(2) != _BACKSLASH) { | 
| -          throw new ArgumentError( | 
| -              r"Windows paths with \\?\ prefix must be absolute"); | 
| -        } | 
| +  /** | 
| +   * Returns the query component. The returned query is encoded. To get | 
| +   * direct access to the decoded query use [queryParameters]. | 
| +   * | 
| +   * Returns the empty string if there is no query component. | 
| +   */ | 
| +  String get query; | 
| + | 
| +  /** | 
| +   * Returns the fragment identifier component. | 
| +   * | 
| +   * Returns the empty string if there is no fragment identifier | 
| +   * component. | 
| +   */ | 
| +  String get fragment; | 
| + | 
| +  /** | 
| +   * Returns the URI path split into its segments. Each of the segments in the | 
| +   * returned list have been decoded. If the path is empty the empty list will | 
| +   * be returned. A leading slash `/` does not affect the segments returned. | 
| +   * | 
| +   * The returned list is unmodifiable and will throw [UnsupportedError] on any | 
| +   * calls that would mutate it. | 
| +   */ | 
| +  List<String> get pathSegments; | 
| + | 
| +  /** | 
| +   * Returns the URI query split into a map according to the rules | 
| +   * specified for FORM post in the [HTML 4.01 specification section | 
| +   * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). | 
| +   * Each key and value in the returned map has been decoded. | 
| +   * If there is no query the empty map is returned. | 
| +   * | 
| +   * Keys in the query string that have no value are mapped to the | 
| +   * empty string. | 
| +   * If a key occurs more than once in the query string, it is mapped to | 
| +   * an arbitrary choice of possible value. | 
| +   * The [queryParametersAll] getter can provide a map | 
| +   * that maps keys to all of their values. | 
| +   * | 
| +   * The returned map is unmodifiable. | 
| +   */ | 
| +  Map<String, String> get queryParameters; | 
| + | 
| +  /** | 
| +   * Returns the URI query split into a map according to the rules | 
| +   * specified for FORM post in the [HTML 4.01 specification section | 
| +   * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). | 
| +   * Each key and value in the returned map has been decoded. If there is no | 
| +   * query the empty map is returned. | 
| +   * | 
| +   * Keys are mapped to lists of their values. If a key occurs only once, | 
| +   * its value is a singleton list. If a key occurs with no value, the | 
| +   * empty string is used as the value for that occurrence. | 
| +   * | 
| +   * The returned map and the lists it contains are unmodifiable. | 
| +   */ | 
| +  Map<String, List<String>> get queryParametersAll; | 
| + | 
| +  /** | 
| +   * Returns whether the URI is absolute. | 
| +   * | 
| +   * A URI is an absolute URI in the sense of RFC 3986 if it has a scheme | 
| +   * and no fragment. | 
| +   */ | 
| +  bool get isAbsolute; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has a [scheme] component. | 
| +   */ | 
| +  bool get hasScheme => scheme.isNotEmpty; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has an [authority] component. | 
| +   */ | 
| +  bool get hasAuthority; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has an explicit port. | 
| +   * | 
| +   * If the port number is the default port number | 
| +   * (zero for unrecognized schemes, with http (80) and https (443) being | 
| +   * recognized), | 
| +   * then the port is made implicit and omitted from the URI. | 
| +   */ | 
| +  bool get hasPort; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has a query part. | 
| +   */ | 
| +  bool get hasQuery; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has a fragment part. | 
| +   */ | 
| +  bool get hasFragment; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has an empty path. | 
| +   */ | 
| +  bool get hasEmptyPath; | 
| + | 
| +  /** | 
| +   * Returns whether the URI has an absolute path (starting with '/'). | 
| +   */ | 
| +  bool get hasAbsolutePath; | 
| + | 
| +  /** | 
| +   * Returns the origin of the URI in the form scheme://host:port for the | 
| +   * schemes http and https. | 
| +   * | 
| +   * It is an error if the scheme is not "http" or "https". | 
| +   * | 
| +   * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | 
| +   */ | 
| +  String get origin; | 
| + | 
| +  /** | 
| +   * Returns the file path from a file URI. | 
| +   * | 
| +   * The returned path has either Windows or non-Windows | 
| +   * semantics. | 
| +   * | 
| +   * For non-Windows semantics the slash ("/") is used to separate | 
| +   * path segments. | 
| +   * | 
| +   * For Windows semantics the backslash ("\\") separator is used to | 
| +   * separate path segments. | 
| +   * | 
| +   * If the URI is absolute the path starts with a path separator | 
| +   * unless Windows semantics is used and the first path segment is a | 
| +   * drive letter. When Windows semantics is used a host component in | 
| +   * the uri in interpreted as a file server and a UNC path is | 
| +   * returned. | 
| +   * | 
| +   * The default for whether to use Windows or non-Windows semantics | 
| +   * determined from the platform Dart is running on. When running in | 
| +   * the standalone VM this is detected by the VM based on the | 
| +   * operating system. When running in a browser non-Windows semantics | 
| +   * is always used. | 
| +   * | 
| +   * To override the automatic detection of which semantics to use pass | 
| +   * a value for [windows]. Passing `true` will use Windows | 
| +   * semantics and passing `false` will use non-Windows semantics. | 
| +   * | 
| +   * If the URI ends with a slash (i.e. the last path component is | 
| +   * empty) the returned file path will also end with a slash. | 
| +   * | 
| +   * With Windows semantics URIs starting with a drive letter cannot | 
| +   * be relative to the current drive on the designated drive. That is | 
| +   * for the URI `file:///c:abc` calling `toFilePath` will throw as a | 
| +   * path segment cannot contain colon on Windows. | 
| +   * | 
| +   * Examples using non-Windows semantics (resulting of calling | 
| +   * toFilePath in comment): | 
| +   * | 
| +   *     Uri.parse("xxx/yyy");  // xxx/yyy | 
| +   *     Uri.parse("xxx/yyy/");  // xxx/yyy/ | 
| +   *     Uri.parse("file:///xxx/yyy");  // /xxx/yyy | 
| +   *     Uri.parse("file:///xxx/yyy/");  // /xxx/yyy/ | 
| +   *     Uri.parse("file:///C:");  // /C: | 
| +   *     Uri.parse("file:///C:a");  // /C:a | 
| +   * | 
| +   * Examples using Windows semantics (resulting URI in comment): | 
| +   * | 
| +   *     Uri.parse("xxx/yyy");  // xxx\yyy | 
| +   *     Uri.parse("xxx/yyy/");  // xxx\yyy\ | 
| +   *     Uri.parse("file:///xxx/yyy");  // \xxx\yyy | 
| +   *     Uri.parse("file:///xxx/yyy/");  // \xxx\yyy/ | 
| +   *     Uri.parse("file:///C:/xxx/yyy");  // C:\xxx\yyy | 
| +   *     Uri.parse("file:C:xxx/yyy");  // Throws as a path segment | 
| +   *                                   // cannot contain colon on Windows. | 
| +   *     Uri.parse("file://server/share/file");  // \\server\share\file | 
| +   * | 
| +   * If the URI is not a file URI calling this throws | 
| +   * [UnsupportedError]. | 
| +   * | 
| +   * If the URI cannot be converted to a file path calling this throws | 
| +   * [UnsupportedError]. | 
| +   */ | 
| +  // TODO(lrn): Deprecate and move functionality to File class or similar. | 
| +  // The core libraries should not worry about the platform. | 
| +  String toFilePath({bool windows}); | 
| + | 
| +  /** | 
| +   * Access the structure of a `data:` URI. | 
| +   * | 
| +   * Returns a [UriData] object for `data:` URIs and `null` for all other | 
| +   * URIs. | 
| +   * The [UriData] object can be used to access the media type and data | 
| +   * of a `data:` URI. | 
| +   */ | 
| +  UriData get data; | 
| + | 
| +  /// Returns a hash code computed as `toString().hashCode`. | 
| +  /// | 
| +  /// This guarantees that URIs with the same normalized | 
| +  int get hashCode; | 
| + | 
| +  /// A URI is equal to another URI with the same normalized representation. | 
| +  bool operator==(Object other); | 
| + | 
| +  /// Returns the normalized string representation of the URI. | 
| +  String toString(); | 
| + | 
| +  /** | 
| +   * Returns a new `Uri` based on this one, but with some parts replaced. | 
| +   * | 
| +   * This method takes the same parameters as the [new Uri] constructor, | 
| +   * and they have the same meaning. | 
| +   * | 
| +   * At most one of [path] and [pathSegments] must be provided. | 
| +   * Likewise, at most one of [query] and [queryParameters] must be provided. | 
| +   * | 
| +   * Each part that is not provided will default to the corresponding | 
| +   * value from this `Uri` instead. | 
| +   * | 
| +   * This method is different from [Uri.resolve] which overrides in a | 
| +   * hierarchial manner, | 
| +   * and can instead replace each part of a `Uri` individually. | 
| +   * | 
| +   * Example: | 
| +   * | 
| +   *     Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); | 
| +   *     Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); | 
| +   *     print(uri2);  // prints "A://b@c:4/D/E/E/?f#G" | 
| +   * | 
| +   * This method acts similarly to using the `new Uri` constructor with | 
| +   * some of the arguments taken from this `Uri` . Example: | 
| +   * | 
| +   *     Uri uri3 = new Uri( | 
| +   *         scheme: "A", | 
| +   *         userInfo: uri1.userInfo, | 
| +   *         host: uri1.host, | 
| +   *         port: uri1.port, | 
| +   *         path: "D/E/E", | 
| +   *         query: uri1.query, | 
| +   *         fragment: "G"); | 
| +   *     print(uri3);  // prints "A://b@c:4/D/E/E/?f#G" | 
| +   *     print(uri2 == uri3);  // prints true. | 
| +   * | 
| +   * Using this method can be seen as a shorthand for the `Uri` constructor | 
| +   * call above, but may also be slightly faster because the parts taken | 
| +   * from this `Uri` need not be checked for validity again. | 
| +   */ | 
| +  Uri replace({String scheme, | 
| +               String userInfo, | 
| +               String host, | 
| +               int port, | 
| +               String path, | 
| +               Iterable<String> pathSegments, | 
| +               String query, | 
| +               Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| +               String fragment}); | 
| + | 
| +  /** | 
| +   * Returns a `Uri` that differs from this only in not having a fragment. | 
| +   * | 
| +   * If this `Uri` does not have a fragment, it is itself returned. | 
| +   */ | 
| +  Uri removeFragment(); | 
| + | 
| +  /** | 
| +   * Resolve [reference] as an URI relative to `this`. | 
| +   * | 
| +   * First turn [reference] into a URI using [Uri.parse]. Then resolve the | 
| +   * resulting URI relative to `this`. | 
| +   * | 
| +   * Returns the resolved URI. | 
| +   * | 
| +   * See [resolveUri] for details. | 
| +   */ | 
| +  Uri resolve(String reference); | 
| + | 
| +  /** | 
| +   * Resolve [reference] as an URI relative to `this`. | 
| +   * | 
| +   * Returns the resolved URI. | 
| +   * | 
| +   * The algorithm "Transform Reference" for resolving a reference is described | 
| +   * in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). | 
| +   * | 
| +   * Updated to handle the case where the base URI is just a relative path - | 
| +   * that is: when it has no scheme or authority and the path does not start | 
| +   * with a slash. | 
| +   * In that case, the paths are combined without removing leading "..", and | 
| +   * an empty path is not converted to "/". | 
| +   */ | 
| +  Uri resolveUri(Uri reference); | 
| + | 
| +  /** | 
| +   * Returns a URI where the path has been normalized. | 
| +   * | 
| +   * A normalized path does not contain `.` segments or non-leading `..` | 
| +   * segments. | 
| +   * Only a relative path with no scheme or authority may contain | 
| +   * leading `..` segments, | 
| +   * a path that starts with `/` will also drop any leading `..` segments. | 
| +   * | 
| +   * This uses the same normalization strategy as `new Uri().resolve(this)`. | 
| +   * | 
| +   * Does not change any part of the URI except the path. | 
| +   * | 
| +   * The default implementation of `Uri` always normalizes paths, so calling | 
| +   * this function has no effect. | 
| +   */ | 
| +  Uri normalizePath(); | 
| + | 
| +  /** | 
| +   * Creates a new `Uri` object by parsing a URI string. | 
| +   * | 
| +   * If [start] and [end] are provided, only the substring from `start` | 
| +   * to `end` is parsed as a URI. | 
| +   * | 
| +   * If the string is not valid as a URI or URI reference, | 
| +   * a [FormatException] is thrown. | 
| +   */ | 
| +  static Uri parse(String uri, [int start = 0, int end]) { | 
| +    // This parsing will not validate percent-encoding, IPv6, etc. | 
| +    // When done splitting into parts, it will call, e.g., [_makeFragment] | 
| +    // to do the final parsing. | 
| +    // | 
| +    // Important parts of the RFC 3986 used here: | 
| +    // URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ] | 
| +    // | 
| +    // hier-part     = "//" authority path-abempty | 
| +    //               / path-absolute | 
| +    //               / path-rootless | 
| +    //               / path-empty | 
| +    // | 
| +    // URI-reference = URI / relative-ref | 
| +    // | 
| +    // absolute-URI  = scheme ":" hier-part [ "?" query ] | 
| +    // | 
| +    // relative-ref  = relative-part [ "?" query ] [ "#" fragment ] | 
| +    // | 
| +    // relative-part = "//" authority path-abempty | 
| +    //               / path-absolute | 
| +    //               / path-noscheme | 
| +    //               / path-empty | 
| +    // | 
| +    // scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) | 
| +    // | 
| +    // authority     = [ userinfo "@" ] host [ ":" port ] | 
| +    // userinfo      = *( unreserved / pct-encoded / sub-delims / ":" ) | 
| +    // host          = IP-literal / IPv4address / reg-name | 
| +    // port          = *DIGIT | 
| +    // reg-name      = *( unreserved / pct-encoded / sub-delims ) | 
| +    // | 
| +    // path          = path-abempty    ; begins with "/" or is empty | 
| +    //               / path-absolute   ; begins with "/" but not "//" | 
| +    //               / path-noscheme   ; begins with a non-colon segment | 
| +    //               / path-rootless   ; begins with a segment | 
| +    //               / path-empty      ; zero characters | 
| +    // | 
| +    // path-abempty  = *( "/" segment ) | 
| +    // path-absolute = "/" [ segment-nz *( "/" segment ) ] | 
| +    // path-noscheme = segment-nz-nc *( "/" segment ) | 
| +    // path-rootless = segment-nz *( "/" segment ) | 
| +    // path-empty    = 0<pchar> | 
| +    // | 
| +    // segment       = *pchar | 
| +    // segment-nz    = 1*pchar | 
| +    // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 
| +    //               ; non-zero-length segment without any colon ":" | 
| +    // | 
| +    // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@" | 
| +    // | 
| +    // query         = *( pchar / "/" / "?" ) | 
| +    // | 
| +    // fragment      = *( pchar / "/" / "?" ) | 
| +    end ??= uri.length; | 
| + | 
| +    // Special case data:URIs. Ignore case when testing. | 
| +    if (end >= start + 5) { | 
| +      int dataDelta = _startsWithData(uri, start); | 
| +      if (dataDelta == 0) { | 
| +        // The case is right. | 
| +        if (start > 0 || end < uri.length) uri = uri.substring(start, end); | 
| +        return UriData._parse(uri, 5, null).uri; | 
| +      } else if (dataDelta == 0x20) { | 
| +        return UriData._parse(uri.substring(start + 5, end), 0, null).uri; | 
| } | 
| -    } else { | 
| -      path = path.replaceAll("/", r'\'); | 
| +      // Otherwise the URI doesn't start with "data:" or any case variant of it. | 
| } | 
| -    const String sep = r'\'; | 
| -    if (path.length > 1 && path.codeUnitAt(1) == _COLON) { | 
| -      _checkWindowsDriveLetter(path.codeUnitAt(0), true); | 
| -      if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) { | 
| -        throw new ArgumentError( | 
| -            "Windows paths with drive letter must be absolute"); | 
| -      } | 
| -      // Absolute file://C:/ URI. | 
| -      var pathSegments = path.split(sep); | 
| -      if (slashTerminated && | 
| -          pathSegments.last.isNotEmpty) { | 
| -        pathSegments.add("");  // Extra separator at end. | 
| -      } | 
| -      _checkWindowsPathReservedCharacters(pathSegments, true, 1); | 
| -      return new Uri(scheme: "file", pathSegments: pathSegments); | 
| + | 
| +    // The following index-normalization belongs with the scanning, but is | 
| +    // easier to do here because we already have extracted variables from the | 
| +    // indices list. | 
| +    var indices = new List<int>(8);//new List<int>.filled(8, start - 1); | 
| + | 
| +    // Set default values for each position. | 
| +    // The value will either be correct in some cases where it isn't set | 
| +    // by the scanner, or it is clearly recognizable as an unset value. | 
| +    indices | 
| +      ..[0] = 0 | 
| +      ..[_schemeEndIndex] = start - 1 | 
| +      ..[_hostStartIndex] = start - 1 | 
| +      ..[_notSimpleIndex] = start - 1 | 
| +      ..[_portStartIndex] = start | 
| +      ..[_pathStartIndex] = start | 
| +      ..[_queryStartIndex] = end | 
| +      ..[_fragmentStartIndex] = end; | 
| +    var state = _scan(uri, start, end, _uriStart, indices); | 
| +    // Some states that should be non-simple, but the URI ended early. | 
| +    // Paths that end at a ".." must be normalized to end in "../". | 
| +    if (state >= _nonSimpleEndStates) { | 
| +      indices[_notSimpleIndex] = end; | 
| +    } | 
| +    int schemeEnd = indices[_schemeEndIndex]; | 
| +    if (schemeEnd >= start) { | 
| +      // Rescan the scheme part now that we know it's not a path. | 
| +      state = _scan(uri, start, schemeEnd, _schemeStart, indices); | 
| +      if (state == _schemeStart) { | 
| +        // Empty scheme. | 
| +        indices[_notSimpleIndex] = schemeEnd; | 
| +      } | 
| } | 
| - | 
| -    if (path.startsWith(sep)) { | 
| -      if (path.startsWith(sep, 1)) { | 
| -        // Absolute file:// URI with host. | 
| -        int pathStart = path.indexOf(r'\', 2); | 
| -        String hostPart = | 
| -            (pathStart < 0) ? path.substring(2) : path.substring(2, pathStart); | 
| -        String pathPart = | 
| -            (pathStart < 0) ? "" : path.substring(pathStart + 1); | 
| -        var pathSegments = pathPart.split(sep); | 
| -        _checkWindowsPathReservedCharacters(pathSegments, true); | 
| -        if (slashTerminated && | 
| -            pathSegments.last.isNotEmpty) { | 
| -          pathSegments.add("");  // Extra separator at end. | 
| -        } | 
| -        return new Uri( | 
| -            scheme: "file", host: hostPart, pathSegments: pathSegments); | 
| +    // The returned positions are limited by the scanners ability to write only | 
| +    // one position per character, and only the current position. | 
| +    // Scanning from left to right, we only know whether something is a scheme | 
| +    // or a path when we see a `:` or `/`, and likewise we only know if the first | 
| +    // `/` is part of the path or is leading an authority component when we see | 
| +    // the next character. | 
| + | 
| +    int hostStart     = indices[_hostStartIndex] + 1; | 
| +    int portStart     = indices[_portStartIndex]; | 
| +    int pathStart     = indices[_pathStartIndex]; | 
| +    int queryStart    = indices[_queryStartIndex]; | 
| +    int fragmentStart = indices[_fragmentStartIndex]; | 
| + | 
| +    // We may discover scheme while handling special cases. | 
| +    String scheme; | 
| + | 
| +    // Derive some positions that weren't set to normalize the indices. | 
| +    // If pathStart isn't set (it's before scheme end or host start), then | 
| +    // the path is empty. | 
| +    if (fragmentStart < queryStart) queryStart = fragmentStart; | 
| +    if (pathStart < hostStart || pathStart <= schemeEnd) { | 
| +      pathStart = queryStart; | 
| +    } | 
| +    // If there is an authority with no port, set the port position | 
| +    // to be at the end of the authority (equal to pathStart). | 
| +    // This also handles a ":" in a user-info component incorrectly setting | 
| +    // the port start position. | 
| +    if (portStart < hostStart) portStart = pathStart; | 
| + | 
| +    assert(hostStart == start || schemeEnd <= hostStart); | 
| +    assert(hostStart <= portStart); | 
| +    assert(schemeEnd <= pathStart); | 
| +    assert(portStart <= pathStart); | 
| +    assert(pathStart <= queryStart); | 
| +    assert(queryStart <= fragmentStart); | 
| + | 
| +    bool isSimple = indices[_notSimpleIndex] < start; | 
| + | 
| +    if (isSimple) { | 
| +      // Check/do normalizations that weren't detected by the scanner. | 
| +      // This includes removal of empty port or userInfo, | 
| +      // or scheme specific port and path normalizations. | 
| +      if (hostStart > schemeEnd + 3) { | 
| +        // Always be non-simple if URI contains user-info. | 
| +        // The scanner doesn't set the not-simple position in this case because | 
| +        // it's setting the host-start position instead. | 
| +        isSimple = false; | 
| +      } else if (portStart > start && portStart + 1 == pathStart) { | 
| +        // If the port is empty, it should be omitted. | 
| +        // Pathological case, don't bother correcting it. | 
| +        isSimple = false; | 
| +      } else if (queryStart < end && | 
| +                 (queryStart == pathStart + 2 && | 
| +                  uri.startsWith("..", pathStart)) || | 
| +                 (queryStart > pathStart + 2 && | 
| +                  uri.startsWith("/..", queryStart - 3))) { | 
| +        // The path ends in a ".." segment. This should be normalized to "../". | 
| +        // We didn't detect this while scanning because a query or fragment was | 
| +        // detected at the same time (which is why we only need to check this | 
| +        // if there is something after the path). | 
| +        isSimple = false; | 
| } else { | 
| -        // Absolute file:// URI. | 
| -        var pathSegments = path.split(sep); | 
| -        if (slashTerminated && | 
| -            pathSegments.last.isNotEmpty) { | 
| -          pathSegments.add("");  // Extra separator at end. | 
| +        // There are a few scheme-based normalizations that | 
| +        // the scanner couldn't check. | 
| +        // That means that the input is very close to simple, so just do | 
| +        // the normalizations. | 
| +        if (schemeEnd == start + 4) { | 
| +          // Do scheme based normalizations for file, http. | 
| +          if (uri.startsWith("file", start)) { | 
| +            scheme = "file"; | 
| +            if (hostStart <= start) { | 
| +              // File URIs should have an authority. | 
| +              // Paths after an authority should be absolute. | 
| +              String schemeAuth = "file://"; | 
| +              int delta = 2; | 
| +              if (!uri.startsWith("/", pathStart)) { | 
| +                schemeAuth = "file:///"; | 
| +                delta = 3; | 
| +              } | 
| +              uri = schemeAuth + uri.substring(pathStart, end); | 
| +              schemeEnd -= start; | 
| +              hostStart = 7; | 
| +              portStart = 7; | 
| +              pathStart = 7; | 
| +              queryStart += delta - start; | 
| +              fragmentStart += delta - start; | 
| +              start = 0; | 
| +              end = uri.length; | 
| +            } else if (pathStart == queryStart) { | 
| +              // Uri has authority and empty path. Add "/" as path. | 
| +              if (start == 0 && end == uri.length) { | 
| +                uri = uri.replaceRange(pathStart, queryStart, "/"); | 
| +                queryStart += 1; | 
| +                fragmentStart += 1; | 
| +                end += 1; | 
| +              } else { | 
| +                uri = "${uri.substring(start, pathStart)}/" | 
| +                      "${uri.substring(queryStart, end)}"; | 
| +                schemeEnd -= start; | 
| +                hostStart -= start; | 
| +                portStart -= start; | 
| +                pathStart -= start; | 
| +                queryStart += 1 - start; | 
| +                fragmentStart += 1 - start; | 
| +                start = 0; | 
| +                end = uri.length; | 
| +              } | 
| +            } | 
| +          } else if (uri.startsWith("http", start)) { | 
| +            scheme = "http"; | 
| +            // HTTP URIs should not have an explicit port of 80. | 
| +            if (portStart > start && portStart + 3 == pathStart && | 
| +                uri.startsWith("80", portStart + 1)) { | 
| +              if (start == 0 && end == uri.length) { | 
| +                uri = uri.replaceRange(portStart, pathStart, ""); | 
| +                pathStart -= 3; | 
| +                queryStart -= 3; | 
| +                fragmentStart -= 3; | 
| +                end -= 3; | 
| +              } else { | 
| +                uri = uri.substring(start, portStart) + | 
| +                      uri.substring(pathStart, end); | 
| +                schemeEnd -= start; | 
| +                hostStart -= start; | 
| +                portStart -= start; | 
| +                pathStart -= 3 + start; | 
| +                queryStart -= 3 + start; | 
| +                fragmentStart -= 3 + start; | 
| +                start = 0; | 
| +                end = uri.length; | 
| +              } | 
| +            } | 
| +          } | 
| +        } else if (schemeEnd == start + 5 && uri.startsWith("https", start)) { | 
| +          scheme = "https"; | 
| +          // HTTPS URIs should not have an explicit port of 443. | 
| +          if (portStart > start && portStart + 4 == pathStart && | 
| +              uri.startsWith("443", portStart + 1)) { | 
| +            if (start == 0 && end == uri.length) { | 
| +              uri = uri.replaceRange(portStart, pathStart, ""); | 
| +              pathStart -= 4; | 
| +              queryStart -= 4; | 
| +              fragmentStart -= 4; | 
| +              end -= 3; | 
| +            } else { | 
| +              uri = uri.substring(start, portStart) + | 
| +                    uri.substring(pathStart, end); | 
| +              schemeEnd -= start; | 
| +              hostStart -= start; | 
| +              portStart -= start; | 
| +              pathStart -= 4 + start; | 
| +              queryStart -= 4 + start; | 
| +              fragmentStart -= 4 + start; | 
| +              start = 0; | 
| +              end = uri.length; | 
| +            } | 
| +          } | 
| } | 
| -        _checkWindowsPathReservedCharacters(pathSegments, true); | 
| -        return new Uri(scheme: "file", pathSegments: pathSegments); | 
| } | 
| -    } else { | 
| -      // Relative URI. | 
| -      var pathSegments = path.split(sep); | 
| -      _checkWindowsPathReservedCharacters(pathSegments, true); | 
| -      if (slashTerminated && | 
| -          pathSegments.isNotEmpty && | 
| -          pathSegments.last.isNotEmpty) { | 
| -        pathSegments.add("");  // Extra separator at end. | 
| +    } | 
| + | 
| +    if (isSimple) { | 
| +      if (start > 0 || end < uri.length) { | 
| +        uri = uri.substring(start, end); | 
| +        schemeEnd -= start; | 
| +        hostStart -= start; | 
| +        portStart -= start; | 
| +        pathStart -= start; | 
| +        queryStart -= start; | 
| +        fragmentStart -= start; | 
| } | 
| -      return new Uri(pathSegments: pathSegments); | 
| +      return new _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, | 
| +                            queryStart, fragmentStart, scheme); | 
| + | 
| } | 
| + | 
| +    return new _Uri.notSimple(uri, start, end, schemeEnd, hostStart, portStart, | 
| +                              pathStart, queryStart, fragmentStart, scheme); | 
| } | 
|  | 
| /** | 
| -   * Returns a new `Uri` based on this one, but with some parts replaced. | 
| -   * | 
| -   * This method takes the same parameters as the [new Uri] constructor, | 
| -   * and they have the same meaning. | 
| +   * Encode the string [component] using percent-encoding to make it | 
| +   * safe for literal use as a URI component. | 
| * | 
| -   * At most one of [path] and [pathSegments] must be provided. | 
| -   * Likewise, at most one of [query] and [queryParameters] must be provided. | 
| +   * All characters except uppercase and lowercase letters, digits and | 
| +   * the characters `-_.!~*'()` are percent-encoded. This is the | 
| +   * set of characters specified in RFC 2396 and the which is | 
| +   * specified for the encodeUriComponent in ECMA-262 version 5.1. | 
| * | 
| -   * Each part that is not provided will default to the corresponding | 
| -   * value from this `Uri` instead. | 
| +   * When manually encoding path segments or query components remember | 
| +   * to encode each part separately before building the path or query | 
| +   * string. | 
| * | 
| -   * This method is different from [Uri.resolve] which overrides in a | 
| -   * hierarchial manner, | 
| -   * and can instead replace each part of a `Uri` individually. | 
| +   * For encoding the query part consider using | 
| +   * [encodeQueryComponent]. | 
| * | 
| -   * Example: | 
| +   * To avoid the need for explicitly encoding use the [pathSegments] | 
| +   * and [queryParameters] optional named arguments when constructing | 
| +   * a [Uri]. | 
| +   */ | 
| +  static String encodeComponent(String component) { | 
| +    return _Uri._uriEncode(_Uri._unreserved2396Table, component, UTF8, false); | 
| +  } | 
| + | 
| +  /** | 
| +   * Encode the string [component] according to the HTML 4.01 rules | 
| +   * for encoding the posting of a HTML form as a query string | 
| +   * component. | 
| * | 
| -   *     Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); | 
| -   *     Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); | 
| -   *     print(uri2);  // prints "A://b@c:4/D/E/E/?f#G" | 
| +   * Encode the string [component] according to the HTML 4.01 rules | 
| +   * for encoding the posting of a HTML form as a query string | 
| +   * component. | 
| + | 
| +   * The component is first encoded to bytes using [encoding]. | 
| +   * The default is to use [UTF8] encoding, which preserves all | 
| +   * the characters that don't need encoding. | 
| + | 
| +   * Then the resulting bytes are "percent-encoded". This transforms | 
| +   * spaces (U+0020) to a plus sign ('+') and all bytes that are not | 
| +   * the ASCII decimal digits, letters or one of '-._~' are written as | 
| +   * a percent sign '%' followed by the two-digit hexadecimal | 
| +   * representation of the byte. | 
| + | 
| +   * Note that the set of characters which are percent-encoded is a | 
| +   * superset of what HTML 4.01 requires, since it refers to RFC 1738 | 
| +   * for reserved characters. | 
| * | 
| -   * This method acts similarly to using the `new Uri` constructor with | 
| -   * some of the arguments taken from this `Uri` . Example: | 
| +   * When manually encoding query components remember to encode each | 
| +   * part separately before building the query string. | 
| * | 
| -   *     Uri uri3 = new Uri( | 
| -   *         scheme: "A", | 
| -   *         userInfo: uri1.userInfo, | 
| -   *         host: uri1.host, | 
| -   *         port: uri1.port, | 
| -   *         path: "D/E/E", | 
| -   *         query: uri1.query, | 
| -   *         fragment: "G"); | 
| -   *     print(uri3);  // prints "A://b@c:4/D/E/E/?f#G" | 
| -   *     print(uri2 == uri3);  // prints true. | 
| +   * To avoid the need for explicitly encoding the query use the | 
| +   * [queryParameters] optional named arguments when constructing a | 
| +   * [Uri]. | 
| * | 
| -   * Using this method can be seen as a shorthand for the `Uri` constructor | 
| -   * call above, but may also be slightly faster because the parts taken | 
| -   * from this `Uri` need not be checked for validity again. | 
| +   * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | 
| +   * details. | 
| */ | 
| -  Uri replace({String scheme, | 
| -               String userInfo, | 
| -               String host, | 
| -               int port, | 
| -               String path, | 
| -               Iterable<String> pathSegments, | 
| -               String query, | 
| -               Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| -               String fragment}) { | 
| -    // Set to true if the scheme has (potentially) changed. | 
| -    // In that case, the default port may also have changed and we need | 
| -    // to check even the existing port. | 
| -    bool schemeChanged = false; | 
| -    if (scheme != null) { | 
| -      scheme = _makeScheme(scheme, 0, scheme.length); | 
| -      schemeChanged = true; | 
| -    } else { | 
| -      scheme = this.scheme; | 
| -    } | 
| -    bool isFile = (scheme == "file"); | 
| -    if (userInfo != null) { | 
| -      userInfo = _makeUserInfo(userInfo, 0, userInfo.length); | 
| -    } else { | 
| -      userInfo = this._userInfo; | 
| -    } | 
| -    if (port != null) { | 
| -      port = _makePort(port, scheme); | 
| -    } else { | 
| -      port = this._port; | 
| -      if (schemeChanged) { | 
| -        // The default port might have changed. | 
| -        port = _makePort(port, scheme); | 
| -      } | 
| -    } | 
| -    if (host != null) { | 
| -      host = _makeHost(host, 0, host.length, false); | 
| -    } else if (this.hasAuthority) { | 
| -      host = this._host; | 
| -    } else if (userInfo.isNotEmpty || port != null || isFile) { | 
| -      host = ""; | 
| -    } | 
| - | 
| -    bool hasAuthority = host != null; | 
| -    if (path != null || pathSegments != null) { | 
| -      path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 
| -                       scheme, hasAuthority); | 
| -    } else { | 
| -      path = this._path; | 
| -      if ((isFile || (hasAuthority && !path.isEmpty)) && | 
| -          !path.startsWith('/')) { | 
| -        path = "/" + path; | 
| -      } | 
| -    } | 
| - | 
| -    if (query != null || queryParameters != null) { | 
| -      query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 
| -    } else { | 
| -      query = this._query; | 
| -    } | 
| - | 
| -    if (fragment != null) { | 
| -      fragment = _makeFragment(fragment, 0, fragment.length); | 
| -    } else { | 
| -      fragment = this._fragment; | 
| -    } | 
| +  static String encodeQueryComponent(String component, | 
| +                                     {Encoding encoding: UTF8}) { | 
| +    return _Uri._uriEncode(_Uri._unreservedTable, component, encoding, true); | 
| +  } | 
|  | 
| -    return new Uri._internal( | 
| -        scheme, userInfo, host, port, path, query, fragment); | 
| +  /** | 
| +   * Decodes the percent-encoding in [encodedComponent]. | 
| +   * | 
| +   * Note that decoding a URI component might change its meaning as | 
| +   * some of the decoded characters could be characters with are | 
| +   * delimiters for a given URI componene type. Always split a URI | 
| +   * component using the delimiters for the component before decoding | 
| +   * the individual parts. | 
| +   * | 
| +   * For handling the [path] and [query] components consider using | 
| +   * [pathSegments] and [queryParameters] to get the separated and | 
| +   * decoded component. | 
| +   */ | 
| +  static String decodeComponent(String encodedComponent) { | 
| +    return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | 
| +                           UTF8, false); | 
| } | 
|  | 
| /** | 
| -   * Returns a `Uri` that differs from this only in not having a fragment. | 
| +   * Decodes the percent-encoding in [encodedComponent], converting | 
| +   * pluses to spaces. | 
| * | 
| -   * If this `Uri` does not have a fragment, it is itself returned. | 
| +   * It will create a byte-list of the decoded characters, and then use | 
| +   * [encoding] to decode the byte-list to a String. The default encoding is | 
| +   * UTF-8. | 
| */ | 
| -  Uri removeFragment() { | 
| -    if (!this.hasFragment) return this; | 
| -    return new Uri._internal(scheme, _userInfo, _host, _port, | 
| -                             _path, _query, null); | 
| +  static String decodeQueryComponent( | 
| +      String encodedComponent, | 
| +      {Encoding encoding: UTF8}) { | 
| +    return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | 
| +                           encoding, true); | 
| } | 
|  | 
| /** | 
| -   * Returns the URI path split into its segments. Each of the segments in the | 
| -   * returned list have been decoded. If the path is empty the empty list will | 
| -   * be returned. A leading slash `/` does not affect the segments returned. | 
| +   * Encode the string [uri] using percent-encoding to make it | 
| +   * safe for literal use as a full URI. | 
| * | 
| -   * The returned list is unmodifiable and will throw [UnsupportedError] on any | 
| -   * calls that would mutate it. | 
| +   * All characters except uppercase and lowercase letters, digits and | 
| +   * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | 
| +   * is the set of characters specified in in ECMA-262 version 5.1 for | 
| +   * the encodeURI function . | 
| */ | 
| -  List<String> get pathSegments { | 
| -    var result = _pathSegments; | 
| -    if (result != null) return result; | 
| +  static String encodeFull(String uri) { | 
| +    return _Uri._uriEncode(_Uri._encodeFullTable, uri, UTF8, false); | 
| +  } | 
|  | 
| -    var pathToSplit = path; | 
| -    if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { | 
| -      pathToSplit = pathToSplit.substring(1); | 
| -    } | 
| -    result = (pathToSplit == "") | 
| -        ? const<String>[] | 
| -        : new List<String>.unmodifiable( | 
| -              pathToSplit.split("/").map(Uri.decodeComponent)); | 
| -    _pathSegments = result; | 
| -    return result; | 
| +  /** | 
| +   * Decodes the percent-encoding in [uri]. | 
| +   * | 
| +   * Note that decoding a full URI might change its meaning as some of | 
| +   * the decoded characters could be reserved characters. In most | 
| +   * cases an encoded URI should be parsed into components using | 
| +   * [Uri.parse] before decoding the separate components. | 
| +   */ | 
| +  static String decodeFull(String uri) { | 
| +    return _Uri._uriDecode(uri, 0, uri.length, UTF8, false); | 
| } | 
|  | 
| /** | 
| -   * Returns the URI query split into a map according to the rules | 
| +   * Returns the [query] split into a map according to the rules | 
| * specified for FORM post in the [HTML 4.01 specification section | 
| * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). | 
| -   * Each key and value in the returned map has been decoded. | 
| -   * If there is no query the empty map is returned. | 
| +   * Each key and value in the returned map has been decoded. If the [query] | 
| +   * is the empty string an empty map is returned. | 
| * | 
| * Keys in the query string that have no value are mapped to the | 
| * empty string. | 
| -   * If a key occurs more than once in the query string, it is mapped to | 
| -   * an arbitrary choice of possible value. | 
| -   * The [queryParametersAll] getter can provide a map | 
| -   * that maps keys to all of their values. | 
| * | 
| -   * The returned map is unmodifiable. | 
| +   * Each query component will be decoded using [encoding]. The default encoding | 
| +   * is UTF-8. | 
| */ | 
| -  Map<String, String> get queryParameters { | 
| -    if (_queryParameters == null) { | 
| -      _queryParameters = | 
| -          new UnmodifiableMapView<String, String>(splitQueryString(query)); | 
| -    } | 
| -    return _queryParameters; | 
| +  static Map<String, String> splitQueryString(String query, | 
| +                                              {Encoding encoding: UTF8}) { | 
| +    return query.split("&").fold({}, (map, element) { | 
| +      int index = element.indexOf("="); | 
| +      if (index == -1) { | 
| +        if (element != "") { | 
| +          map[decodeQueryComponent(element, encoding: encoding)] = ""; | 
| +        } | 
| +      } else if (index != 0) { | 
| +        var key = element.substring(0, index); | 
| +        var value = element.substring(index + 1); | 
| +        map[decodeQueryComponent(key, encoding: encoding)] = | 
| +            decodeQueryComponent(value, encoding: encoding); | 
| +      } | 
| +      return map; | 
| +    }); | 
| } | 
|  | 
| + | 
| /** | 
| -   * Returns the URI query split into a map according to the rules | 
| -   * specified for FORM post in the [HTML 4.01 specification section | 
| -   * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). | 
| -   * Each key and value in the returned map has been decoded. If there is no | 
| -   * query the empty map is returned. | 
| -   * | 
| -   * Keys are mapped to lists of their values. If a key occurs only once, | 
| -   * its value is a singleton list. If a key occurs with no value, the | 
| -   * empty string is used as the value for that occurrence. | 
| +   * Parse the [host] as an IP version 4 (IPv4) address, returning the address | 
| +   * as a list of 4 bytes in network byte order (big endian). | 
| * | 
| -   * The returned map and the lists it contains are unmodifiable. | 
| +   * Throws a [FormatException] if [host] is not a valid IPv4 address | 
| +   * representation. | 
| */ | 
| -  Map<String, List<String>> get queryParametersAll { | 
| -    if (_queryParameterLists == null) { | 
| -      Map queryParameterLists = _splitQueryStringAll(query); | 
| -      for (var key in queryParameterLists.keys) { | 
| -        queryParameterLists[key] = | 
| -            new List<String>.unmodifiable(queryParameterLists[key]); | 
| +  static List<int> parseIPv4Address(String host) => | 
| +       _parseIPv4Address(host, 0, host.length); | 
| + | 
| +  /// Implementation of [parseIPv4Address] that can work on a substring. | 
| +  static List<int> _parseIPv4Address(String host, int start, int end) { | 
| +    void error(String msg, int position) { | 
| +      throw new FormatException('Illegal IPv4 address, $msg', host, position); | 
| +    } | 
| + | 
| +    var result = new Uint8List(4); | 
| +    int partIndex = 0; | 
| +    int partStart = start; | 
| +    for (int i = start; i < end; i++) { | 
| +      int char = host.codeUnitAt(i); | 
| +      if (char != _DOT) { | 
| +        if (char ^ 0x30 > 9) { | 
| +          // Fail on a non-digit character. | 
| +          error("invalid character", i); | 
| +        } | 
| +      } else { | 
| +        if (partIndex == 3) { | 
| +          error('IPv4 address should contain exactly 4 parts', i); | 
| +        } | 
| +        int part = int.parse(host.substring(partStart, i)); | 
| +        if (part > 255) { | 
| +          error("each part must be in the range 0..255", partStart); | 
| +        } | 
| +        result[partIndex++] = part; | 
| +        partStart = i + 1; | 
| } | 
| -      _queryParameterLists = | 
| -          new Map<String, List<String>>.unmodifiable(queryParameterLists); | 
| } | 
| -    return _queryParameterLists; | 
| + | 
| +    if (partIndex != 3) { | 
| +      error('IPv4 address should contain exactly 4 parts', end); | 
| +    } | 
| + | 
| +    int part = int.parse(host.substring(partStart, end)); | 
| +    if (part > 255) { | 
| +      error("each part must be in the range 0..255", partStart); | 
| +    } | 
| +    result[partIndex] = part; | 
| + | 
| +    return result; | 
| } | 
|  | 
| /** | 
| -   * Returns a URI where the path has been normalized. | 
| -   * | 
| -   * A normalized path does not contain `.` segments or non-leading `..` | 
| -   * segments. | 
| -   * Only a relative path with no scheme or authority may contain | 
| -   * leading `..` segments, | 
| -   * a path that starts with `/` will also drop any leading `..` segments. | 
| +   * Parse the [host] as an IP version 6 (IPv6) address, returning the address | 
| +   * as a list of 16 bytes in network byte order (big endian). | 
| * | 
| -   * This uses the same normalization strategy as `new Uri().resolve(this)`. | 
| +   * Throws a [FormatException] if [host] is not a valid IPv6 address | 
| +   * representation. | 
| * | 
| -   * Does not change any part of the URI except the path. | 
| +   * Acts on the substring from [start] to [end]. If [end] is omitted, it | 
| +   * defaults ot the end of the string. | 
| * | 
| -   * The default implementation of `Uri` always normalizes paths, so calling | 
| -   * this function has no effect. | 
| +   * Some examples of IPv6 addresses: | 
| +   *  * ::1 | 
| +   *  * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | 
| +   *  * 3ffe:2a00:100:7031::1 | 
| +   *  * ::FFFF:129.144.52.38 | 
| +   *  * 2010:836B:4179::836B:4179 | 
| */ | 
| -  Uri normalizePath() { | 
| -    String path = _normalizePath(_path, scheme, hasAuthority); | 
| -    if (identical(path, _path)) return this; | 
| -    return this.replace(path: path); | 
| -  } | 
| +  static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | 
| +    if (end == null) end = host.length; | 
| +    // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, separated | 
| +    // by `:`'s, with the following exceptions: | 
| +    // | 
| +    //  - One (and only one) wildcard (`::`) may be present, representing a fill | 
| +    //    of 0's. The IPv6 `::` is thus 16 bytes of `0`. | 
| +    //  - The last two parts may be replaced by an IPv4 "dotted-quad" address. | 
|  | 
| -  static int _makePort(int port, String scheme) { | 
| -    // Perform scheme specific normalization. | 
| -    if (port != null && port == _defaultPort(scheme)) return null; | 
| -    return port; | 
| -  } | 
| +    // Helper function for reporting a badly formatted IPv6 address. | 
| +    void error(String msg, [position]) { | 
| +      throw new FormatException('Illegal IPv6 address, $msg', host, position); | 
| +    } | 
|  | 
| -  /** | 
| -   * Check and normalize a host name. | 
| -   * | 
| -   * If the host name starts and ends with '[' and ']', it is considered an | 
| -   * IPv6 address. If [strictIPv6] is false, the address is also considered | 
| -   * an IPv6 address if it contains any ':' character. | 
| -   * | 
| -   * If it is not an IPv6 address, it is case- and escape-normalized. | 
| -   * This escapes all characters not valid in a reg-name, | 
| -   * and converts all non-escape upper-case letters to lower-case. | 
| -   */ | 
| -  static String _makeHost(String host, int start, int end, bool strictIPv6) { | 
| -    // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? | 
| -    if (host == null) return null; | 
| -    if (start == end) return ""; | 
| -    // Host is an IPv6 address if it starts with '[' or contains a colon. | 
| -    if (host.codeUnitAt(start) == _LEFT_BRACKET) { | 
| -      if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { | 
| -        _fail(host, start, 'Missing end `]` to match `[` in host'); | 
| +    // Parse a hex block. | 
| +    int parseHex(int start, int end) { | 
| +      if (end - start > 4) { | 
| +        error('an IPv6 part can only contain a maximum of 4 hex digits', start); | 
| } | 
| -      parseIPv6Address(host, start + 1, end - 1); | 
| -      // RFC 5952 requires hex digits to be lower case. | 
| -      return host.substring(start, end).toLowerCase(); | 
| -    } | 
| -    if (!strictIPv6) { | 
| -      // TODO(lrn): skip if too short to be a valid IPv6 address? | 
| -      for (int i = start; i < end; i++) { | 
| -        if (host.codeUnitAt(i) == _COLON) { | 
| -          parseIPv6Address(host, start, end); | 
| -          return '[$host]'; | 
| -        } | 
| +      int value = int.parse(host.substring(start, end), radix: 16); | 
| +      if (value < 0 || value > 0xFFFF) { | 
| +        error('each part must be in the range of `0x0..0xFFFF`', start); | 
| } | 
| +      return value; | 
| } | 
| -    return _normalizeRegName(host, start, end); | 
| -  } | 
| - | 
| -  static bool _isRegNameChar(int char) { | 
| -    return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | 
| -  } | 
| - | 
| -  /** | 
| -   * Validates and does case- and percent-encoding normalization. | 
| -   * | 
| -   * The [host] must be an RFC3986 "reg-name". It is converted | 
| -   * to lower case, and percent escapes are converted to either | 
| -   * lower case unreserved characters or upper case escapes. | 
| -   */ | 
| -  static String _normalizeRegName(String host, int start, int end) { | 
| -    StringBuffer buffer; | 
| -    int sectionStart = start; | 
| -    int index = start; | 
| -    // Whether all characters between sectionStart and index are normalized, | 
| -    bool isNormalized = true; | 
|  | 
| -    while (index < end) { | 
| -      int char = host.codeUnitAt(index); | 
| -      if (char == _PERCENT) { | 
| -        // The _regNameTable contains "%", so we check that first. | 
| -        String replacement = _normalizeEscape(host, index, true); | 
| -        if (replacement == null && isNormalized) { | 
| -          index += 3; | 
| -          continue; | 
| -        } | 
| -        if (buffer == null) buffer = new StringBuffer(); | 
| -        String slice = host.substring(sectionStart, index); | 
| -        if (!isNormalized) slice = slice.toLowerCase(); | 
| -        buffer.write(slice); | 
| -        int sourceLength = 3; | 
| -        if (replacement == null) { | 
| -          replacement = host.substring(index, index + 3); | 
| -        } else if (replacement == "%") { | 
| -          replacement = "%25"; | 
| -          sourceLength = 1; | 
| -        } | 
| -        buffer.write(replacement); | 
| -        index += sourceLength; | 
| -        sectionStart = index; | 
| -        isNormalized = true; | 
| -      } else if (_isRegNameChar(char)) { | 
| -        if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) { | 
| -          // Put initial slice in buffer and continue in non-normalized mode | 
| -          if (buffer == null) buffer = new StringBuffer(); | 
| -          if (sectionStart < index) { | 
| -            buffer.write(host.substring(sectionStart, index)); | 
| -            sectionStart = index; | 
| +    if (host.length < 2) error('address is too short'); | 
| +    List<int> parts = []; | 
| +    bool wildcardSeen = false; | 
| +    // Set if seeing a ".", suggesting that there is an IPv4 address. | 
| +    bool seenDot = false; | 
| +    int partStart = start; | 
| +    // Parse all parts, except a potential last one. | 
| +    for (int i = start; i < end; i++) { | 
| +      int char = host.codeUnitAt(i); | 
| +      if (char == _COLON) { | 
| +        if (i == start) { | 
| +          // If we see a `:` in the beginning, expect wildcard. | 
| +          i++; | 
| +          if (host.codeUnitAt(i) != _COLON) { | 
| +            error('invalid start colon.', i); | 
| } | 
| -          isNormalized = false; | 
| +          partStart = i; | 
| } | 
| -        index++; | 
| -      } else if (_isGeneralDelimiter(char)) { | 
| -        _fail(host, index, "Invalid character"); | 
| -      } else { | 
| -        int sourceLength = 1; | 
| -        if ((char & 0xFC00) == 0xD800 && (index + 1) < end) { | 
| -          int tail = host.codeUnitAt(index + 1); | 
| -          if ((tail & 0xFC00) == 0xDC00) { | 
| -            char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | 
| -            sourceLength = 2; | 
| +        if (i == partStart) { | 
| +          // Wildcard. We only allow one. | 
| +          if (wildcardSeen) { | 
| +            error('only one wildcard `::` is allowed', i); | 
| } | 
| +          wildcardSeen = true; | 
| +          parts.add(-1); | 
| +        } else { | 
| +          // Found a single colon. Parse [partStart..i] as a hex entry. | 
| +          parts.add(parseHex(partStart, i)); | 
| } | 
| -        if (buffer == null) buffer = new StringBuffer(); | 
| -        String slice = host.substring(sectionStart, index); | 
| -        if (!isNormalized) slice = slice.toLowerCase(); | 
| -        buffer.write(slice); | 
| -        buffer.write(_escapeChar(char)); | 
| -        index += sourceLength; | 
| -        sectionStart = index; | 
| +        partStart = i + 1; | 
| +      } else if (char == _DOT) { | 
| +        seenDot = true; | 
| +      } | 
| +    } | 
| +    if (parts.length == 0) error('too few parts'); | 
| +    bool atEnd = (partStart == end); | 
| +    bool isLastWildcard = (parts.last == -1); | 
| +    if (atEnd && !isLastWildcard) { | 
| +      error('expected a part after last `:`', end); | 
| +    } | 
| +    if (!atEnd) { | 
| +      if (!seenDot) { | 
| +        parts.add(parseHex(partStart, end)); | 
| +      } else { | 
| +        List<int> last = _parseIPv4Address(host, partStart, end); | 
| +        parts.add(last[0] << 8 | last[1]); | 
| +        parts.add(last[2] << 8 | last[3]); | 
| +      } | 
| +    } | 
| +    if (wildcardSeen) { | 
| +      if (parts.length > 7) { | 
| +        error('an address with a wildcard must have less than 7 parts'); | 
| } | 
| +    } else if (parts.length != 8) { | 
| +      error('an address without a wildcard must contain exactly 8 parts'); | 
| } | 
| -    if (buffer == null) return host.substring(start, end); | 
| -    if (sectionStart < end) { | 
| -      String slice = host.substring(sectionStart, end); | 
| -      if (!isNormalized) slice = slice.toLowerCase(); | 
| -      buffer.write(slice); | 
| +    List<int> bytes = new Uint8List(16); | 
| +    for (int i = 0, index = 0; i < parts.length; i++) { | 
| +      int value = parts[i]; | 
| +      if (value == -1) { | 
| +        int wildCardLength = 9 - parts.length; | 
| +        for (int j = 0; j < wildCardLength; j++) { | 
| +          bytes[index] = 0; | 
| +          bytes[index + 1] = 0; | 
| +          index += 2; | 
| +        } | 
| +      } else { | 
| +        bytes[index] = value >> 8; | 
| +        bytes[index + 1] = value & 0xff; | 
| +        index += 2; | 
| +      } | 
| } | 
| -    return buffer.toString(); | 
| +    return bytes; | 
| } | 
| +} | 
| + | 
| +class _Uri implements Uri { | 
| +  // We represent the missing scheme as an empty string. | 
| +  // A valid scheme cannot be empty. | 
| +  final String scheme; | 
|  | 
| /** | 
| -   * Validates scheme characters and does case-normalization. | 
| +   * The user-info part of the authority. | 
| * | 
| -   * Schemes are converted to lower case. They cannot contain escapes. | 
| +   * Does not distinguish between an empty user-info and an absent one. | 
| +   * The value is always non-null. | 
| +   * Is considered absent if [_host] is `null`. | 
| */ | 
| -  static String _makeScheme(String scheme, int start, int end) { | 
| -    if (start == end) return ""; | 
| -    final int firstCodeUnit = scheme.codeUnitAt(start); | 
| -    if (!_isAlphabeticCharacter(firstCodeUnit)) { | 
| -      _fail(scheme, start, "Scheme not starting with alphabetic character"); | 
| +  final String _userInfo; | 
| + | 
| +  /** | 
| +   * The host name of the URI. | 
| +   * | 
| +   * Set to `null` if there is no authority in the URI. | 
| +   * The host name is the only mandatory part of an authority, so we use | 
| +   * it to mark whether an authority part was present or not. | 
| +   */ | 
| +  final String _host; | 
| + | 
| +  /** | 
| +   * The port number part of the authority. | 
| +   * | 
| +   * The port. Set to null if there is no port. Normalized to null if | 
| +   * the port is the default port for the scheme. | 
| +   */ | 
| +  int _port; | 
| + | 
| +  /** | 
| +   * The path of the URI. | 
| +   * | 
| +   * Always non-null. | 
| +   */ | 
| +  String _path; | 
| + | 
| +  // The query content, or null if there is no query. | 
| +  final String _query; | 
| + | 
| +  // The fragment content, or null if there is no fragment. | 
| +  final String _fragment; | 
| + | 
| +  /** | 
| +   * Cache the computed return value of [pathSegements]. | 
| +   */ | 
| +  List<String> _pathSegments; | 
| + | 
| +  /** | 
| +   * Cache of the full normalized text representation of the URI. | 
| +   */ | 
| +  String _text; | 
| + | 
| +  /** | 
| +   * Cache of the hashCode of [_text]. | 
| +   * | 
| +   * Is null until computed. | 
| +   */ | 
| +  int _hashCodeCache; | 
| + | 
| +  /** | 
| +   * Cache the computed return value of [queryParameters]. | 
| +   */ | 
| +  Map<String, String> _queryParameters; | 
| +  Map<String, List<String>> _queryParameterLists; | 
| + | 
| +  /// Internal non-verifying constructor. Only call with validated arguments. | 
| +  _Uri._internal(this.scheme, | 
| +                 this._userInfo, | 
| +                 this._host, | 
| +                 this._port, | 
| +                 this._path, | 
| +                 this._query, | 
| +                 this._fragment); | 
| + | 
| +  /// Create a [_Uri] from parts of [uri]. | 
| +  /// | 
| +  /// The parameters specify the start/end of particular components of the URI. | 
| +  /// The [scheme] may contain a string representing a normalized scheme | 
| +  /// component if one has already been discovered. | 
| +  factory _Uri.notSimple(String uri, int start, int end, int schemeEnd, | 
| +                        int hostStart, int portStart, int pathStart, | 
| +                        int queryStart, int fragmentStart, String scheme) { | 
| +    if (scheme == null) { | 
| +      scheme = ""; | 
| +      if (schemeEnd > start) { | 
| +        scheme = _makeScheme(uri, start, schemeEnd); | 
| +      } else if (schemeEnd == start) { | 
| +        _fail(uri, start, "Invalid empty scheme"); | 
| +      } | 
| } | 
| -    bool containsUpperCase = false; | 
| -    for (int i = start; i < end; i++) { | 
| -      final int codeUnit = scheme.codeUnitAt(i); | 
| -      if (!_isSchemeCharacter(codeUnit)) { | 
| -        _fail(scheme, i, "Illegal scheme character"); | 
| +    String userInfo = ""; | 
| +    String host; | 
| +    int port; | 
| +    if (hostStart > start) { | 
| +      int userInfoStart = schemeEnd + 3; | 
| +      if (userInfoStart < hostStart) { | 
| +        userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1); | 
| } | 
| -      if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { | 
| -        containsUpperCase = true; | 
| +      host = _makeHost(uri, hostStart, portStart, false); | 
| +      if (portStart + 1 < pathStart) { | 
| +        // Should throw because invalid. | 
| +        port = int.parse(uri.substring(portStart + 1, pathStart), onError: (_) { | 
| +          throw new FormatException("Invalid port", uri, portStart + 1); | 
| +        }); | 
| +        port = _makePort(port, scheme); | 
| } | 
| } | 
| -    scheme = scheme.substring(start, end); | 
| -    if (containsUpperCase) scheme = scheme.toLowerCase(); | 
| -    return scheme; | 
| -  } | 
| - | 
| -  static String _makeUserInfo(String userInfo, int start, int end) { | 
| -    if (userInfo == null) return ""; | 
| -    return _normalize(userInfo, start, end, _userinfoTable); | 
| +    String path = _makePath(uri, pathStart, queryStart, null, | 
| +                            scheme, host != null); | 
| +    String query; | 
| +    if (queryStart < fragmentStart) { | 
| +      query = _makeQuery(uri, queryStart + 1, fragmentStart, null); | 
| +    } | 
| +    String fragment; | 
| +    if (fragmentStart < end) { | 
| +      fragment = _makeFragment(uri, fragmentStart + 1, end); | 
| +    } | 
| +    return new _Uri._internal(scheme, | 
| +                              userInfo, | 
| +                              host, | 
| +                              port, | 
| +                              path, | 
| +                              query, | 
| +                              fragment); | 
| } | 
|  | 
| -  static String _makePath(String path, int start, int end, | 
| -                          Iterable<String> pathSegments, | 
| -                          String scheme, | 
| -                          bool hasAuthority) { | 
| +  /// Implementation of [Uri.Uri]. | 
| +  factory _Uri({String scheme, | 
| +                String userInfo, | 
| +                String host, | 
| +                int port, | 
| +                String path, | 
| +                Iterable<String> pathSegments, | 
| +                String query, | 
| +                Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| +                String fragment}) { | 
| +    scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | 
| +    userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | 
| +    host = _makeHost(host, 0, _stringOrNullLength(host), false); | 
| +    // Special case this constructor for backwards compatibility. | 
| +    if (query == "") query = null; | 
| +    query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 
| +    fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | 
| +    port = _makePort(port, scheme); | 
| bool isFile = (scheme == "file"); | 
| -    bool ensureLeadingSlash = isFile || hasAuthority; | 
| -    if (path == null && pathSegments == null) return isFile ? "/" : ""; | 
| -    if (path != null && pathSegments != null) { | 
| -      throw new ArgumentError('Both path and pathSegments specified'); | 
| +    if (host == null && | 
| +        (userInfo.isNotEmpty || port != null || isFile)) { | 
| +      host = ""; | 
| } | 
| -    var result; | 
| -    if (path != null) { | 
| -      result = _normalize(path, start, end, _pathCharOrSlashTable); | 
| +    bool hasAuthority = (host != null); | 
| +    path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 
| +                     scheme, hasAuthority); | 
| +    if (scheme.isEmpty && host == null && !path.startsWith('/')) { | 
| +      path = _normalizeRelativePath(path); | 
| } else { | 
| -      result = pathSegments.map((s) => | 
| -          _uriEncode(_pathCharTable, s, UTF8, false)).join("/"); | 
| +      path = _removeDotSegments(path); | 
| } | 
| -    if (result.isEmpty) { | 
| -      if (isFile) return "/"; | 
| -    } else if (ensureLeadingSlash && !result.startsWith('/')) { | 
| -      result = "/" + result; | 
| +    return new _Uri._internal(scheme, userInfo, host, port, | 
| +                              path, query, fragment); | 
| +  } | 
| + | 
| +  /// Implementation of [Uri.http]. | 
| +  factory _Uri.http(String authority, | 
| +                    String unencodedPath, | 
| +                    [Map<String, String> queryParameters]) { | 
| +    return _makeHttpUri("http", authority, unencodedPath, queryParameters); | 
| +  } | 
| + | 
| +  /// Implementation of [Uri.https]. | 
| +  factory _Uri.https(String authority, | 
| +                     String unencodedPath, | 
| +                     [Map<String, String> queryParameters]) { | 
| +    return _makeHttpUri("https", authority, unencodedPath, queryParameters); | 
| +  } | 
| + | 
| +  String get authority { | 
| +    if (!hasAuthority) return ""; | 
| +    var sb = new StringBuffer(); | 
| +    _writeAuthority(sb); | 
| +    return sb.toString(); | 
| +  } | 
| + | 
| +  String get userInfo => _userInfo; | 
| + | 
| +  String get host { | 
| +    if (_host == null) return ""; | 
| +    if (_host.startsWith('[')) { | 
| +      return _host.substring(1, _host.length - 1); | 
| } | 
| -    result = _normalizePath(result, scheme, hasAuthority); | 
| -    return result; | 
| +    return _host; | 
| } | 
|  | 
| -  /// Performs path normalization (remove dot segments) on a path. | 
| -  /// | 
| -  /// If the URI has neither scheme nor authority, it's considered a | 
| -  /// "pure path" and normalization won't remove leading ".." segments. | 
| -  /// Otherwise it follows the RFC 3986 "remove dot segments" algorithm. | 
| -  static String _normalizePath(String path, String scheme, bool hasAuthority) { | 
| -    if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) { | 
| -      return _normalizeRelativePath(path); | 
| +  int get port { | 
| +    if (_port == null) return _defaultPort(scheme); | 
| +    return _port; | 
| +  } | 
| + | 
| +  // The default port for the scheme of this Uri. | 
| +  static int _defaultPort(String scheme) { | 
| +    if (scheme == "http") return 80; | 
| +    if (scheme == "https") return 443; | 
| +    return 0; | 
| +  } | 
| + | 
| +  String get path => _path; | 
| + | 
| +  String get query => _query ?? ""; | 
| + | 
| +  String get fragment => _fragment ?? ""; | 
| + | 
| +  // Report a parse failure. | 
| +  static void _fail(String uri, int index, String message) { | 
| +    throw new FormatException(message, uri, index); | 
| +  } | 
| + | 
| +  static Uri _makeHttpUri(String scheme, | 
| +                          String authority, | 
| +                          String unencodedPath, | 
| +                          Map<String, String> queryParameters) { | 
| +    var userInfo = ""; | 
| +    var host = null; | 
| +    var port = null; | 
| + | 
| +    if (authority != null && authority.isNotEmpty) { | 
| +      var hostStart = 0; | 
| +      // Split off the user info. | 
| +      bool hasUserInfo = false; | 
| +      for (int i = 0; i < authority.length; i++) { | 
| +        const int atSign = 0x40; | 
| +        if (authority.codeUnitAt(i) == atSign) { | 
| +          hasUserInfo = true; | 
| +          userInfo = authority.substring(0, i); | 
| +          hostStart = i + 1; | 
| +          break; | 
| +        } | 
| +      } | 
| +      var hostEnd = hostStart; | 
| +      if (hostStart < authority.length && | 
| +          authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | 
| +        // IPv6 host. | 
| +        for (; hostEnd < authority.length; hostEnd++) { | 
| +          if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | 
| +        } | 
| +        if (hostEnd == authority.length) { | 
| +          throw new FormatException("Invalid IPv6 host entry.", | 
| +                                    authority, hostStart); | 
| +        } | 
| +        Uri.parseIPv6Address(authority, hostStart + 1, hostEnd); | 
| +        hostEnd++;  // Skip the closing bracket. | 
| +        if (hostEnd != authority.length && | 
| +            authority.codeUnitAt(hostEnd) != _COLON) { | 
| +          throw new FormatException("Invalid end of authority", | 
| +                                    authority, hostEnd); | 
| +        } | 
| +      } | 
| +      // Split host and port. | 
| +      bool hasPort = false; | 
| +      for (; hostEnd < authority.length; hostEnd++) { | 
| +        if (authority.codeUnitAt(hostEnd) == _COLON) { | 
| +          var portString = authority.substring(hostEnd + 1); | 
| +          // We allow the empty port - falling back to initial value. | 
| +          if (portString.isNotEmpty) port = int.parse(portString); | 
| +          break; | 
| +        } | 
| +      } | 
| +      host = authority.substring(hostStart, hostEnd); | 
| } | 
| -    return _removeDotSegments(path); | 
| +    return new Uri(scheme: scheme, | 
| +                   userInfo: userInfo, | 
| +                   host: host, | 
| +                   port: port, | 
| +                   pathSegments: unencodedPath.split("/"), | 
| +                   queryParameters: queryParameters); | 
| +  } | 
| + | 
| +  /// Implementation of [Uri.file]. | 
| +  factory _Uri.file(String path, {bool windows}) { | 
| +    windows = (windows == null) ? _Uri._isWindows : windows; | 
| +    return windows ? _makeWindowsFileUrl(path, false) | 
| +                   : _makeFileUri(path, false); | 
| } | 
|  | 
| -  static String _makeQuery( | 
| -      String query, int start, int end, | 
| -      Map<String, dynamic/*String|Iterable<String>*/> queryParameters) { | 
| -    if (query == null && queryParameters == null) return null; | 
| -    if (query != null && queryParameters != null) { | 
| -      throw new ArgumentError('Both query and queryParameters specified'); | 
| -    } | 
| -    if (query != null) return _normalize(query, start, end, _queryCharTable); | 
| +  /// Implementation of [Uri.directory]. | 
| +  factory _Uri.directory(String path, {bool windows}) { | 
| +    windows = (windows == null) ? _Uri._isWindows : windows; | 
| +    return windows ? _makeWindowsFileUrl(path, true) | 
| +                   : _makeFileUri(path, true); | 
| +  } | 
|  | 
| -    var result = new StringBuffer(); | 
| -    var separator = ""; | 
|  | 
| -    void writeParameter(String key, String value) { | 
| -      result.write(separator); | 
| -      separator = "&"; | 
| -      result.write(Uri.encodeQueryComponent(key)); | 
| -      if (value != null && value.isNotEmpty) { | 
| -        result.write("="); | 
| -        result.write(Uri.encodeQueryComponent(value)); | 
| -      } | 
| -    } | 
| +  /// Used internally in path-related constructors. | 
| +  external static bool get _isWindows; | 
|  | 
| -    queryParameters.forEach((key, value) { | 
| -      if (value == null || value is String) { | 
| -        writeParameter(key, value); | 
| -      } else { | 
| -        Iterable values = value; | 
| -        for (String value in values) { | 
| -          writeParameter(key, value); | 
| +  static _checkNonWindowsPathReservedCharacters(List<String> segments, | 
| +                                                bool argumentError) { | 
| +    segments.forEach((segment) { | 
| +      if (segment.contains("/")) { | 
| +        if (argumentError) { | 
| +          throw new ArgumentError("Illegal path character $segment"); | 
| +        } else { | 
| +          throw new UnsupportedError("Illegal path character $segment"); | 
| } | 
| } | 
| }); | 
| -    return result.toString(); | 
| } | 
|  | 
| -  static String _makeFragment(String fragment, int start, int end) { | 
| -    if (fragment == null) return null; | 
| -    return _normalize(fragment, start, end, _queryCharTable); | 
| +  static _checkWindowsPathReservedCharacters(List<String> segments, | 
| +                                             bool argumentError, | 
| +                                             [int firstSegment = 0]) { | 
| +    for (var segment in segments.skip(firstSegment)) { | 
| +      if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { | 
| +        if (argumentError) { | 
| +          throw new ArgumentError("Illegal character in path"); | 
| +        } else { | 
| +          throw new UnsupportedError("Illegal character in path"); | 
| +        } | 
| +      } | 
| +    } | 
| } | 
|  | 
| -  static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | 
| +  static _checkWindowsDriveLetter(int charCode, bool argumentError) { | 
| +    if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) || | 
| +        (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) { | 
| +      return; | 
| +    } | 
| +    if (argumentError) { | 
| +      throw new ArgumentError("Illegal drive letter " + | 
| +                              new String.fromCharCode(charCode)); | 
| +    } else { | 
| +      throw new UnsupportedError("Illegal drive letter " + | 
| +                              new String.fromCharCode(charCode)); | 
| +    } | 
| +  } | 
|  | 
| -  /** | 
| -   * Performs RFC 3986 Percent-Encoding Normalization. | 
| -   * | 
| -   * Returns a replacement string that should be replace the original escape. | 
| -   * Returns null if no replacement is necessary because the escape is | 
| -   * not for an unreserved character and is already non-lower-case. | 
| -   * | 
| -   * Returns "%" if the escape is invalid (not two valid hex digits following | 
| -   * the percent sign). The calling code should replace the percent | 
| -   * sign with "%25", but leave the following two characters unmodified. | 
| -   * | 
| -   * If [lowerCase] is true, a single character returned is always lower case, | 
| -   */ | 
| -  static String _normalizeEscape(String source, int index, bool lowerCase) { | 
| -    assert(source.codeUnitAt(index) == _PERCENT); | 
| -    if (index + 2 >= source.length) { | 
| -      return "%";  // Marks the escape as invalid. | 
| +  static _makeFileUri(String path, bool slashTerminated) { | 
| +    const String sep = "/"; | 
| +    var segments = path.split(sep); | 
| +    if (slashTerminated && | 
| +        segments.isNotEmpty && | 
| +        segments.last.isNotEmpty) { | 
| +      segments.add("");  // Extra separator at end. | 
| } | 
| -    int firstDigit = source.codeUnitAt(index + 1); | 
| -    int secondDigit = source.codeUnitAt(index + 2); | 
| -    int firstDigitValue = _parseHexDigit(firstDigit); | 
| -    int secondDigitValue = _parseHexDigit(secondDigit); | 
| -    if (firstDigitValue < 0 || secondDigitValue < 0) { | 
| -      return "%";  // Marks the escape as invalid. | 
| +    if (path.startsWith(sep)) { | 
| +      // Absolute file:// URI. | 
| +      return new Uri(scheme: "file", pathSegments: segments); | 
| +    } else { | 
| +      // Relative URI. | 
| +      return new Uri(pathSegments: segments); | 
| } | 
| -    int value = firstDigitValue * 16 + secondDigitValue; | 
| -    if (_isUnreservedChar(value)) { | 
| -      if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { | 
| -        value |= 0x20; | 
| +  } | 
| + | 
| +  static _makeWindowsFileUrl(String path, bool slashTerminated) { | 
| +    if (path.startsWith(r"\\?\")) { | 
| +      if (path.startsWith(r"UNC\", 4)) { | 
| +        path = path.replaceRange(0, 7, r'\'); | 
| +      } else { | 
| +        path = path.substring(4); | 
| +        if (path.length < 3 || | 
| +            path.codeUnitAt(1) != _COLON || | 
| +            path.codeUnitAt(2) != _BACKSLASH) { | 
| +          throw new ArgumentError( | 
| +              r"Windows paths with \\?\ prefix must be absolute"); | 
| +        } | 
| } | 
| -      return new String.fromCharCode(value); | 
| +    } else { | 
| +      path = path.replaceAll("/", r'\'); | 
| } | 
| -    if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { | 
| -      // Either digit is lower case. | 
| -      return source.substring(index, index + 3).toUpperCase(); | 
| +    const String sep = r'\'; | 
| +    if (path.length > 1 && path.codeUnitAt(1) == _COLON) { | 
| +      _checkWindowsDriveLetter(path.codeUnitAt(0), true); | 
| +      if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) { | 
| +        throw new ArgumentError( | 
| +            "Windows paths with drive letter must be absolute"); | 
| +      } | 
| +      // Absolute file://C:/ URI. | 
| +      var pathSegments = path.split(sep); | 
| +      if (slashTerminated && | 
| +          pathSegments.last.isNotEmpty) { | 
| +        pathSegments.add("");  // Extra separator at end. | 
| +      } | 
| +      _checkWindowsPathReservedCharacters(pathSegments, true, 1); | 
| +      return new Uri(scheme: "file", pathSegments: pathSegments); | 
| } | 
| -    // Escape is retained, and is already non-lower case, so return null to | 
| -    // represent "no replacement necessary". | 
| -    return null; | 
| -  } | 
|  | 
| -  // Converts a UTF-16 code-unit to its value as a hex digit. | 
| -  // Returns -1 for non-hex digits. | 
| -  static int _parseHexDigit(int char) { | 
| -    int digit = char ^ Uri._ZERO; | 
| -    if (digit <= 9) return digit; | 
| -    int lowerCase = char | 0x20; | 
| -    if (Uri._LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { | 
| -      return lowerCase - (_LOWER_CASE_A - 10); | 
| +    if (path.startsWith(sep)) { | 
| +      if (path.startsWith(sep, 1)) { | 
| +        // Absolute file:// URI with host. | 
| +        int pathStart = path.indexOf(r'\', 2); | 
| +        String hostPart = | 
| +            (pathStart < 0) ? path.substring(2) : path.substring(2, pathStart); | 
| +        String pathPart = | 
| +            (pathStart < 0) ? "" : path.substring(pathStart + 1); | 
| +        var pathSegments = pathPart.split(sep); | 
| +        _checkWindowsPathReservedCharacters(pathSegments, true); | 
| +        if (slashTerminated && | 
| +            pathSegments.last.isNotEmpty) { | 
| +          pathSegments.add("");  // Extra separator at end. | 
| +        } | 
| +        return new Uri( | 
| +            scheme: "file", host: hostPart, pathSegments: pathSegments); | 
| +      } else { | 
| +        // Absolute file:// URI. | 
| +        var pathSegments = path.split(sep); | 
| +        if (slashTerminated && | 
| +            pathSegments.last.isNotEmpty) { | 
| +          pathSegments.add("");  // Extra separator at end. | 
| +        } | 
| +        _checkWindowsPathReservedCharacters(pathSegments, true); | 
| +        return new Uri(scheme: "file", pathSegments: pathSegments); | 
| +      } | 
| +    } else { | 
| +      // Relative URI. | 
| +      var pathSegments = path.split(sep); | 
| +      _checkWindowsPathReservedCharacters(pathSegments, true); | 
| +      if (slashTerminated && | 
| +          pathSegments.isNotEmpty && | 
| +          pathSegments.last.isNotEmpty) { | 
| +        pathSegments.add("");  // Extra separator at end. | 
| +      } | 
| +      return new Uri(pathSegments: pathSegments); | 
| } | 
| -    return -1; | 
| } | 
|  | 
| -  static String _escapeChar(int char) { | 
| -    assert(char <= 0x10ffff);  // It's a valid unicode code point. | 
| -    List<int> codeUnits; | 
| -    if (char < 0x80) { | 
| -      // ASCII, a single percent encoded sequence. | 
| -      codeUnits = new List(3); | 
| -      codeUnits[0] = _PERCENT; | 
| -      codeUnits[1] = _hexDigits.codeUnitAt(char >> 4); | 
| -      codeUnits[2] = _hexDigits.codeUnitAt(char & 0xf); | 
| +  Uri replace({String scheme, | 
| +               String userInfo, | 
| +               String host, | 
| +               int port, | 
| +               String path, | 
| +               Iterable<String> pathSegments, | 
| +               String query, | 
| +               Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| +               String fragment}) { | 
| +    // Set to true if the scheme has (potentially) changed. | 
| +    // In that case, the default port may also have changed and we need | 
| +    // to check even the existing port. | 
| +    bool schemeChanged = false; | 
| +    if (scheme != null) { | 
| +      scheme = _makeScheme(scheme, 0, scheme.length); | 
| +      schemeChanged = (scheme != this.scheme); | 
| } else { | 
| -      // Do UTF-8 encoding of character, then percent encode bytes. | 
| -      int flag = 0xc0;  // The high-bit markers on the first byte of UTF-8. | 
| -      int encodedBytes = 2; | 
| -      if (char > 0x7ff) { | 
| -        flag = 0xe0; | 
| -        encodedBytes = 3; | 
| -        if (char > 0xffff) { | 
| -          encodedBytes = 4; | 
| -          flag = 0xf0; | 
| -        } | 
| -      } | 
| -      codeUnits = new List(3 * encodedBytes); | 
| -      int index = 0; | 
| -      while (--encodedBytes >= 0) { | 
| -        int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; | 
| -        codeUnits[index] = _PERCENT; | 
| -        codeUnits[index + 1] = _hexDigits.codeUnitAt(byte >> 4); | 
| -        codeUnits[index + 2] = _hexDigits.codeUnitAt(byte & 0xf); | 
| -        index += 3; | 
| -        flag = 0x80;  // Following bytes have only high bit set. | 
| +      scheme = this.scheme; | 
| +    } | 
| +    bool isFile = (scheme == "file"); | 
| +    if (userInfo != null) { | 
| +      userInfo = _makeUserInfo(userInfo, 0, userInfo.length); | 
| +    } else { | 
| +      userInfo = this._userInfo; | 
| +    } | 
| +    if (port != null) { | 
| +      port = _makePort(port, scheme); | 
| +    } else { | 
| +      port = this._port; | 
| +      if (schemeChanged) { | 
| +        // The default port might have changed. | 
| +        port = _makePort(port, scheme); | 
| } | 
| } | 
| -    return new String.fromCharCodes(codeUnits); | 
| -  } | 
| +    if (host != null) { | 
| +      host = _makeHost(host, 0, host.length, false); | 
| +    } else if (this.hasAuthority) { | 
| +      host = this._host; | 
| +    } else if (userInfo.isNotEmpty || port != null || isFile) { | 
| +      host = ""; | 
| +    } | 
|  | 
| -  /** | 
| -   * Runs through component checking that each character is valid and | 
| -   * normalize percent escapes. | 
| -   * | 
| -   * Uses [charTable] to check if a non-`%` character is allowed. | 
| -   * Each `%` character must be followed by two hex digits. | 
| -   * If the hex-digits are lower case letters, they are converted to | 
| -   * upper case. | 
| -   */ | 
| -  static String _normalize(String component, int start, int end, | 
| -                           List<int> charTable) { | 
| -    StringBuffer buffer; | 
| -    int sectionStart = start; | 
| -    int index = start; | 
| -    // Loop while characters are valid and escapes correct and upper-case. | 
| -    while (index < end) { | 
| -      int char = component.codeUnitAt(index); | 
| -      if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) { | 
| -        index++; | 
| -      } else { | 
| -        String replacement; | 
| -        int sourceLength; | 
| -        if (char == _PERCENT) { | 
| -          replacement = _normalizeEscape(component, index, false); | 
| -          // Returns null if we should keep the existing escape. | 
| -          if (replacement == null) { | 
| -            index += 3; | 
| -            continue; | 
| -          } | 
| -          // Returns "%" if we should escape the existing percent. | 
| -          if ("%" == replacement) { | 
| -            replacement = "%25"; | 
| -            sourceLength = 1; | 
| -          } else { | 
| -            sourceLength = 3; | 
| -          } | 
| -        } else if (_isGeneralDelimiter(char)) { | 
| -          _fail(component, index, "Invalid character"); | 
| -        } else { | 
| -          sourceLength = 1; | 
| -          if ((char & 0xFC00) == 0xD800) { | 
| -            // Possible lead surrogate. | 
| -            if (index + 1 < end) { | 
| -              int tail = component.codeUnitAt(index + 1); | 
| -              if ((tail & 0xFC00) == 0xDC00) { | 
| -                // Tail surrogat. | 
| -                sourceLength = 2; | 
| -                char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | 
| -              } | 
| -            } | 
| -          } | 
| -          replacement = _escapeChar(char); | 
| -        } | 
| -        if (buffer == null) buffer = new StringBuffer(); | 
| -        buffer.write(component.substring(sectionStart, index)); | 
| -        buffer.write(replacement); | 
| -        index += sourceLength; | 
| -        sectionStart = index; | 
| +    bool hasAuthority = host != null; | 
| +    if (path != null || pathSegments != null) { | 
| +      path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 
| +                       scheme, hasAuthority); | 
| +    } else { | 
| +      path = this._path; | 
| +      if ((isFile || (hasAuthority && !path.isEmpty)) && | 
| +          !path.startsWith('/')) { | 
| +        path = "/" + path; | 
| } | 
| } | 
| -    if (buffer == null) { | 
| -      // Makes no copy if start == 0 and end == component.length. | 
| -      return component.substring(start, end); | 
| + | 
| +    if (query != null || queryParameters != null) { | 
| +      query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 
| +    } else { | 
| +      query = this._query; | 
| } | 
| -    if (sectionStart < end) { | 
| -      buffer.write(component.substring(sectionStart, end)); | 
| + | 
| +    if (fragment != null) { | 
| +      fragment = _makeFragment(fragment, 0, fragment.length); | 
| +    } else { | 
| +      fragment = this._fragment; | 
| } | 
| -    return buffer.toString(); | 
| -  } | 
|  | 
| -  static bool _isSchemeCharacter(int ch) { | 
| -    return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 
| +    return new _Uri._internal( | 
| +        scheme, userInfo, host, port, path, query, fragment); | 
| } | 
|  | 
| -  static bool _isGeneralDelimiter(int ch) { | 
| -    return ch <= _RIGHT_BRACKET && | 
| -        ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 
| +  Uri removeFragment() { | 
| +    if (!this.hasFragment) return this; | 
| +    return new _Uri._internal(scheme, _userInfo, _host, _port, | 
| +                             _path, _query, null); | 
| } | 
|  | 
| -  /** | 
| -   * Returns whether the URI is absolute. | 
| -   */ | 
| -  bool get isAbsolute => scheme != "" && fragment == ""; | 
| +  List<String> get pathSegments { | 
| +    var result = _pathSegments; | 
| +    if (result != null) return result; | 
|  | 
| -  String _mergePaths(String base, String reference) { | 
| -    // Optimize for the case: absolute base, reference beginning with "../". | 
| -    int backCount = 0; | 
| -    int refStart = 0; | 
| -    // Count number of "../" at beginning of reference. | 
| -    while (reference.startsWith("../", refStart)) { | 
| -      refStart += 3; | 
| -      backCount++; | 
| +    var pathToSplit = path; | 
| +    if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { | 
| +      pathToSplit = pathToSplit.substring(1); | 
| } | 
| +    result = (pathToSplit == "") | 
| +        ? const<String>[] | 
| +        : new List<String>.unmodifiable( | 
| +              pathToSplit.split("/").map(Uri.decodeComponent)); | 
| +    _pathSegments = result; | 
| +    return result; | 
| +  } | 
|  | 
| -    // Drop last segment - everything after last '/' of base. | 
| -    int baseEnd = base.lastIndexOf('/'); | 
| -    // Drop extra segments for each leading "../" of reference. | 
| -    while (baseEnd > 0 && backCount > 0) { | 
| -      int newEnd = base.lastIndexOf('/', baseEnd - 1); | 
| -      if (newEnd < 0) { | 
| -        break; | 
| -      } | 
| -      int delta = baseEnd - newEnd; | 
| -      // If we see a "." or ".." segment in base, stop here and let | 
| -      // _removeDotSegments handle it. | 
| -      if ((delta == 2 || delta == 3) && | 
| -          base.codeUnitAt(newEnd + 1) == _DOT && | 
| -          (delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) { | 
| -        break; | 
| +  Map<String, String> get queryParameters { | 
| +    if (_queryParameters == null) { | 
| +      _queryParameters = | 
| +          new UnmodifiableMapView<String, String>(Uri.splitQueryString(query)); | 
| +    } | 
| +    return _queryParameters; | 
| +  } | 
| + | 
| +  Map<String, List<String>> get queryParametersAll { | 
| +    if (_queryParameterLists == null) { | 
| +      Map queryParameterLists = _splitQueryStringAll(query); | 
| +      for (var key in queryParameterLists.keys) { | 
| +        queryParameterLists[key] = | 
| +            new List<String>.unmodifiable(queryParameterLists[key]); | 
| } | 
| -      baseEnd = newEnd; | 
| -      backCount--; | 
| +      _queryParameterLists = | 
| +          new Map<String, List<String>>.unmodifiable(queryParameterLists); | 
| } | 
| -    return base.replaceRange(baseEnd + 1, null, | 
| -                             reference.substring(refStart - 3 * backCount)); | 
| +    return _queryParameterLists; | 
| } | 
|  | 
| -  /// Make a guess at whether a path contains a `..` or `.` segment. | 
| -  /// | 
| -  /// This is a primitive test that can cause false positives. | 
| -  /// It's only used to avoid a more expensive operation in the case where | 
| -  /// it's not necessary. | 
| -  static bool _mayContainDotSegments(String path) { | 
| -    if (path.startsWith('.')) return true; | 
| -    int index = path.indexOf("/."); | 
| -    return index != -1; | 
| +  Uri normalizePath() { | 
| +    String path = _normalizePath(_path, scheme, hasAuthority); | 
| +    if (identical(path, _path)) return this; | 
| +    return this.replace(path: path); | 
| } | 
|  | 
| -  /// Removes '.' and '..' segments from a path. | 
| -  /// | 
| -  /// Follows the RFC 2986 "remove dot segments" algorithm. | 
| -  /// This algorithm is only used on paths of URIs with a scheme, | 
| -  /// and it treats the path as if it is absolute (leading '..' are removed). | 
| -  static String _removeDotSegments(String path) { | 
| -    if (!_mayContainDotSegments(path)) return path; | 
| -    assert(path.isNotEmpty);  // An empty path would not have dot segments. | 
| -    List<String> output = []; | 
| -    bool appendSlash = false; | 
| -    for (String segment in path.split("/")) { | 
| -      appendSlash = false; | 
| -      if (segment == "..") { | 
| -        if (output.isNotEmpty) { | 
| -          output.removeLast(); | 
| -          if (output.isEmpty) { | 
| -            output.add(""); | 
| -          } | 
| -        } | 
| -        appendSlash = true; | 
| -      } else if ("." == segment) { | 
| -        appendSlash = true; | 
| -      } else { | 
| -        output.add(segment); | 
| -      } | 
| -    } | 
| -    if (appendSlash) output.add(""); | 
| -    return output.join("/"); | 
| +  static int _makePort(int port, String scheme) { | 
| +    // Perform scheme specific normalization. | 
| +    if (port != null && port == _defaultPort(scheme)) return null; | 
| +    return port; | 
| } | 
|  | 
| -  /// Removes all `.` segments and any non-leading `..` segments. | 
| -  /// | 
| -  /// Removing the ".." from a "bar/foo/.." sequence results in "bar/" | 
| -  /// (trailing "/"). If the entire path is removed (because it contains as | 
| -  /// many ".." segments as real segments), the result is "./". | 
| -  /// This is different from an empty string, which represents "no path", | 
| -  /// when you resolve it against a base URI with a path with a non-empty | 
| -  /// final segment. | 
| -  static String _normalizeRelativePath(String path) { | 
| -    assert(!path.startsWith('/'));  // Only get called for relative paths. | 
| -    if (!_mayContainDotSegments(path)) return path; | 
| -    assert(path.isNotEmpty);  // An empty path would not have dot segments. | 
| -    List<String> output = []; | 
| -    bool appendSlash = false; | 
| -    for (String segment in path.split("/")) { | 
| -      appendSlash = false; | 
| -      if (".." == segment) { | 
| -        if (!output.isEmpty && output.last != "..") { | 
| -          output.removeLast(); | 
| -          appendSlash = true; | 
| -        } else { | 
| -          output.add(".."); | 
| -        } | 
| -      } else if ("." == segment) { | 
| -        appendSlash = true; | 
| -      } else { | 
| -        output.add(segment); | 
| +  /** | 
| +   * Check and normalize a host name. | 
| +   * | 
| +   * If the host name starts and ends with '[' and ']', it is considered an | 
| +   * IPv6 address. If [strictIPv6] is false, the address is also considered | 
| +   * an IPv6 address if it contains any ':' character. | 
| +   * | 
| +   * If it is not an IPv6 address, it is case- and escape-normalized. | 
| +   * This escapes all characters not valid in a reg-name, | 
| +   * and converts all non-escape upper-case letters to lower-case. | 
| +   */ | 
| +  static String _makeHost(String host, int start, int end, bool strictIPv6) { | 
| +    // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? | 
| +    if (host == null) return null; | 
| +    if (start == end) return ""; | 
| +    // Host is an IPv6 address if it starts with '[' or contains a colon. | 
| +    if (host.codeUnitAt(start) == _LEFT_BRACKET) { | 
| +      if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { | 
| +        _fail(host, start, 'Missing end `]` to match `[` in host'); | 
| +      } | 
| +      Uri.parseIPv6Address(host, start + 1, end - 1); | 
| +      // RFC 5952 requires hex digits to be lower case. | 
| +      return host.substring(start, end).toLowerCase(); | 
| +    } | 
| +    if (!strictIPv6) { | 
| +      // TODO(lrn): skip if too short to be a valid IPv6 address? | 
| +      for (int i = start; i < end; i++) { | 
| +        if (host.codeUnitAt(i) == _COLON) { | 
| +          Uri.parseIPv6Address(host, start, end); | 
| +          return '[$host]'; | 
| +        } | 
| } | 
| } | 
| -    if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { | 
| -      return "./"; | 
| -    } | 
| -    if (appendSlash || output.last == '..') output.add(""); | 
| -    return output.join("/"); | 
| +    return _normalizeRegName(host, start, end); | 
| } | 
|  | 
| -  /** | 
| -   * Resolve [reference] as an URI relative to `this`. | 
| -   * | 
| -   * First turn [reference] into a URI using [Uri.parse]. Then resolve the | 
| -   * resulting URI relative to `this`. | 
| -   * | 
| -   * Returns the resolved URI. | 
| -   * | 
| -   * See [resolveUri] for details. | 
| -   */ | 
| -  Uri resolve(String reference) { | 
| -    return resolveUri(Uri.parse(reference)); | 
| +  static bool _isRegNameChar(int char) { | 
| +    return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | 
| } | 
|  | 
| /** | 
| -   * Resolve [reference] as an URI relative to `this`. | 
| -   * | 
| -   * Returns the resolved URI. | 
| -   * | 
| -   * The algorithm "Transform Reference" for resolving a reference is described | 
| -   * in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). | 
| +   * Validates and does case- and percent-encoding normalization. | 
| * | 
| -   * Updated to handle the case where the base URI is just a relative path - | 
| -   * that is: when it has no scheme or authority and the path does not start | 
| -   * with a slash. | 
| -   * In that case, the paths are combined without removing leading "..", and | 
| -   * an empty path is not converted to "/". | 
| +   * The [host] must be an RFC3986 "reg-name". It is converted | 
| +   * to lower case, and percent escapes are converted to either | 
| +   * lower case unreserved characters or upper case escapes. | 
| */ | 
| -  Uri resolveUri(Uri reference) { | 
| -    // From RFC 3986. | 
| -    String targetScheme; | 
| -    String targetUserInfo = ""; | 
| -    String targetHost; | 
| -    int targetPort; | 
| -    String targetPath; | 
| -    String targetQuery; | 
| -    if (reference.scheme.isNotEmpty) { | 
| -      targetScheme = reference.scheme; | 
| -      if (reference.hasAuthority) { | 
| -        targetUserInfo = reference.userInfo; | 
| -        targetHost = reference.host; | 
| -        targetPort = reference.hasPort ? reference.port : null; | 
| -      } | 
| -      targetPath = _removeDotSegments(reference.path); | 
| -      if (reference.hasQuery) { | 
| -        targetQuery = reference.query; | 
| -      } | 
| -    } else { | 
| -      targetScheme = this.scheme; | 
| -      if (reference.hasAuthority) { | 
| -        targetUserInfo = reference.userInfo; | 
| -        targetHost = reference.host; | 
| -        targetPort = _makePort(reference.hasPort ? reference.port : null, | 
| -                               targetScheme); | 
| -        targetPath = _removeDotSegments(reference.path); | 
| -        if (reference.hasQuery) targetQuery = reference.query; | 
| -      } else { | 
| -        targetUserInfo = this._userInfo; | 
| -        targetHost = this._host; | 
| -        targetPort = this._port; | 
| -        if (reference.path == "") { | 
| -          targetPath = this._path; | 
| -          if (reference.hasQuery) { | 
| -            targetQuery = reference.query; | 
| -          } else { | 
| -            targetQuery = this._query; | 
| +  static String _normalizeRegName(String host, int start, int end) { | 
| +    StringBuffer buffer; | 
| +    int sectionStart = start; | 
| +    int index = start; | 
| +    // Whether all characters between sectionStart and index are normalized, | 
| +    bool isNormalized = true; | 
| + | 
| +    while (index < end) { | 
| +      int char = host.codeUnitAt(index); | 
| +      if (char == _PERCENT) { | 
| +        // The _regNameTable contains "%", so we check that first. | 
| +        String replacement = _normalizeEscape(host, index, true); | 
| +        if (replacement == null && isNormalized) { | 
| +          index += 3; | 
| +          continue; | 
| +        } | 
| +        if (buffer == null) buffer = new StringBuffer(); | 
| +        String slice = host.substring(sectionStart, index); | 
| +        if (!isNormalized) slice = slice.toLowerCase(); | 
| +        buffer.write(slice); | 
| +        int sourceLength = 3; | 
| +        if (replacement == null) { | 
| +          replacement = host.substring(index, index + 3); | 
| +        } else if (replacement == "%") { | 
| +          replacement = "%25"; | 
| +          sourceLength = 1; | 
| +        } | 
| +        buffer.write(replacement); | 
| +        index += sourceLength; | 
| +        sectionStart = index; | 
| +        isNormalized = true; | 
| +      } else if (_isRegNameChar(char)) { | 
| +        if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) { | 
| +          // Put initial slice in buffer and continue in non-normalized mode | 
| +          if (buffer == null) buffer = new StringBuffer(); | 
| +          if (sectionStart < index) { | 
| +            buffer.write(host.substring(sectionStart, index)); | 
| +            sectionStart = index; | 
| } | 
| -        } else { | 
| -          if (reference.hasAbsolutePath) { | 
| -            targetPath = _removeDotSegments(reference.path); | 
| -          } else { | 
| -            // This is the RFC 3986 behavior for merging. | 
| -            if (this.hasEmptyPath) { | 
| -              if (!this.hasScheme && !this.hasAuthority) { | 
| -                // Keep the path relative if no scheme or authority. | 
| -                targetPath = reference.path; | 
| -              } else { | 
| -                // Add path normalization on top of RFC algorithm. | 
| -                targetPath = _removeDotSegments("/" + reference.path); | 
| -              } | 
| -            } else { | 
| -              var mergedPath = _mergePaths(this._path, reference.path); | 
| -              if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { | 
| -                targetPath = _removeDotSegments(mergedPath); | 
| -              } else { | 
| -                // Non-RFC 3986 beavior. If both base and reference are relative | 
| -                // path, allow the merged path to start with "..". | 
| -                // The RFC only specifies the case where the base has a scheme. | 
| -                targetPath = _normalizeRelativePath(mergedPath); | 
| -              } | 
| -            } | 
| +          isNormalized = false; | 
| +        } | 
| +        index++; | 
| +      } else if (_isGeneralDelimiter(char)) { | 
| +        _fail(host, index, "Invalid character"); | 
| +      } else { | 
| +        int sourceLength = 1; | 
| +        if ((char & 0xFC00) == 0xD800 && (index + 1) < end) { | 
| +          int tail = host.codeUnitAt(index + 1); | 
| +          if ((tail & 0xFC00) == 0xDC00) { | 
| +            char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | 
| +            sourceLength = 2; | 
| } | 
| -          if (reference.hasQuery) targetQuery = reference.query; | 
| } | 
| +        if (buffer == null) buffer = new StringBuffer(); | 
| +        String slice = host.substring(sectionStart, index); | 
| +        if (!isNormalized) slice = slice.toLowerCase(); | 
| +        buffer.write(slice); | 
| +        buffer.write(_escapeChar(char)); | 
| +        index += sourceLength; | 
| +        sectionStart = index; | 
| } | 
| } | 
| -    String fragment = reference.hasFragment ? reference.fragment : null; | 
| -    return new Uri._internal(targetScheme, | 
| -                             targetUserInfo, | 
| -                             targetHost, | 
| -                             targetPort, | 
| -                             targetPath, | 
| -                             targetQuery, | 
| -                             fragment); | 
| +    if (buffer == null) return host.substring(start, end); | 
| +    if (sectionStart < end) { | 
| +      String slice = host.substring(sectionStart, end); | 
| +      if (!isNormalized) slice = slice.toLowerCase(); | 
| +      buffer.write(slice); | 
| +    } | 
| +    return buffer.toString(); | 
| } | 
|  | 
| /** | 
| -   * Returns whether the URI has a [scheme] component. | 
| +   * Validates scheme characters and does case-normalization. | 
| +   * | 
| +   * Schemes are converted to lower case. They cannot contain escapes. | 
| */ | 
| -  bool get hasScheme => scheme.isNotEmpty; | 
| +  static String _makeScheme(String scheme, int start, int end) { | 
| +    if (start == end) return ""; | 
| +    final int firstCodeUnit = scheme.codeUnitAt(start); | 
| +    if (!_isAlphabeticCharacter(firstCodeUnit)) { | 
| +      _fail(scheme, start, "Scheme not starting with alphabetic character"); | 
| +    } | 
| +    bool containsUpperCase = false; | 
| +    for (int i = start; i < end; i++) { | 
| +      final int codeUnit = scheme.codeUnitAt(i); | 
| +      if (!_isSchemeCharacter(codeUnit)) { | 
| +        _fail(scheme, i, "Illegal scheme character"); | 
| +      } | 
| +      if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { | 
| +        containsUpperCase = true; | 
| +      } | 
| +    } | 
| +    scheme = scheme.substring(start, end); | 
| +    if (containsUpperCase) scheme = scheme.toLowerCase(); | 
| +    return _canonicalizeScheme(scheme); | 
| +  } | 
|  | 
| -  /** | 
| -   * Returns whether the URI has an [authority] component. | 
| -   */ | 
| -  bool get hasAuthority => _host != null; | 
| +  // Canonicalize a few often-used scheme strings. | 
| +  // | 
| +  // This improves memory usage and makes comparison faster. | 
| +  static String _canonicalizeScheme(String scheme) { | 
| +    if (scheme == "http") return "http"; | 
| +    if (scheme == "file") return "file"; | 
| +    if (scheme == "https") return "https"; | 
| +    if (scheme == "package") return "package"; | 
| +    return scheme; | 
| +  } | 
|  | 
| -  /** | 
| -   * Returns whether the URI has an explicit port. | 
| -   * | 
| -   * If the port number is the default port number | 
| -   * (zero for unrecognized schemes, with http (80) and https (443) being | 
| -   * recognized), | 
| -   * then the port is made implicit and omitted from the URI. | 
| -   */ | 
| -  bool get hasPort => _port != null; | 
| +  static String _makeUserInfo(String userInfo, int start, int end) { | 
| +    if (userInfo == null) return ""; | 
| +    return _normalize(userInfo, start, end, _userinfoTable); | 
| +  } | 
|  | 
| -  /** | 
| -   * Returns whether the URI has a query part. | 
| -   */ | 
| -  bool get hasQuery => _query != null; | 
| +  static String _makePath(String path, int start, int end, | 
| +                          Iterable<String> pathSegments, | 
| +                          String scheme, | 
| +                          bool hasAuthority) { | 
| +    bool isFile = (scheme == "file"); | 
| +    bool ensureLeadingSlash = isFile || hasAuthority; | 
| +    if (path == null && pathSegments == null) return isFile ? "/" : ""; | 
| +    if (path != null && pathSegments != null) { | 
| +      throw new ArgumentError('Both path and pathSegments specified'); | 
| +    } | 
| +    var result; | 
| +    if (path != null) { | 
| +      result = _normalize(path, start, end, _pathCharOrSlashTable); | 
| +    } else { | 
| +      result = pathSegments.map((s) => | 
| +          _uriEncode(_pathCharTable, s, UTF8, false)).join("/"); | 
| +    } | 
| +    if (result.isEmpty) { | 
| +      if (isFile) return "/"; | 
| +    } else if (ensureLeadingSlash && !result.startsWith('/')) { | 
| +      result = "/" + result; | 
| +    } | 
| +    result = _normalizePath(result, scheme, hasAuthority); | 
| +    return result; | 
| +  } | 
|  | 
| -  /** | 
| -   * Returns whether the URI has a fragment part. | 
| -   */ | 
| -  bool get hasFragment => _fragment != null; | 
| +  /// Performs path normalization (remove dot segments) on a path. | 
| +  /// | 
| +  /// If the URI has neither scheme nor authority, it's considered a | 
| +  /// "pure path" and normalization won't remove leading ".." segments. | 
| +  /// Otherwise it follows the RFC 3986 "remove dot segments" algorithm. | 
| +  static String _normalizePath(String path, String scheme, bool hasAuthority) { | 
| +    if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) { | 
| +      return _normalizeRelativePath(path); | 
| +    } | 
| +    return _removeDotSegments(path); | 
| +  } | 
|  | 
| -  /** | 
| -   * Returns whether the URI has an empty path. | 
| -   */ | 
| -  bool get hasEmptyPath => _path.isEmpty; | 
| +  static String _makeQuery( | 
| +      String query, int start, int end, | 
| +      Map<String, dynamic/*String|Iterable<String>*/> queryParameters) { | 
| +    if (query == null && queryParameters == null) return null; | 
| +    if (query != null && queryParameters != null) { | 
| +      throw new ArgumentError('Both query and queryParameters specified'); | 
| +    } | 
| +    if (query != null) return _normalize(query, start, end, _queryCharTable); | 
|  | 
| -  /** | 
| -   * Returns whether the URI has an absolute path (starting with '/'). | 
| -   */ | 
| -  bool get hasAbsolutePath => _path.startsWith('/'); | 
| +    var result = new StringBuffer(); | 
| +    var separator = ""; | 
|  | 
| -  /** | 
| -   * Returns the origin of the URI in the form scheme://host:port for the | 
| -   * schemes http and https. | 
| -   * | 
| -   * It is an error if the scheme is not "http" or "https". | 
| -   * | 
| -   * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | 
| -   */ | 
| -  String get origin { | 
| -    if (scheme == "" || _host == null || _host == "") { | 
| -      throw new StateError("Cannot use origin without a scheme: $this"); | 
| -    } | 
| -    if (scheme != "http" && scheme != "https") { | 
| -      throw new StateError( | 
| -        "Origin is only applicable schemes http and https: $this"); | 
| +    void writeParameter(String key, String value) { | 
| +      result.write(separator); | 
| +      separator = "&"; | 
| +      result.write(Uri.encodeQueryComponent(key)); | 
| +      if (value != null && value.isNotEmpty) { | 
| +        result.write("="); | 
| +        result.write(Uri.encodeQueryComponent(value)); | 
| +      } | 
| } | 
| -    if (_port == null) return "$scheme://$_host"; | 
| -    return "$scheme://$_host:$_port"; | 
| + | 
| +    queryParameters.forEach((key, value) { | 
| +      if (value == null || value is String) { | 
| +        writeParameter(key, value); | 
| +      } else { | 
| +        Iterable values = value; | 
| +        for (String value in values) { | 
| +          writeParameter(key, value); | 
| +        } | 
| +      } | 
| +    }); | 
| +    return result.toString(); | 
| +  } | 
| + | 
| +  static String _makeFragment(String fragment, int start, int end) { | 
| +    if (fragment == null) return null; | 
| +    return _normalize(fragment, start, end, _queryCharTable); | 
| } | 
|  | 
| /** | 
| -   * Returns the file path from a file URI. | 
| -   * | 
| -   * The returned path has either Windows or non-Windows | 
| -   * semantics. | 
| -   * | 
| -   * For non-Windows semantics the slash ("/") is used to separate | 
| -   * path segments. | 
| -   * | 
| -   * For Windows semantics the backslash ("\\") separator is used to | 
| -   * separate path segments. | 
| -   * | 
| -   * If the URI is absolute the path starts with a path separator | 
| -   * unless Windows semantics is used and the first path segment is a | 
| -   * drive letter. When Windows semantics is used a host component in | 
| -   * the uri in interpreted as a file server and a UNC path is | 
| -   * returned. | 
| -   * | 
| -   * The default for whether to use Windows or non-Windows semantics | 
| -   * determined from the platform Dart is running on. When running in | 
| -   * the standalone VM this is detected by the VM based on the | 
| -   * operating system. When running in a browser non-Windows semantics | 
| -   * is always used. | 
| -   * | 
| -   * To override the automatic detection of which semantics to use pass | 
| -   * a value for [windows]. Passing `true` will use Windows | 
| -   * semantics and passing `false` will use non-Windows semantics. | 
| -   * | 
| -   * If the URI ends with a slash (i.e. the last path component is | 
| -   * empty) the returned file path will also end with a slash. | 
| -   * | 
| -   * With Windows semantics URIs starting with a drive letter cannot | 
| -   * be relative to the current drive on the designated drive. That is | 
| -   * for the URI `file:///c:abc` calling `toFilePath` will throw as a | 
| -   * path segment cannot contain colon on Windows. | 
| -   * | 
| -   * Examples using non-Windows semantics (resulting of calling | 
| -   * toFilePath in comment): | 
| -   * | 
| -   *     Uri.parse("xxx/yyy");  // xxx/yyy | 
| -   *     Uri.parse("xxx/yyy/");  // xxx/yyy/ | 
| -   *     Uri.parse("file:///xxx/yyy");  // /xxx/yyy | 
| -   *     Uri.parse("file:///xxx/yyy/");  // /xxx/yyy/ | 
| -   *     Uri.parse("file:///C:");  // /C: | 
| -   *     Uri.parse("file:///C:a");  // /C:a | 
| -   * | 
| -   * Examples using Windows semantics (resulting URI in comment): | 
| +   * Performs RFC 3986 Percent-Encoding Normalization. | 
| * | 
| -   *     Uri.parse("xxx/yyy");  // xxx\yyy | 
| -   *     Uri.parse("xxx/yyy/");  // xxx\yyy\ | 
| -   *     Uri.parse("file:///xxx/yyy");  // \xxx\yyy | 
| -   *     Uri.parse("file:///xxx/yyy/");  // \xxx\yyy/ | 
| -   *     Uri.parse("file:///C:/xxx/yyy");  // C:\xxx\yyy | 
| -   *     Uri.parse("file:C:xxx/yyy");  // Throws as a path segment | 
| -   *                                   // cannot contain colon on Windows. | 
| -   *     Uri.parse("file://server/share/file");  // \\server\share\file | 
| +   * Returns a replacement string that should be replace the original escape. | 
| +   * Returns null if no replacement is necessary because the escape is | 
| +   * not for an unreserved character and is already non-lower-case. | 
| * | 
| -   * If the URI is not a file URI calling this throws | 
| -   * [UnsupportedError]. | 
| +   * Returns "%" if the escape is invalid (not two valid hex digits following | 
| +   * the percent sign). The calling code should replace the percent | 
| +   * sign with "%25", but leave the following two characters unmodified. | 
| * | 
| -   * If the URI cannot be converted to a file path calling this throws | 
| -   * [UnsupportedError]. | 
| +   * If [lowerCase] is true, a single character returned is always lower case, | 
| */ | 
| -  String toFilePath({bool windows}) { | 
| -    if (scheme != "" && scheme != "file") { | 
| -      throw new UnsupportedError( | 
| -          "Cannot extract a file path from a $scheme URI"); | 
| +  static String _normalizeEscape(String source, int index, bool lowerCase) { | 
| +    assert(source.codeUnitAt(index) == _PERCENT); | 
| +    if (index + 2 >= source.length) { | 
| +      return "%";  // Marks the escape as invalid. | 
| } | 
| -    if (query != "") { | 
| -      throw new UnsupportedError( | 
| -          "Cannot extract a file path from a URI with a query component"); | 
| +    int firstDigit = source.codeUnitAt(index + 1); | 
| +    int secondDigit = source.codeUnitAt(index + 2); | 
| +    int firstDigitValue = _parseHexDigit(firstDigit); | 
| +    int secondDigitValue = _parseHexDigit(secondDigit); | 
| +    if (firstDigitValue < 0 || secondDigitValue < 0) { | 
| +      return "%";  // Marks the escape as invalid. | 
| } | 
| -    if (fragment != "") { | 
| -      throw new UnsupportedError( | 
| -          "Cannot extract a file path from a URI with a fragment component"); | 
| +    int value = firstDigitValue * 16 + secondDigitValue; | 
| +    if (_isUnreservedChar(value)) { | 
| +      if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { | 
| +        value |= 0x20; | 
| +      } | 
| +      return new String.fromCharCode(value); | 
| } | 
| -    if (windows == null) windows = _isWindows; | 
| -    return windows ? _toWindowsFilePath() : _toFilePath(); | 
| +    if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { | 
| +      // Either digit is lower case. | 
| +      return source.substring(index, index + 3).toUpperCase(); | 
| +    } | 
| +    // Escape is retained, and is already non-lower case, so return null to | 
| +    // represent "no replacement necessary". | 
| +    return null; | 
| } | 
|  | 
| -  String _toFilePath() { | 
| -    if (host != "") { | 
| -      throw new UnsupportedError( | 
| -          "Cannot extract a non-Windows file path from a file URI " | 
| -          "with an authority"); | 
| +  // Converts a UTF-16 code-unit to its value as a hex digit. | 
| +  // Returns -1 for non-hex digits. | 
| +  static int _parseHexDigit(int char) { | 
| +    const int zeroDigit = 0x30; | 
| +    int digit = char ^ zeroDigit; | 
| +    if (digit <= 9) return digit; | 
| +    int lowerCase = char | 0x20; | 
| +    if (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { | 
| +      return lowerCase - (_LOWER_CASE_A - 10); | 
| } | 
| -    _checkNonWindowsPathReservedCharacters(pathSegments, false); | 
| -    var result = new StringBuffer(); | 
| -    if (_isPathAbsolute) result.write("/"); | 
| -    result.writeAll(pathSegments, "/"); | 
| -    return result.toString(); | 
| +    return -1; | 
| } | 
|  | 
| -  String _toWindowsFilePath() { | 
| -    bool hasDriveLetter = false; | 
| -    var segments = pathSegments; | 
| -    if (segments.length > 0 && | 
| -        segments[0].length == 2 && | 
| -        segments[0].codeUnitAt(1) == _COLON) { | 
| -      _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); | 
| -      _checkWindowsPathReservedCharacters(segments, false, 1); | 
| -      hasDriveLetter = true; | 
| +  static String _escapeChar(int char) { | 
| +    assert(char <= 0x10ffff);  // It's a valid unicode code point. | 
| +    List<int> codeUnits; | 
| +    if (char < 0x80) { | 
| +      // ASCII, a single percent encoded sequence. | 
| +      codeUnits = new List(3); | 
| +      codeUnits[0] = _PERCENT; | 
| +      codeUnits[1] = _hexDigits.codeUnitAt(char >> 4); | 
| +      codeUnits[2] = _hexDigits.codeUnitAt(char & 0xf); | 
| } else { | 
| -      _checkWindowsPathReservedCharacters(segments, false); | 
| +      // Do UTF-8 encoding of character, then percent encode bytes. | 
| +      int flag = 0xc0;  // The high-bit markers on the first byte of UTF-8. | 
| +      int encodedBytes = 2; | 
| +      if (char > 0x7ff) { | 
| +        flag = 0xe0; | 
| +        encodedBytes = 3; | 
| +        if (char > 0xffff) { | 
| +          encodedBytes = 4; | 
| +          flag = 0xf0; | 
| +        } | 
| +      } | 
| +      codeUnits = new List(3 * encodedBytes); | 
| +      int index = 0; | 
| +      while (--encodedBytes >= 0) { | 
| +        int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; | 
| +        codeUnits[index] = _PERCENT; | 
| +        codeUnits[index + 1] = _hexDigits.codeUnitAt(byte >> 4); | 
| +        codeUnits[index + 2] = _hexDigits.codeUnitAt(byte & 0xf); | 
| +        index += 3; | 
| +        flag = 0x80;  // Following bytes have only high bit set. | 
| +      } | 
| +    } | 
| +    return new String.fromCharCodes(codeUnits); | 
| +  } | 
| + | 
| +  /** | 
| +   * Runs through component checking that each character is valid and | 
| +   * normalize percent escapes. | 
| +   * | 
| +   * Uses [charTable] to check if a non-`%` character is allowed. | 
| +   * Each `%` character must be followed by two hex digits. | 
| +   * If the hex-digits are lower case letters, they are converted to | 
| +   * upper case. | 
| +   */ | 
| +  static String _normalize(String component, int start, int end, | 
| +                           List<int> charTable) { | 
| +    StringBuffer buffer; | 
| +    int sectionStart = start; | 
| +    int index = start; | 
| +    // Loop while characters are valid and escapes correct and upper-case. | 
| +    while (index < end) { | 
| +      int char = component.codeUnitAt(index); | 
| +      if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) { | 
| +        index++; | 
| +      } else { | 
| +        String replacement; | 
| +        int sourceLength; | 
| +        if (char == _PERCENT) { | 
| +          replacement = _normalizeEscape(component, index, false); | 
| +          // Returns null if we should keep the existing escape. | 
| +          if (replacement == null) { | 
| +            index += 3; | 
| +            continue; | 
| +          } | 
| +          // Returns "%" if we should escape the existing percent. | 
| +          if ("%" == replacement) { | 
| +            replacement = "%25"; | 
| +            sourceLength = 1; | 
| +          } else { | 
| +            sourceLength = 3; | 
| +          } | 
| +        } else if (_isGeneralDelimiter(char)) { | 
| +          _fail(component, index, "Invalid character"); | 
| +        } else { | 
| +          sourceLength = 1; | 
| +          if ((char & 0xFC00) == 0xD800) { | 
| +            // Possible lead surrogate. | 
| +            if (index + 1 < end) { | 
| +              int tail = component.codeUnitAt(index + 1); | 
| +              if ((tail & 0xFC00) == 0xDC00) { | 
| +                // Tail surrogate. | 
| +                sourceLength = 2; | 
| +                char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | 
| +              } | 
| +            } | 
| +          } | 
| +          replacement = _escapeChar(char); | 
| +        } | 
| +        if (buffer == null) buffer = new StringBuffer(); | 
| +        buffer.write(component.substring(sectionStart, index)); | 
| +        buffer.write(replacement); | 
| +        index += sourceLength; | 
| +        sectionStart = index; | 
| +      } | 
| } | 
| -    var result = new StringBuffer(); | 
| -    if (_isPathAbsolute && !hasDriveLetter) result.write("\\"); | 
| -    if (host != "") { | 
| -      result.write("\\"); | 
| -      result.write(host); | 
| -      result.write("\\"); | 
| -    } | 
| -    result.writeAll(segments, "\\"); | 
| -    if (hasDriveLetter && segments.length == 1) result.write("\\"); | 
| -    return result.toString(); | 
| +    if (buffer == null) { | 
| +      // Makes no copy if start == 0 and end == component.length. | 
| +      return component.substring(start, end); | 
| +    } | 
| +    if (sectionStart < end) { | 
| +      buffer.write(component.substring(sectionStart, end)); | 
| +    } | 
| +    return buffer.toString(); | 
| } | 
|  | 
| -  bool get _isPathAbsolute { | 
| -    if (path == null || path.isEmpty) return false; | 
| -    return path.startsWith('/'); | 
| +  static bool _isSchemeCharacter(int ch) { | 
| +    return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 
| } | 
|  | 
| -  void _writeAuthority(StringSink ss) { | 
| -    if (_userInfo.isNotEmpty) { | 
| -      ss.write(_userInfo); | 
| -      ss.write("@"); | 
| -    } | 
| -    if (_host != null) ss.write(_host); | 
| -    if (_port != null) { | 
| -      ss.write(":"); | 
| -      ss.write(_port); | 
| -    } | 
| +  static bool _isGeneralDelimiter(int ch) { | 
| +    return ch <= _RIGHT_BRACKET && | 
| +        ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 
| } | 
|  | 
| /** | 
| -   * Access the structure of a `data:` URI. | 
| -   * | 
| -   * Returns a [UriData] object for `data:` URIs and `null` for all other | 
| -   * URIs. | 
| -   * The [UriData] object can be used to access the media type and data | 
| -   * of a `data:` URI. | 
| +   * Returns whether the URI is absolute. | 
| */ | 
| -  UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; | 
| +  bool get isAbsolute => scheme != "" && fragment == ""; | 
|  | 
| -  String toString() { | 
| -    StringBuffer sb = new StringBuffer(); | 
| -    _addIfNonEmpty(sb, scheme, scheme, ':'); | 
| -    if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | 
| -      // File URIS always have the authority, even if it is empty. | 
| -      // The empty URI means "localhost". | 
| -      sb.write("//"); | 
| -      _writeAuthority(sb); | 
| +  String _mergePaths(String base, String reference) { | 
| +    // Optimize for the case: absolute base, reference beginning with "../". | 
| +    int backCount = 0; | 
| +    int refStart = 0; | 
| +    // Count number of "../" at beginning of reference. | 
| +    while (reference.startsWith("../", refStart)) { | 
| +      refStart += 3; | 
| +      backCount++; | 
| } | 
| -    sb.write(path); | 
| -    if (_query != null) { sb..write("?")..write(_query); } | 
| -    if (_fragment != null) { sb..write("#")..write(_fragment); } | 
| -    return sb.toString(); | 
| + | 
| +    // Drop last segment - everything after last '/' of base. | 
| +    int baseEnd = base.lastIndexOf('/'); | 
| +    // Drop extra segments for each leading "../" of reference. | 
| +    while (baseEnd > 0 && backCount > 0) { | 
| +      int newEnd = base.lastIndexOf('/', baseEnd - 1); | 
| +      if (newEnd < 0) { | 
| +        break; | 
| +      } | 
| +      int delta = baseEnd - newEnd; | 
| +      // If we see a "." or ".." segment in base, stop here and let | 
| +      // _removeDotSegments handle it. | 
| +      if ((delta == 2 || delta == 3) && | 
| +          base.codeUnitAt(newEnd + 1) == _DOT && | 
| +          (delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) { | 
| +        break; | 
| +      } | 
| +      baseEnd = newEnd; | 
| +      backCount--; | 
| +    } | 
| +    return base.replaceRange(baseEnd + 1, null, | 
| +                             reference.substring(refStart - 3 * backCount)); | 
| } | 
|  | 
| -  bool operator==(other) { | 
| -    if (other is! Uri) return false; | 
| -    Uri uri = other; | 
| -    return scheme       == uri.scheme       && | 
| -           hasAuthority == uri.hasAuthority && | 
| -           userInfo     == uri.userInfo     && | 
| -           host         == uri.host         && | 
| -           port         == uri.port         && | 
| -           path         == uri.path         && | 
| -           hasQuery     == uri.hasQuery     && | 
| -           query        == uri.query        && | 
| -           hasFragment  == uri.hasFragment  && | 
| -           fragment     == uri.fragment; | 
| +  /// Make a guess at whether a path contains a `..` or `.` segment. | 
| +  /// | 
| +  /// This is a primitive test that can cause false positives. | 
| +  /// It's only used to avoid a more expensive operation in the case where | 
| +  /// it's not necessary. | 
| +  static bool _mayContainDotSegments(String path) { | 
| +    if (path.startsWith('.')) return true; | 
| +    int index = path.indexOf("/."); | 
| +    return index != -1; | 
| } | 
|  | 
| -  int get hashCode { | 
| -    int combine(part, current) { | 
| -      // The sum is truncated to 30 bits to make sure it fits into a Smi. | 
| -      return (current * 31 + part.hashCode) & 0x3FFFFFFF; | 
| +  /// Removes '.' and '..' segments from a path. | 
| +  /// | 
| +  /// Follows the RFC 2986 "remove dot segments" algorithm. | 
| +  /// This algorithm is only used on paths of URIs with a scheme, | 
| +  /// and it treats the path as if it is absolute (leading '..' are removed). | 
| +  static String _removeDotSegments(String path) { | 
| +    if (!_mayContainDotSegments(path)) return path; | 
| +    assert(path.isNotEmpty);  // An empty path would not have dot segments. | 
| +    List<String> output = []; | 
| +    bool appendSlash = false; | 
| +    for (String segment in path.split("/")) { | 
| +      appendSlash = false; | 
| +      if (segment == "..") { | 
| +        if (output.isNotEmpty) { | 
| +          output.removeLast(); | 
| +          if (output.isEmpty) { | 
| +            output.add(""); | 
| +          } | 
| +        } | 
| +        appendSlash = true; | 
| +      } else if ("." == segment) { | 
| +        appendSlash = true; | 
| +      } else { | 
| +        output.add(segment); | 
| +      } | 
| } | 
| -    return combine(scheme, combine(userInfo, combine(host, combine(port, | 
| -        combine(path, combine(query, combine(fragment, 1))))))); | 
| +    if (appendSlash) output.add(""); | 
| +    return output.join("/"); | 
| } | 
|  | 
| -  static void _addIfNonEmpty(StringBuffer sb, String test, | 
| -                             String first, String second) { | 
| -    if ("" != test) { | 
| -      sb.write(first); | 
| -      sb.write(second); | 
| +  /// Removes all `.` segments and any non-leading `..` segments. | 
| +  /// | 
| +  /// Removing the ".." from a "bar/foo/.." sequence results in "bar/" | 
| +  /// (trailing "/"). If the entire path is removed (because it contains as | 
| +  /// many ".." segments as real segments), the result is "./". | 
| +  /// This is different from an empty string, which represents "no path", | 
| +  /// when you resolve it against a base URI with a path with a non-empty | 
| +  /// final segment. | 
| +  static String _normalizeRelativePath(String path) { | 
| +    assert(!path.startsWith('/'));  // Only get called for relative paths. | 
| +    if (!_mayContainDotSegments(path)) return path; | 
| +    assert(path.isNotEmpty);  // An empty path would not have dot segments. | 
| +    List<String> output = []; | 
| +    bool appendSlash = false; | 
| +    for (String segment in path.split("/")) { | 
| +      appendSlash = false; | 
| +      if (".." == segment) { | 
| +        if (!output.isEmpty && output.last != "..") { | 
| +          output.removeLast(); | 
| +          appendSlash = true; | 
| +        } else { | 
| +          output.add(".."); | 
| +        } | 
| +      } else if ("." == segment) { | 
| +        appendSlash = true; | 
| +      } else { | 
| +        output.add(segment); | 
| +      } | 
| } | 
| +    if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { | 
| +      return "./"; | 
| +    } | 
| +    if (appendSlash || output.last == '..') output.add(""); | 
| +    return output.join("/"); | 
| } | 
|  | 
| -  /** | 
| -   * Encode the string [component] using percent-encoding to make it | 
| -   * safe for literal use as a URI component. | 
| -   * | 
| -   * All characters except uppercase and lowercase letters, digits and | 
| -   * the characters `-_.!~*'()` are percent-encoded. This is the | 
| -   * set of characters specified in RFC 2396 and the which is | 
| -   * specified for the encodeUriComponent in ECMA-262 version 5.1. | 
| -   * | 
| -   * When manually encoding path segments or query components remember | 
| -   * to encode each part separately before building the path or query | 
| -   * string. | 
| -   * | 
| -   * For encoding the query part consider using | 
| -   * [encodeQueryComponent]. | 
| -   * | 
| -   * To avoid the need for explicitly encoding use the [pathSegments] | 
| -   * and [queryParameters] optional named arguments when constructing | 
| -   * a [Uri]. | 
| -   */ | 
| -  static String encodeComponent(String component) { | 
| -    return _uriEncode(_unreserved2396Table, component, UTF8, false); | 
| +  Uri resolve(String reference) { | 
| +    return resolveUri(Uri.parse(reference)); | 
| } | 
|  | 
| -  /** | 
| -   * Encode the string [component] according to the HTML 4.01 rules | 
| -   * for encoding the posting of a HTML form as a query string | 
| -   * component. | 
| -   * | 
| -   * Encode the string [component] according to the HTML 4.01 rules | 
| -   * for encoding the posting of a HTML form as a query string | 
| -   * component. | 
| - | 
| -   * The component is first encoded to bytes using [encoding]. | 
| -   * The default is to use [UTF8] encoding, which preserves all | 
| -   * the characters that don't need encoding. | 
| - | 
| -   * Then the resulting bytes are "percent-encoded". This transforms | 
| -   * spaces (U+0020) to a plus sign ('+') and all bytes that are not | 
| -   * the ASCII decimal digits, letters or one of '-._~' are written as | 
| -   * a percent sign '%' followed by the two-digit hexadecimal | 
| -   * representation of the byte. | 
| - | 
| -   * Note that the set of characters which are percent-encoded is a | 
| -   * superset of what HTML 4.01 requires, since it refers to RFC 1738 | 
| -   * for reserved characters. | 
| -   * | 
| -   * When manually encoding query components remember to encode each | 
| -   * part separately before building the query string. | 
| -   * | 
| -   * To avoid the need for explicitly encoding the query use the | 
| -   * [queryParameters] optional named arguments when constructing a | 
| -   * [Uri]. | 
| -   * | 
| -   * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | 
| -   * details. | 
| -   */ | 
| -  static String encodeQueryComponent(String component, | 
| -                                     {Encoding encoding: UTF8}) { | 
| -    return _uriEncode(_unreservedTable, component, encoding, true); | 
| +  Uri resolveUri(Uri reference) { | 
| +    // From RFC 3986. | 
| +    String targetScheme; | 
| +    String targetUserInfo = ""; | 
| +    String targetHost; | 
| +    int targetPort; | 
| +    String targetPath; | 
| +    String targetQuery; | 
| +    if (reference.scheme.isNotEmpty) { | 
| +      targetScheme = reference.scheme; | 
| +      if (reference.hasAuthority) { | 
| +        targetUserInfo = reference.userInfo; | 
| +        targetHost = reference.host; | 
| +        targetPort = reference.hasPort ? reference.port : null; | 
| +      } | 
| +      targetPath = _removeDotSegments(reference.path); | 
| +      if (reference.hasQuery) { | 
| +        targetQuery = reference.query; | 
| +      } | 
| +    } else { | 
| +      targetScheme = this.scheme; | 
| +      if (reference.hasAuthority) { | 
| +        targetUserInfo = reference.userInfo; | 
| +        targetHost = reference.host; | 
| +        targetPort = _makePort(reference.hasPort ? reference.port : null, | 
| +                               targetScheme); | 
| +        targetPath = _removeDotSegments(reference.path); | 
| +        if (reference.hasQuery) targetQuery = reference.query; | 
| +      } else { | 
| +        targetUserInfo = this._userInfo; | 
| +        targetHost = this._host; | 
| +        targetPort = this._port; | 
| +        if (reference.path == "") { | 
| +          targetPath = this._path; | 
| +          if (reference.hasQuery) { | 
| +            targetQuery = reference.query; | 
| +          } else { | 
| +            targetQuery = this._query; | 
| +          } | 
| +        } else { | 
| +          if (reference.hasAbsolutePath) { | 
| +            targetPath = _removeDotSegments(reference.path); | 
| +          } else { | 
| +            // This is the RFC 3986 behavior for merging. | 
| +            if (this.hasEmptyPath) { | 
| +              if (!this.hasAuthority) { | 
| +                if (!this.hasScheme) { | 
| +                  // Keep the path relative if no scheme or authority. | 
| +                  targetPath = reference.path; | 
| +                } else { | 
| +                  // Remove leading dot-segments if the path is put | 
| +                  // beneath a scheme. | 
| +                  targetPath = _removeDotSegments(reference.path); | 
| +                } | 
| +              } else { | 
| +                // RFC algorithm for base with authority and empty path. | 
| +                targetPath = _removeDotSegments("/" + reference.path); | 
| +              } | 
| +            } else { | 
| +              var mergedPath = _mergePaths(this._path, reference.path); | 
| +              if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { | 
| +                targetPath = _removeDotSegments(mergedPath); | 
| +              } else { | 
| +                // Non-RFC 3986 behavior. | 
| +                // If both base and reference are relative paths, | 
| +                // allow the merged path to start with "..". | 
| +                // The RFC only specifies the case where the base has a scheme. | 
| +                targetPath = _normalizeRelativePath(mergedPath); | 
| +              } | 
| +            } | 
| +          } | 
| +          if (reference.hasQuery) targetQuery = reference.query; | 
| +        } | 
| +      } | 
| +    } | 
| +    String fragment = reference.hasFragment ? reference.fragment : null; | 
| +    return new _Uri._internal(targetScheme, | 
| +                              targetUserInfo, | 
| +                              targetHost, | 
| +                              targetPort, | 
| +                              targetPath, | 
| +                              targetQuery, | 
| +                              fragment); | 
| } | 
|  | 
| -  /** | 
| -   * Decodes the percent-encoding in [encodedComponent]. | 
| -   * | 
| -   * Note that decoding a URI component might change its meaning as | 
| -   * some of the decoded characters could be characters with are | 
| -   * delimiters for a given URI componene type. Always split a URI | 
| -   * component using the delimiters for the component before decoding | 
| -   * the individual parts. | 
| -   * | 
| -   * For handling the [path] and [query] components consider using | 
| -   * [pathSegments] and [queryParameters] to get the separated and | 
| -   * decoded component. | 
| -   */ | 
| -  static String decodeComponent(String encodedComponent) { | 
| -    return _uriDecode(encodedComponent, 0, encodedComponent.length, | 
| -                      UTF8, false); | 
| -  } | 
| +  bool get hasScheme => scheme.isNotEmpty; | 
|  | 
| -  /** | 
| -   * Decodes the percent-encoding in [encodedComponent], converting | 
| -   * pluses to spaces. | 
| -   * | 
| -   * It will create a byte-list of the decoded characters, and then use | 
| -   * [encoding] to decode the byte-list to a String. The default encoding is | 
| -   * UTF-8. | 
| -   */ | 
| -  static String decodeQueryComponent( | 
| -      String encodedComponent, | 
| -      {Encoding encoding: UTF8}) { | 
| -    return _uriDecode(encodedComponent, 0, encodedComponent.length, | 
| -                      encoding, true); | 
| -  } | 
| +  bool get hasAuthority => _host != null; | 
|  | 
| -  /** | 
| -   * Encode the string [uri] using percent-encoding to make it | 
| -   * safe for literal use as a full URI. | 
| -   * | 
| -   * All characters except uppercase and lowercase letters, digits and | 
| -   * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | 
| -   * is the set of characters specified in in ECMA-262 version 5.1 for | 
| -   * the encodeURI function . | 
| -   */ | 
| -  static String encodeFull(String uri) { | 
| -    return _uriEncode(_encodeFullTable, uri, UTF8, false); | 
| -  } | 
| +  bool get hasPort => _port != null; | 
|  | 
| -  /** | 
| -   * Decodes the percent-encoding in [uri]. | 
| -   * | 
| -   * Note that decoding a full URI might change its meaning as some of | 
| -   * the decoded characters could be reserved characters. In most | 
| -   * cases an encoded URI should be parsed into components using | 
| -   * [Uri.parse] before decoding the separate components. | 
| -   */ | 
| -  static String decodeFull(String uri) { | 
| -    return _uriDecode(uri, 0, uri.length, UTF8, false); | 
| -  } | 
| +  bool get hasQuery => _query != null; | 
|  | 
| -  /** | 
| -   * Returns the [query] split into a map according to the rules | 
| -   * specified for FORM post in the [HTML 4.01 specification section | 
| -   * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). | 
| -   * Each key and value in the returned map has been decoded. If the [query] | 
| -   * is the empty string an empty map is returned. | 
| -   * | 
| -   * Keys in the query string that have no value are mapped to the | 
| -   * empty string. | 
| -   * | 
| -   * Each query component will be decoded using [encoding]. The default encoding | 
| -   * is UTF-8. | 
| -   */ | 
| -  static Map<String, String> splitQueryString(String query, | 
| -                                              {Encoding encoding: UTF8}) { | 
| -    return query.split("&").fold({}, (map, element) { | 
| -      int index = element.indexOf("="); | 
| -      if (index == -1) { | 
| -        if (element != "") { | 
| -          map[decodeQueryComponent(element, encoding: encoding)] = ""; | 
| -        } | 
| -      } else if (index != 0) { | 
| -        var key = element.substring(0, index); | 
| -        var value = element.substring(index + 1); | 
| -        map[Uri.decodeQueryComponent(key, encoding: encoding)] = | 
| -            decodeQueryComponent(value, encoding: encoding); | 
| -      } | 
| -      return map; | 
| -    }); | 
| -  } | 
| +  bool get hasFragment => _fragment != null; | 
|  | 
| -  static List _createList() => []; | 
| +  bool get hasEmptyPath => _path.isEmpty; | 
|  | 
| -  static Map _splitQueryStringAll( | 
| -      String query, {Encoding encoding: UTF8}) { | 
| -    Map result = {}; | 
| -    int i = 0; | 
| -    int start = 0; | 
| -    int equalsIndex = -1; | 
| +  bool get hasAbsolutePath => _path.startsWith('/'); | 
|  | 
| -    void parsePair(int start, int equalsIndex, int end) { | 
| -      String key; | 
| -      String value; | 
| -      if (start == end) return; | 
| -      if (equalsIndex < 0) { | 
| -        key =  _uriDecode(query, start, end, encoding, true); | 
| -        value = ""; | 
| -      } else { | 
| -        key = _uriDecode(query, start, equalsIndex, encoding, true); | 
| -        value = _uriDecode(query, equalsIndex + 1, end, encoding, true); | 
| -      } | 
| -      result.putIfAbsent(key, _createList).add(value); | 
| +  String get origin { | 
| +    if (scheme == "" || _host == null || _host == "") { | 
| +      throw new StateError("Cannot use origin without a scheme: $this"); | 
| } | 
| - | 
| -    const int _equals = 0x3d; | 
| -    const int _ampersand = 0x26; | 
| -    while (i < query.length) { | 
| -      int char = query.codeUnitAt(i); | 
| -      if (char == _equals) { | 
| -        if (equalsIndex < 0) equalsIndex = i; | 
| -      } else if (char == _ampersand) { | 
| -        parsePair(start, equalsIndex, i); | 
| -        start = i + 1; | 
| -        equalsIndex = -1; | 
| -      } | 
| -      i++; | 
| +    if (scheme != "http" && scheme != "https") { | 
| +      throw new StateError( | 
| +        "Origin is only applicable schemes http and https: $this"); | 
| } | 
| -    parsePair(start, equalsIndex, i); | 
| -    return result; | 
| +    if (_port == null) return "$scheme://$_host"; | 
| +    return "$scheme://$_host:$_port"; | 
| } | 
|  | 
| -  /** | 
| -   * Parse the [host] as an IP version 4 (IPv4) address, returning the address | 
| -   * as a list of 4 bytes in network byte order (big endian). | 
| -   * | 
| -   * Throws a [FormatException] if [host] is not a valid IPv4 address | 
| -   * representation. | 
| -   */ | 
| -  static List<int> parseIPv4Address(String host) { | 
| -    void error(String msg) { | 
| -      throw new FormatException('Illegal IPv4 address, $msg'); | 
| +  String toFilePath({bool windows}) { | 
| +    if (scheme != "" && scheme != "file") { | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a file path from a $scheme URI"); | 
| } | 
| -    var bytes = host.split('.'); | 
| -    if (bytes.length != 4) { | 
| -      error('IPv4 address should contain exactly 4 parts'); | 
| +    if (query != "") { | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a file path from a URI with a query component"); | 
| } | 
| -    // TODO(ajohnsen): Consider using Uint8List. | 
| -    return bytes | 
| -        .map((byteString) { | 
| -          int byte = int.parse(byteString); | 
| -          if (byte < 0 || byte > 255) { | 
| -            error('each part must be in the range of `0..255`'); | 
| -          } | 
| -          return byte; | 
| -        }) | 
| -        .toList(); | 
| +    if (fragment != "") { | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a file path from a URI with a fragment component"); | 
| +    } | 
| +    if (windows == null) windows = _isWindows; | 
| +    return windows ? _toWindowsFilePath(this) : _toFilePath(); | 
| } | 
|  | 
| -  /** | 
| -   * Parse the [host] as an IP version 6 (IPv6) address, returning the address | 
| -   * as a list of 16 bytes in network byte order (big endian). | 
| -   * | 
| -   * Throws a [FormatException] if [host] is not a valid IPv6 address | 
| -   * representation. | 
| -   * | 
| -   * Acts on the substring from [start] to [end]. If [end] is omitted, it | 
| -   * defaults ot the end of the string. | 
| -   * | 
| -   * Some examples of IPv6 addresses: | 
| -   *  * ::1 | 
| -   *  * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | 
| -   *  * 3ffe:2a00:100:7031::1 | 
| -   *  * ::FFFF:129.144.52.38 | 
| -   *  * 2010:836B:4179::836B:4179 | 
| -   */ | 
| -  static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | 
| -    if (end == null) end = host.length; | 
| -    // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated | 
| -    // by `:`'s, with the following exceptions: | 
| -    // | 
| -    //  - One (and only one) wildcard (`::`) may be present, representing a fill | 
| -    //    of 0's. The IPv6 `::` is thus 16 bytes of `0`. | 
| -    //  - The last two parts may be replaced by an IPv4 address. | 
| -    void error(String msg, [position]) { | 
| -      throw new FormatException('Illegal IPv6 address, $msg', host, position); | 
| +  String _toFilePath() { | 
| +    if (hasAuthority && host != "") { | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a non-Windows file path from a file URI " | 
| +          "with an authority"); | 
| } | 
| -    int parseHex(int start, int end) { | 
| -      if (end - start > 4) { | 
| -        error('an IPv6 part can only contain a maximum of 4 hex digits', start); | 
| -      } | 
| -      int value = int.parse(host.substring(start, end), radix: 16); | 
| -      if (value < 0 || value > (1 << 16) - 1) { | 
| -        error('each part must be in the range of `0x0..0xFFFF`', start); | 
| -      } | 
| -      return value; | 
| +    // Use path segments to have any escapes unescaped. | 
| +    var pathSegments = this.pathSegments; | 
| +    _checkNonWindowsPathReservedCharacters(pathSegments, false); | 
| +    var result = new StringBuffer(); | 
| +    if (hasAbsolutePath) result.write("/"); | 
| +    result.writeAll(pathSegments, "/"); | 
| +    return result.toString(); | 
| +  } | 
| + | 
| +  static String _toWindowsFilePath(Uri uri) { | 
| +    bool hasDriveLetter = false; | 
| +    var segments = uri.pathSegments; | 
| +    if (segments.length > 0 && | 
| +        segments[0].length == 2 && | 
| +        segments[0].codeUnitAt(1) == _COLON) { | 
| +      _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); | 
| +      _checkWindowsPathReservedCharacters(segments, false, 1); | 
| +      hasDriveLetter = true; | 
| +    } else { | 
| +      _checkWindowsPathReservedCharacters(segments, false, 0); | 
| } | 
| -    if (host.length < 2) error('address is too short'); | 
| -    List<int> parts = []; | 
| -    bool wildcardSeen = false; | 
| -    int partStart = start; | 
| -    // Parse all parts, except a potential last one. | 
| -    for (int i = start; i < end; i++) { | 
| -      if (host.codeUnitAt(i) == _COLON) { | 
| -        if (i == start) { | 
| -          // If we see a `:` in the beginning, expect wildcard. | 
| -          i++; | 
| -          if (host.codeUnitAt(i) != _COLON) { | 
| -            error('invalid start colon.', i); | 
| -          } | 
| -          partStart = i; | 
| -        } | 
| -        if (i == partStart) { | 
| -          // Wildcard. We only allow one. | 
| -          if (wildcardSeen) { | 
| -            error('only one wildcard `::` is allowed', i); | 
| -          } | 
| -          wildcardSeen = true; | 
| -          parts.add(-1); | 
| -        } else { | 
| -          // Found a single colon. Parse [partStart..i] as a hex entry. | 
| -          parts.add(parseHex(partStart, i)); | 
| -        } | 
| -        partStart = i + 1; | 
| +    var result = new StringBuffer(); | 
| +    if (uri.hasAbsolutePath && !hasDriveLetter) result.write(r"\"); | 
| +    if (uri.hasAuthority) { | 
| +      var host = uri.host; | 
| +      if (host.isNotEmpty) { | 
| +        result.write(r"\"); | 
| +        result.write(host); | 
| +        result.write(r"\"); | 
| } | 
| } | 
| -    if (parts.length == 0) error('too few parts'); | 
| -    bool atEnd = (partStart == end); | 
| -    bool isLastWildcard = (parts.last == -1); | 
| -    if (atEnd && !isLastWildcard) { | 
| -      error('expected a part after last `:`', end); | 
| +    result.writeAll(segments, r"\"); | 
| +    if (hasDriveLetter && segments.length == 1) result.write(r"\"); | 
| +    return result.toString(); | 
| +  } | 
| + | 
| +  bool get _isPathAbsolute { | 
| +    return _path != null && _path.startsWith('/'); | 
| +  } | 
| + | 
| +  void _writeAuthority(StringSink ss) { | 
| +    if (_userInfo.isNotEmpty) { | 
| +      ss.write(_userInfo); | 
| +      ss.write("@"); | 
| +    } | 
| +    if (_host != null) ss.write(_host); | 
| +    if (_port != null) { | 
| +      ss.write(":"); | 
| +      ss.write(_port); | 
| +    } | 
| +  } | 
| + | 
| +  /** | 
| +   * Access the structure of a `data:` URI. | 
| +   * | 
| +   * Returns a [UriData] object for `data:` URIs and `null` for all other | 
| +   * URIs. | 
| +   * The [UriData] object can be used to access the media type and data | 
| +   * of a `data:` URI. | 
| +   */ | 
| +  UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; | 
| + | 
| +  String toString() { | 
| +    return _text ??= _initializeText(); | 
| +  } | 
| + | 
| +  String _initializeText() { | 
| +    assert(_text == null); | 
| +    StringBuffer sb = new StringBuffer(); | 
| +    if (scheme.isNotEmpty) sb..write(scheme)..write(":"); | 
| +    if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | 
| +      // File URIS always have the authority, even if it is empty. | 
| +      // The empty URI means "localhost". | 
| +      sb.write("//"); | 
| +      _writeAuthority(sb); | 
| } | 
| -    if (!atEnd) { | 
| -      try { | 
| -        parts.add(parseHex(partStart, end)); | 
| -      } catch (e) { | 
| -        // Failed to parse the last chunk as hex. Try IPv4. | 
| -        try { | 
| -          List<int> last = parseIPv4Address(host.substring(partStart, end)); | 
| -          parts.add(last[0] << 8 | last[1]); | 
| -          parts.add(last[2] << 8 | last[3]); | 
| -        } catch (e) { | 
| -          error('invalid end of IPv6 address.', partStart); | 
| -        } | 
| -      } | 
| +    sb.write(path); | 
| +    if (_query != null) sb..write("?")..write(_query); | 
| +    if (_fragment != null) sb..write("#")..write(_fragment); | 
| +    return sb.toString(); | 
| +  } | 
| + | 
| +  bool operator==(other) { | 
| +    if (identical(this, other)) return true; | 
| +    if (other is Uri) { | 
| +      Uri uri = other; | 
| +      return scheme       == uri.scheme       && | 
| +             hasAuthority == uri.hasAuthority && | 
| +             userInfo     == uri.userInfo     && | 
| +             host         == uri.host         && | 
| +             port         == uri.port         && | 
| +             path         == uri.path         && | 
| +             hasQuery     == uri.hasQuery     && | 
| +             query        == uri.query        && | 
| +             hasFragment  == uri.hasFragment  && | 
| +             fragment     == uri.fragment; | 
| } | 
| -    if (wildcardSeen) { | 
| -      if (parts.length > 7) { | 
| -        error('an address with a wildcard must have less than 7 parts'); | 
| +    return false; | 
| +  } | 
| + | 
| +  int get hashCode { | 
| +    return _hashCodeCache ??= toString().hashCode; | 
| +  } | 
| + | 
| +  static List _createList() => []; | 
| + | 
| +  static Map _splitQueryStringAll( | 
| +      String query, {Encoding encoding: UTF8}) { | 
| +    Map result = {}; | 
| +    int i = 0; | 
| +    int start = 0; | 
| +    int equalsIndex = -1; | 
| + | 
| +    void parsePair(int start, int equalsIndex, int end) { | 
| +      String key; | 
| +      String value; | 
| +      if (start == end) return; | 
| +      if (equalsIndex < 0) { | 
| +        key =  _uriDecode(query, start, end, encoding, true); | 
| +        value = ""; | 
| +      } else { | 
| +        key = _uriDecode(query, start, equalsIndex, encoding, true); | 
| +        value = _uriDecode(query, equalsIndex + 1, end, encoding, true); | 
| } | 
| -    } else if (parts.length != 8) { | 
| -      error('an address without a wildcard must contain exactly 8 parts'); | 
| +      result.putIfAbsent(key, _createList).add(value); | 
| } | 
| -    List<int> bytes = new Uint8List(16); | 
| -    for (int i = 0, index = 0; i < parts.length; i++) { | 
| -      int value = parts[i]; | 
| -      if (value == -1) { | 
| -        int wildCardLength = 9 - parts.length; | 
| -        for (int j = 0; j < wildCardLength; j++) { | 
| -          bytes[index] = 0; | 
| -          bytes[index + 1] = 0; | 
| -          index += 2; | 
| -        } | 
| -      } else { | 
| -        bytes[index] = value >> 8; | 
| -        bytes[index + 1] = value & 0xff; | 
| -        index += 2; | 
| + | 
| +    const int _equals = 0x3d; | 
| +    const int _ampersand = 0x26; | 
| +    while (i < query.length) { | 
| +      int char = query.codeUnitAt(i); | 
| +      if (char == _equals) { | 
| +        if (equalsIndex < 0) equalsIndex = i; | 
| +      } else if (char == _ampersand) { | 
| +        parsePair(start, equalsIndex, i); | 
| +        start = i + 1; | 
| +        equalsIndex = -1; | 
| } | 
| +      i++; | 
| } | 
| -    return bytes; | 
| +    parsePair(start, equalsIndex, i); | 
| +    return result; | 
| } | 
|  | 
| -  // Frequently used character codes. | 
| -  static const int _SPACE = 0x20; | 
| -  static const int _DOUBLE_QUOTE = 0x22; | 
| -  static const int _NUMBER_SIGN = 0x23; | 
| -  static const int _PERCENT = 0x25; | 
| -  static const int _ASTERISK = 0x2A; | 
| -  static const int _PLUS = 0x2B; | 
| -  static const int _DOT = 0x2E; | 
| -  static const int _SLASH = 0x2F; | 
| -  static const int _ZERO = 0x30; | 
| -  static const int _NINE = 0x39; | 
| -  static const int _COLON = 0x3A; | 
| -  static const int _LESS = 0x3C; | 
| -  static const int _GREATER = 0x3E; | 
| -  static const int _QUESTION = 0x3F; | 
| -  static const int _AT_SIGN = 0x40; | 
| -  static const int _UPPER_CASE_A = 0x41; | 
| -  static const int _UPPER_CASE_F = 0x46; | 
| -  static const int _UPPER_CASE_Z = 0x5A; | 
| -  static const int _LEFT_BRACKET = 0x5B; | 
| -  static const int _BACKSLASH = 0x5C; | 
| -  static const int _RIGHT_BRACKET = 0x5D; | 
| -  static const int _LOWER_CASE_A = 0x61; | 
| -  static const int _LOWER_CASE_F = 0x66; | 
| -  static const int _LOWER_CASE_Z = 0x7A; | 
| -  static const int _BAR = 0x7C; | 
| - | 
| -  static const String _hexDigits = "0123456789ABCDEF"; | 
| - | 
| external static String _uriEncode(List<int> canonicalTable, | 
| String text, | 
| Encoding encoding, | 
| @@ -2941,13 +3189,13 @@ class UriData { | 
| throw new ArgumentError.value(mimeType, "mimeType", | 
| "Invalid MIME type"); | 
| } | 
| -      buffer.write(Uri._uriEncode(_tokenCharTable, | 
| -                                  mimeType.substring(0, slashIndex), | 
| -                                  UTF8, false)); | 
| +      buffer.write(_Uri._uriEncode(_tokenCharTable, | 
| +                                   mimeType.substring(0, slashIndex), | 
| +                                   UTF8, false)); | 
| buffer.write("/"); | 
| -      buffer.write(Uri._uriEncode(_tokenCharTable, | 
| -                                  mimeType.substring(slashIndex + 1), | 
| -                                  UTF8, false)); | 
| +      buffer.write(_Uri._uriEncode(_tokenCharTable, | 
| +                                   mimeType.substring(slashIndex + 1), | 
| +                                   UTF8, false)); | 
| } | 
| if (charsetName != null) { | 
| if (indices != null) { | 
| @@ -2955,7 +3203,7 @@ class UriData { | 
| ..add(buffer.length + 8); | 
| } | 
| buffer.write(";charset="); | 
| -      buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); | 
| +      buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); | 
| } | 
| parameters?.forEach((var key, var value) { | 
| if (key.isEmpty) { | 
| @@ -2968,10 +3216,10 @@ class UriData { | 
| if (indices != null) indices.add(buffer.length); | 
| buffer.write(';'); | 
| // Encode any non-RFC2045-token character and both '%' and '#'. | 
| -      buffer.write(Uri._uriEncode(_tokenCharTable, key, UTF8, false)); | 
| +      buffer.write(_Uri._uriEncode(_tokenCharTable, key, UTF8, false)); | 
| if (indices != null) indices.add(buffer.length); | 
| buffer.write('='); | 
| -      buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); | 
| +      buffer.write(_Uri._uriEncode(_tokenCharTable, value, UTF8, false)); | 
| }); | 
| } | 
|  | 
| @@ -2988,7 +3236,7 @@ class UriData { | 
| int slashIndex = -1; | 
| for (int i = 0; i < mimeType.length; i++) { | 
| var char = mimeType.codeUnitAt(i); | 
| -      if (char != Uri._SLASH) continue; | 
| +      if (char != _SLASH) continue; | 
| if (slashIndex < 0) { | 
| slashIndex = i; | 
| continue; | 
| @@ -3008,7 +3256,7 @@ class UriData { | 
| * ```` | 
| * | 
| * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, | 
| -   * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). | 
| +   * and `data` is a sequence of URI-characters (RFC-2396 `uric`). | 
| * | 
| * This means that all the characters must be ASCII, but the URI may contain | 
| * percent-escapes for non-ASCII byte values that need an interpretation | 
| @@ -3019,13 +3267,22 @@ class UriData { | 
| * and `,` delimiters. | 
| * | 
| * Accessing the individual parts may fail later if they turn out to have | 
| -   * content that can't be decoded sucessfully as a string. | 
| +   * content that can't be decoded successfully as a string. | 
| */ | 
| static UriData parse(String uri) { | 
| -    if (!uri.startsWith("data:")) { | 
| -      throw new FormatException("Does not start with 'data:'", uri, 0); | 
| +    if (uri.length >= 5) { | 
| +      int dataDelta = _startsWithData(uri, 0); | 
| +      if (dataDelta == 0) { | 
| +        // Exact match on "data:". | 
| +        return _parse(uri, 5, null); | 
| +      } | 
| +      if (dataDelta == 0x20) { | 
| +        // Starts with a non-normalized "data" scheme containing upper-case | 
| +        // letters. Parse anyway, but throw away the scheme. | 
| +        return _parse(uri.substring(5), 0, null); | 
| +      } | 
| } | 
| -    return _parse(uri, 5, null); | 
| +    throw new FormatException("Does not start with 'data:'", uri, 0); | 
| } | 
|  | 
| /** | 
| @@ -3050,7 +3307,7 @@ class UriData { | 
| // That's perfectly reasonable - data URIs are not hierarchical, | 
| // but it may make some consumers stumble. | 
| // Should we at least do escape normalization? | 
| -    _uriCache = new Uri._internal("data", "", null, null, path, query, null); | 
| +    _uriCache = new _Uri._internal("data", "", null, null, path, query, null); | 
| return _uriCache; | 
| } | 
|  | 
| @@ -3075,7 +3332,7 @@ class UriData { | 
| int start = _separatorIndices[0] + 1; | 
| int end = _separatorIndices[1]; | 
| if (start == end) return "text/plain"; | 
| -    return Uri._uriDecode(_text, start, end, UTF8, false); | 
| +    return _Uri._uriDecode(_text, start, end, UTF8, false); | 
| } | 
|  | 
| /** | 
| @@ -3096,8 +3353,8 @@ class UriData { | 
| var keyStart = _separatorIndices[i] + 1; | 
| var keyEnd = _separatorIndices[i + 1]; | 
| if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { | 
| -        return Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], | 
| -                              UTF8, false); | 
| +        return _Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], | 
| +                               UTF8, false); | 
| } | 
| } | 
| return "US-ASCII"; | 
| @@ -3155,8 +3412,8 @@ class UriData { | 
| result[index++] = codeUnit; | 
| } else { | 
| if (i + 2 < text.length) { | 
| -          var digit1 = Uri._parseHexDigit(text.codeUnitAt(i + 1)); | 
| -          var digit2 = Uri._parseHexDigit(text.codeUnitAt(i + 2)); | 
| +          var digit1 = _Uri._parseHexDigit(text.codeUnitAt(i + 1)); | 
| +          var digit2 = _Uri._parseHexDigit(text.codeUnitAt(i + 2)); | 
| if (digit1 >= 0 && digit2 >= 0) { | 
| int byte = digit1 * 16 + digit2; | 
| result[index++] = byte; | 
| @@ -3177,7 +3434,7 @@ class UriData { | 
| * If the content is Base64 encoded, it will be decoded to bytes and then | 
| * decoded to a string using [encoding]. | 
| * If encoding is omitted, the value of a `charset` parameter is used | 
| -   * if it is recongized by [Encoding.getByName], otherwise it defaults to | 
| +   * if it is recognized by [Encoding.getByName], otherwise it defaults to | 
| * the [ASCII] encoding, which is the default encoding for data URIs | 
| * that do not specify an encoding. | 
| * | 
| @@ -3199,7 +3456,7 @@ class UriData { | 
| var converter = BASE64.decoder.fuse(encoding.decoder); | 
| return converter.convert(text.substring(start)); | 
| } | 
| -    return Uri._uriDecode(text, start, text.length, encoding, false); | 
| +    return _Uri._uriDecode(text, start, text.length, encoding, false); | 
| } | 
|  | 
| /** | 
| @@ -3222,8 +3479,8 @@ class UriData { | 
| var start = _separatorIndices[i - 2] + 1; | 
| var equals = _separatorIndices[i - 1]; | 
| var end = _separatorIndices[i]; | 
| -      String key = Uri._uriDecode(_text, start, equals, UTF8, false); | 
| -      String value = Uri._uriDecode(_text,equals + 1, end, UTF8, false); | 
| +      String key = _Uri._uriDecode(_text, start, equals, UTF8, false); | 
| +      String value = _Uri._uriDecode(_text,equals + 1, end, UTF8, false); | 
| result[key] = value; | 
| } | 
| return result; | 
| @@ -3306,9 +3563,9 @@ class UriData { | 
| ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | 
| buffer.writeCharCode(byte); | 
| } else { | 
| -        buffer.writeCharCode(Uri._PERCENT); | 
| -        buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte >> 4)); | 
| -        buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte & 0x0f)); | 
| +        buffer.writeCharCode(_PERCENT); | 
| +        buffer.writeCharCode(_hexDigits.codeUnitAt(byte >> 4)); | 
| +        buffer.writeCharCode(_hexDigits.codeUnitAt(byte & 0x0f)); | 
| } | 
| } | 
| if ((byteOr & ~0xFF) != 0) { | 
| @@ -3357,5 +3614,852 @@ class UriData { | 
| //  mark        =  "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" | 
| // | 
| // This is the same characters as in a URI query (which is URI pchar plus '?') | 
| -  static const _uricTable = Uri._queryCharTable; | 
| +  static const _uricTable = _Uri._queryCharTable; | 
| +} | 
| + | 
| +// -------------------------------------------------------------------- | 
| +// Constants used to read the scanner result. | 
| +// The indices points into the table filled by [_scan] which contains | 
| +// recognized positions in the scanned URI. | 
| +// The `0` index is only used internally. | 
| + | 
| +/// Index of the position of that `:` after a scheme. | 
| +const int _schemeEndIndex     = 1; | 
| +/// Index of the position of the character just before the host name. | 
| +const int _hostStartIndex     = 2; | 
| +/// Index of the position of the `:` before a port value. | 
| +const int _portStartIndex     = 3; | 
| +/// Index of the position of the first character of a path. | 
| +const int _pathStartIndex     = 4; | 
| +/// Index of the position of the `?` before a query. | 
| +const int _queryStartIndex    = 5; | 
| +/// Index of the position of the `#` before a fragment. | 
| +const int _fragmentStartIndex = 6; | 
| +/// Index of a position where the URI was determined to be "non-simple". | 
| +const int _notSimpleIndex     = 7; | 
| + | 
| +// Initial state for scanner. | 
| +const int _uriStart           = 00; | 
| + | 
| +// If scanning of a URI terminates in this state or above, | 
| +// consider the URI non-simple | 
| +const int _nonSimpleEndStates = 14; | 
| + | 
| +// Initial state for scheme validation. | 
| +const int _schemeStart        = 20; | 
| + | 
| +/// Transition tables used to scan a URI to determine its structure. | 
| +/// | 
| +/// The tables represent a state machine with output. | 
| +/// | 
| +/// To scan the URI, start in the [_uriStart] state, then read each character | 
| +/// of the URI in order, from start to end, and for each character perform a | 
| +/// transition to a new state while writing the current position into the output | 
| +/// buffer at a designated index. | 
| +/// | 
| +/// Each state, represented by an integer which is an index into | 
| +/// [_scannerTables], has a set of transitions, one for each character. | 
| +/// The transitions are encoded as a 5-bit integer representing the next state | 
| +/// and a 3-bit index into the output table. | 
| +/// | 
| +/// For URI scanning, only characters in the range U+0020 through U+007E are | 
| +/// interesting, all characters outside that range are treated the same. | 
| +/// The tables only contain 96 entries, representing that characters in the | 
| +/// interesting range, plus one more to represent all values outside the range. | 
| +/// The character entries are stored in one `Uint8List` per state, with the | 
| +/// transition for a character at position `character ^ 0x60`, | 
| +/// which maps the range U+0020 .. U+007F into positions 0 .. 95. | 
| +/// All remaining characters are mapped to position 31 (`0x7f ^ 0x60`) which | 
| +/// represents the transition for all remaining characters. | 
| +final List<Uint8List> _scannerTables = _createTables(); | 
| + | 
| +// ---------------------------------------------------------------------- | 
| +// Code to create the URI scanner table. | 
| + | 
| +/// Creates the tables for [_scannerTables] used by [Uri.parse]. | 
| +/// | 
| +/// See [_scannerTables] for the generated format. | 
| +/// | 
| +/// The concrete tables are chosen as a trade-off between the number of states | 
| +/// needed and the precision of the result. | 
| +/// This allows definitely recognizing the general structure of the URI | 
| +/// (presence and location of scheme, user-info, host, port, path, query and | 
| +/// fragment) while at the same time detecting that some components are not | 
| +/// in canonical form (anything containing a `%`, a host-name containing a | 
| +/// capital letter). Since the scanner doesn't know whether something is a | 
| +/// scheme or a path until it sees `:`, or user-info or host until it sees | 
| +/// a `@`, a second pass is needed to validate the scheme and any user-info | 
| +/// is considered non-canonical by default. | 
| +/// | 
| +/// The states (starting from [_uriStart]) write positions while scanning | 
| +/// a string from `start` to `end` as follows: | 
| +/// | 
| +/// - [_schemeEndIndex]: Should be initialized to `start-1`. | 
| +///   If the URI has a scheme, it is set to the position of the `:` after | 
| +///   the scheme. | 
| +/// - [_hostStartIndex]: Should be initialized to `start - 1`. | 
| +///   If the URI has an authority, it is set to the character before the | 
| +///   host name - either the second `/` in the `//` leading the authority, | 
| +///   or the `@` after a user-info. Comparing this value to the scheme end | 
| +///   position can be used to detect that there is a user-info component. | 
| +/// - [_portStartIndex]: Should be initialized to `start`. | 
| +///   Set to the position of the last `:` in an authority, and unchanged | 
| +///   if there is no authority or no `:` in an authority. | 
| +///   If this position is after the host start, there is a port, otherwise it | 
| +///   is just marking a colon in the user-info component. | 
| +/// - [_pathStartIndex]: Should be initialized to `start`. | 
| +///   Is set to the first path character unless the path is empty. | 
| +///   If the path is empty, the position is either unchanged (`start`) or | 
| +///   the first slash of an authority. So, if the path start is before a | 
| +///   host start or scheme end, the path is empty. | 
| +/// - [_queryStartIndex]: Should be initialized to `end`. | 
| +///   The position of the `?` leading a query if the URI contains a query. | 
| +/// - [_fragmentStartIndex]: Should be initialized to `end`. | 
| +///   The position of the `#` leading a fragment if the URI contains a fragment. | 
| +/// - [_notSimpleIndex]: Should be initialized to `start - 1`. | 
| +///   Set to another value if the URI is considered "not simple". | 
| +///   This is elaborated below. | 
| +/// | 
| +/// # Simple URIs | 
| +/// A URI is considered "simple" if it is in a normalized form containing no | 
| +/// escapes. This allows us to skip normalization and checking whether escapes | 
| +/// are valid, and to extract components without worrying about unescaping. | 
| +/// | 
| +/// The scanner computes a conservative approximation of being "simple". | 
| +/// It rejects any URI with an escape, with a user-info component (mainly | 
| +/// because they are rare and would increase the number of states in the | 
| +/// scanner significantly), with an IPV6 host or with a capital letter in | 
| +/// the scheme or host name (the scheme is handled in a second scan using | 
| +/// a separate two-state table). | 
| +/// Further, paths containing `..` or `.` path segments are considered | 
| +/// non-simple except for pure relative paths (no scheme or authority) starting | 
| +/// with a sequence of "../" segments. | 
| +/// | 
| +/// The transition tables cannot detect a trailing ".." in the path, | 
| +/// followed by a query or fragment, because the segment is not known to be | 
| +/// complete until we are past it, and we then need to store the query/fragment | 
| +/// start instead. This cast is checked manually post-scanning (such a path | 
| +/// needs to be normalized to end in "../", so the URI shouldn't be considered | 
| +/// simple). | 
| +List<Uint8List> _createTables() { | 
| +  // TODO(lrn): Use a precomputed table. | 
| + | 
| +  // Total number of states for the scanner. | 
| +  const int stateCount         = 22; | 
| + | 
| +  // States used to scan a URI from scratch. | 
| +  const int schemeOrPath       = 01; | 
| +  const int authOrPath         = 02; | 
| +  const int authOrPathSlash    = 03; | 
| +  const int uinfoOrHost0       = 04; | 
| +  const int uinfoOrHost        = 05; | 
| +  const int uinfoOrPort0       = 06; | 
| +  const int uinfoOrPort        = 07; | 
| +  const int ipv6Host           = 08; | 
| +  const int relPathSeg         = 09; | 
| +  const int pathSeg            = 10; | 
| +  const int path               = 11; | 
| +  const int query              = 12; | 
| +  const int fragment           = 13; | 
| +  const int schemeOrPathDot    = 14; | 
| +  const int schemeOrPathDot2   = 15; | 
| +  const int relPathSegDot      = 16; | 
| +  const int relPathSegDot2     = 17; | 
| +  const int pathSegDot         = 18; | 
| +  const int pathSegDot2        = 19; | 
| + | 
| +  // States used to validate a scheme after its end position has been found. | 
| +  const int scheme0            = _schemeStart; | 
| +  const int scheme             = 21; | 
| + | 
| +  // Constants encoding the write-index for the state transition into the top 5 | 
| +  // bits of a byte. | 
| +  const int schemeEnd          = _schemeEndIndex     << 5; | 
| +  const int hostStart          = _hostStartIndex     << 5; | 
| +  const int portStart          = _portStartIndex     << 5; | 
| +  const int pathStart          = _pathStartIndex     << 5; | 
| +  const int queryStart         = _queryStartIndex    << 5; | 
| +  const int fragmentStart      = _fragmentStartIndex << 5; | 
| +  const int notSimple          = _notSimpleIndex     << 5; | 
| + | 
| +  /// The `unreserved` characters of RFC 3986. | 
| +  const unreserved = | 
| +      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~"  ; | 
| +  /// The `sub-delim` characters of RFC 3986. | 
| +  const subDelims = r"!$&'()*+,;="; | 
| +  // The `pchar` characters of RFC 3986: characters that may occur in a path, | 
| +  // excluding escapes. | 
| +  const pchar = "$unreserved$subDelims"; | 
| + | 
| +  var tables = new List<Uint8List>.generate(stateCount, | 
| +      (_) => new Uint8List(96)); | 
| + | 
| +  // Helper function which initialize the table for [state] with a default | 
| +  // transition and returns the table. | 
| +  Uint8List build(state, defaultTransition) => | 
| +      tables[state]..fillRange(0, 96, defaultTransition); | 
| + | 
| +  // Helper function which sets the transition for each character in [chars] | 
| +  // to [transition] in the [target] table. | 
| +  // The [chars] string must contain only characters in the U+0020 .. U+007E | 
| +  // range. | 
| +  void setChars(Uint8List target, String chars, int transition) { | 
| +    for (int i = 0; i < chars.length; i++) { | 
| +      var char = chars.codeUnitAt(i); | 
| +      target[char ^ 0x60] = transition; | 
| +    } | 
| +  } | 
| + | 
| +  /// Helper function which sets the transition for all characters in the | 
| +  /// range from `range[0]` to `range[1]` to [transition] in the [target] table. | 
| +  /// | 
| +  /// The [range] must be a two-character string where both characters are in | 
| +  /// the U+0020 .. U+007E range and the former character must have a lower | 
| +  /// code point than the latter. | 
| +  void setRange(Uint8List target, String range, int transition) { | 
| +    for (int i = range.codeUnitAt(0), n = range.codeUnitAt(1); i <= n; i++) { | 
| +      target[i ^ 0x60] = transition; | 
| +    } | 
| +  } | 
| + | 
| +  // Create the transitions for each state. | 
| +  var b; | 
| + | 
| +  // Validate as path, if it is a scheme, we handle it later. | 
| +  b = build(_uriStart, schemeOrPath | notSimple); | 
| +  setChars(b, pchar, schemeOrPath); | 
| +  setChars(b, ".", schemeOrPathDot); | 
| +  setChars(b, ":", authOrPath | schemeEnd);  // Handle later. | 
| +  setChars(b, "/", authOrPathSlash); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(schemeOrPathDot, schemeOrPath | notSimple); | 
| +  setChars(b, pchar, schemeOrPath); | 
| +  setChars(b, ".", schemeOrPathDot2); | 
| +  setChars(b, ':', authOrPath | schemeEnd); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(schemeOrPathDot2, schemeOrPath | notSimple); | 
| +  setChars(b, pchar, schemeOrPath); | 
| +  setChars(b, "%", schemeOrPath | notSimple); | 
| +  setChars(b, ':', authOrPath | schemeEnd); | 
| +  setChars(b, "/", relPathSeg); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(schemeOrPath, schemeOrPath | notSimple); | 
| +  setChars(b, pchar, schemeOrPath); | 
| +  setChars(b, ':', authOrPath | schemeEnd); | 
| +  setChars(b, "/", pathSeg); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(authOrPath, path | notSimple); | 
| +  setChars(b, pchar, path | pathStart); | 
| +  setChars(b, "/", authOrPathSlash | pathStart); | 
| +  setChars(b, ".", pathSegDot | pathStart); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(authOrPathSlash, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, "/", uinfoOrHost0 | hostStart); | 
| +  setChars(b, ".", pathSegDot); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(uinfoOrHost0, uinfoOrHost | notSimple); | 
| +  setChars(b, pchar, uinfoOrHost); | 
| +  setRange(b, "AZ", uinfoOrHost | notSimple); | 
| +  setChars(b, ":", uinfoOrPort0 | portStart); | 
| +  setChars(b, "@", uinfoOrHost0 | hostStart); | 
| +  setChars(b, "[", ipv6Host | notSimple); | 
| +  setChars(b, "/", pathSeg | pathStart); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(uinfoOrHost, uinfoOrHost | notSimple); | 
| +  setChars(b, pchar, uinfoOrHost); | 
| +  setRange(b, "AZ", uinfoOrHost | notSimple); | 
| +  setChars(b, ":", uinfoOrPort0 | portStart); | 
| +  setChars(b, "@", uinfoOrHost0 | hostStart); | 
| +  setChars(b, "/", pathSeg | pathStart); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(uinfoOrPort0, uinfoOrPort | notSimple); | 
| +  setRange(b, "19", uinfoOrPort); | 
| +  setChars(b, "@", uinfoOrHost0 | hostStart); | 
| +  setChars(b, "/", pathSeg | pathStart); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(uinfoOrPort, uinfoOrPort | notSimple); | 
| +  setRange(b, "09", uinfoOrPort); | 
| +  setChars(b, "@", uinfoOrHost0 | hostStart); | 
| +  setChars(b, "/", pathSeg | pathStart); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(ipv6Host, ipv6Host); | 
| +  setChars(b, "]", uinfoOrHost); | 
| + | 
| +  b = build(relPathSeg, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, ".", relPathSegDot); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(relPathSegDot, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, ".", relPathSegDot2); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(relPathSegDot2, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, "/", relPathSeg); | 
| +  setChars(b, "?", query | queryStart);  // This should be non-simple. | 
| +  setChars(b, "#", fragment | fragmentStart);  // This should be non-simple. | 
| + | 
| +  b = build(pathSeg, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, ".", pathSegDot); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(pathSegDot, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, ".", pathSegDot2); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(pathSegDot2, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, "/", pathSeg | notSimple); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(path, path | notSimple); | 
| +  setChars(b, pchar, path); | 
| +  setChars(b, "/", pathSeg); | 
| +  setChars(b, "?", query | queryStart); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(query, query | notSimple); | 
| +  setChars(b, pchar, query); | 
| +  setChars(b, "?", query); | 
| +  setChars(b, "#", fragment | fragmentStart); | 
| + | 
| +  b = build(fragment, fragment | notSimple); | 
| +  setChars(b, pchar, fragment); | 
| +  setChars(b, "?", fragment); | 
| + | 
| +  // A separate two-state validator for lower-case scheme names. | 
| +  // Any non-scheme character or upper-case letter is marked as non-simple. | 
| +  b = build(scheme0, scheme | notSimple); | 
| +  setRange(b, "az", scheme); | 
| + | 
| +  b = build(scheme, scheme | notSimple); | 
| +  setRange(b, "az", scheme); | 
| +  setRange(b, "09", scheme); | 
| +  setChars(b, "+-.", scheme); | 
| + | 
| +  return tables; | 
| +} | 
| + | 
| +// -------------------------------------------------------------------- | 
| +// Code that uses the URI scanner table. | 
| + | 
| +/// Scan a string using the [_scannerTables] state machine. | 
| +/// | 
| +/// Scans [uri] from [start] to [end], startig in state [state] and | 
| +/// writing output into [indices]. | 
| +/// | 
| +/// Returns the final state. | 
| +int _scan(String uri, int start, int end, int state, List<int> indices) { | 
| +  var tables = _scannerTables; | 
| +  assert(end <= uri.length); | 
| +  for (int i = start; i < end; i++) { | 
| +    var table = tables[state]; | 
| +    // Xor with 0x60 to move range 0x20-0x7f into 0x00-0x5f | 
| +    int char = uri.codeUnitAt(i) ^ 0x60; | 
| +    // Use 0x1f (nee 0x7f) to represent all unhandled characters. | 
| +    if (char > 0x5f) char = 0x1f; | 
| +    int transition = table[char]; | 
| +    state = transition & 0x1f; | 
| +    indices[transition >> 5] = i; | 
| +  } | 
| +  return state; | 
| +} | 
| + | 
| +class _SimpleUri implements Uri { | 
| +  final String _uri; | 
| +  final int _schemeEnd; | 
| +  final int _hostStart; | 
| +  final int _portStart; | 
| +  final int _pathStart; | 
| +  final int _queryStart; | 
| +  final int _fragmentStart; | 
| +  /// The scheme is often used to distinguish URIs. | 
| +  /// To make comparisons more efficient, we cache the value, and | 
| +  /// canonicalize a few known types. | 
| +  String _schemeCache; | 
| +  int _hashCodeCache; | 
| + | 
| +  _SimpleUri( | 
| +      this._uri, | 
| +      this._schemeEnd, | 
| +      this._hostStart, | 
| +      this._portStart, | 
| +      this._pathStart, | 
| +      this._queryStart, | 
| +      this._fragmentStart, | 
| +      this._schemeCache); | 
| + | 
| +  bool get hasScheme => _schemeEnd > 0; | 
| +  bool get hasAuthority => _hostStart > 0; | 
| +  bool get hasUserInfo => _hostStart > _schemeEnd + 4; | 
| +  bool get hasPort => _hostStart > 0 && _portStart + 1 < _pathStart; | 
| +  bool get hasQuery => _queryStart < _fragmentStart; | 
| +  bool get hasFragment => _fragmentStart < _uri.length; | 
| + | 
| +  bool get _isFile => _schemeEnd == 4 && _uri.startsWith("file"); | 
| +  bool get _isHttp => _schemeEnd == 4 && _uri.startsWith("http"); | 
| +  bool get _isHttps => _schemeEnd == 5 && _uri.startsWith("https"); | 
| +  bool get _isPackage => _schemeEnd == 7 && _uri.startsWith("package"); | 
| +  bool _isScheme(String scheme) => | 
| +    _schemeEnd == scheme.length && _uri.startsWith(scheme); | 
| + | 
| +  bool get hasAbsolutePath => _uri.startsWith("/", _pathStart); | 
| +  bool get hasEmptyPath => _pathStart == _queryStart; | 
| + | 
| +  bool get isAbsolute => hasScheme && !hasFragment; | 
| + | 
| +  String get scheme { | 
| +    if (_schemeEnd <= 0) return ""; | 
| +    if (_schemeCache != null) return _schemeCache; | 
| +    if (_isHttp) { | 
| +      _schemeCache = "http"; | 
| +    } else if (_isHttps) { | 
| +      _schemeCache = "https"; | 
| +    } else if (_isFile) { | 
| +      _schemeCache = "file"; | 
| +    } else if (_isPackage) { | 
| +      _schemeCache = "package"; | 
| +    } else { | 
| +      _schemeCache = _uri.substring(0, _schemeEnd); | 
| +    } | 
| +    return _schemeCache; | 
| +  } | 
| +  String get authority => _hostStart > 0 ? | 
| +      _uri.substring(_schemeEnd + 3, _pathStart) : ""; | 
| +  String get userInfo => (_hostStart > _schemeEnd + 3) ? | 
| +      _uri.substring(_schemeEnd + 3, _hostStart - 1) : ""; | 
| +  String get host => | 
| +      _hostStart > 0 ? _uri.substring(_hostStart, _portStart) : ""; | 
| +  int get port { | 
| +    if (hasPort) return int.parse(_uri.substring(_portStart + 1, _pathStart)); | 
| +    if (_isHttp) return 80; | 
| +    if (_isHttps) return 443; | 
| +    return 0; | 
| +  } | 
| +  String get path =>_uri.substring(_pathStart, _queryStart); | 
| +  String get query => (_queryStart < _fragmentStart) ? | 
| +     _uri.substring(_queryStart + 1, _fragmentStart) : ""; | 
| +  String get fragment => (_fragmentStart < _uri.length) ? | 
| +     _uri.substring(_fragmentStart + 1) : ""; | 
| + | 
| +  String get origin { | 
| +    // Check original behavior - W3C spec is wonky! | 
| +    bool isHttp = _isHttp; | 
| +    if (_schemeEnd < 0 || _hostStart == _portStart) { | 
| +      throw new StateError("Cannot use origin without a scheme: $this"); | 
| +    } | 
| +    if (!isHttp && !_isHttps) { | 
| +      throw new StateError( | 
| +        "Origin is only applicable schemes http and https: $this"); | 
| +    } | 
| +    if (_hostStart == _schemeEnd + 3) { | 
| +      return _uri.substring(0, _pathStart); | 
| +    } | 
| +    // Need to drop anon-empty userInfo. | 
| +    return _uri.substring(0, _schemeEnd + 3) + | 
| +           _uri.substring(_hostStart, _pathStart); | 
| +  } | 
| + | 
| +  List<String> get pathSegments { | 
| +    int start = _pathStart; | 
| +    int end = _queryStart; | 
| +    if (_uri.startsWith("/", start)) start++; | 
| +    if (start == end) return const <String>[]; | 
| +    List<String> parts = []; | 
| +    for (int i = start; i < end; i++) { | 
| +      var char = _uri.codeUnitAt(i); | 
| +      if (char == _SLASH) { | 
| +        parts.add(_uri.substring(start, i)); | 
| +        start = i + 1; | 
| +      } | 
| +    } | 
| +    parts.add(_uri.substring(start, end)); | 
| +    return new List<String>.unmodifiable(parts); | 
| +  } | 
| + | 
| +  Map<String, String> get queryParameters { | 
| +    if (!hasQuery) return const <String, String>{}; | 
| +    return new UnmodifiableMapView<String, String>( | 
| +        Uri.splitQueryString(query)); | 
| +  } | 
| + | 
| +  Map<String, List<String>> get queryParametersAll { | 
| +    if (!hasQuery) return const <String, List<String>>{}; | 
| +    Map queryParameterLists = _Uri._splitQueryStringAll(query); | 
| +    for (var key in queryParameterLists.keys) { | 
| +      queryParameterLists[key] = | 
| +          new List<String>.unmodifiable(queryParameterLists[key]); | 
| +    } | 
| +    return new Map<String, List<String>>.unmodifiable(queryParameterLists); | 
| +  } | 
| + | 
| +  bool _isPort(String port) { | 
| +    int portDigitStart = _portStart + 1; | 
| +    return portDigitStart + port.length == _pathStart && | 
| +           _uri.startsWith(port, portDigitStart); | 
| +  } | 
| + | 
| +  Uri normalizePath() => this; | 
| + | 
| +  Uri removeFragment() { | 
| +    if (!hasFragment) return this; | 
| +    return new _SimpleUri( | 
| +      _uri.substring(0, _fragmentStart), | 
| +      _schemeEnd, _hostStart, _portStart, | 
| +      _pathStart, _queryStart, _fragmentStart, _schemeCache); | 
| +  } | 
| + | 
| +  Uri replace({String scheme, | 
| +               String userInfo, | 
| +               String host, | 
| +               int port, | 
| +               String path, | 
| +               Iterable<String> pathSegments, | 
| +               String query, | 
| +               Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 
| +               String fragment}) { | 
| +    bool schemeChanged = false; | 
| +    if (scheme != null) { | 
| +      scheme = _Uri._makeScheme(scheme, 0, scheme.length); | 
| +      schemeChanged = !_isScheme(scheme); | 
| +    } else { | 
| +      scheme = this.scheme; | 
| +    } | 
| +    bool isFile = (scheme == "file"); | 
| +    if (userInfo != null) { | 
| +      userInfo = _Uri._makeUserInfo(userInfo, 0, userInfo.length); | 
| +    } else if (_hostStart > 0) { | 
| +      userInfo = _uri.substring(_schemeEnd + 3, _hostStart); | 
| +    } else { | 
| +      userInfo = ""; | 
| +    } | 
| +    if (port != null) { | 
| +      port = _Uri._makePort(port, scheme); | 
| +    } else { | 
| +      port = this.hasPort ? this.port : null; | 
| +      if (schemeChanged) { | 
| +        // The default port might have changed. | 
| +        port = _Uri._makePort(port, scheme); | 
| +      } | 
| +    } | 
| +    if (host != null) { | 
| +      host = _Uri._makeHost(host, 0, host.length, false); | 
| +    } else if (_hostStart > 0) { | 
| +      host = _uri.substring(_hostStart, _portStart); | 
| +    } else if (userInfo.isNotEmpty || port != null || isFile) { | 
| +      host = ""; | 
| +    } | 
| + | 
| +    bool hasAuthority = host != null; | 
| +    if (path != null || pathSegments != null) { | 
| +      path = _Uri._makePath(path, 0, _stringOrNullLength(path), pathSegments, | 
| +                           scheme, hasAuthority); | 
| +    } else { | 
| +      path = _uri.substring(_pathStart, _queryStart); | 
| +      if ((isFile || (hasAuthority && !path.isEmpty)) && | 
| +          !path.startsWith('/')) { | 
| +        path = "/" + path; | 
| +      } | 
| +    } | 
| + | 
| +    if (query != null || queryParameters != null) { | 
| +      query = _Uri._makeQuery( | 
| +          query, 0, _stringOrNullLength(query), queryParameters); | 
| +    } else if (_queryStart < _fragmentStart) { | 
| +      query = _uri.substring(_queryStart + 1, _fragmentStart); | 
| +    } | 
| + | 
| +    if (fragment != null) { | 
| +      fragment = _Uri._makeFragment(fragment, 0, fragment.length); | 
| +    } else if (_fragmentStart < _uri.length) { | 
| +      fragment = _uri.substring(_fragmentStart + 1); | 
| +    } | 
| + | 
| +    return new _Uri._internal( | 
| +        scheme, userInfo, host, port, path, query, fragment); | 
| +  } | 
| + | 
| +  Uri resolve(String reference) { | 
| +    return resolveUri(Uri.parse(reference)); | 
| +  } | 
| + | 
| +  Uri resolveUri(Uri reference) { | 
| +    if (reference is _SimpleUri) { | 
| +      return _simpleMerge(this, reference); | 
| +    } | 
| +    return _toNonSimple().resolveUri(reference); | 
| +  } | 
| + | 
| +  // Merge two simple URIs. This should always result in a prefix of | 
| +  // one concatentated with a suffix of the other, possibly with a `/` in | 
| +  // the middle of two merged paths, which is again simple. | 
| +  // In a few cases, there might be a need for extra normalization, when | 
| +  // resolving on top of a known scheme. | 
| +  Uri _simpleMerge(_SimpleUri base, _SimpleUri ref) { | 
| +    if (ref.hasScheme) return ref; | 
| +    if (ref.hasAuthority) { | 
| +      if (!base.hasScheme) return ref; | 
| +      bool isSimple = true; | 
| +      if (base._isFile) { | 
| +        isSimple = !ref.hasEmptyPath; | 
| +      } else if (base._isHttp) { | 
| +        isSimple = !ref._isPort("80"); | 
| +      } else if (base._isHttps) { | 
| +        isSimple = !ref._isPort("443"); | 
| +      } | 
| +      if (isSimple) { | 
| +        var delta = base._schemeEnd + 1; | 
| +        var newUri = base._uri.substring(0, base._schemeEnd + 1) + | 
| +                     ref._uri.substring(ref._schemeEnd + 1); | 
| +        return new _SimpleUri(newUri, | 
| +           base._schemeEnd, | 
| +           ref._hostStart + delta, | 
| +           ref._portStart + delta, | 
| +           ref._pathStart + delta, | 
| +           ref._queryStart + delta, | 
| +           ref._fragmentStart + delta, | 
| +           base._schemeCache); | 
| +      } else { | 
| +        // This will require normalization, so use the _Uri implementation. | 
| +        return _toNonSimple().resolveUri(ref); | 
| +      } | 
| +    } | 
| +    if (ref.hasEmptyPath) { | 
| +      if (ref.hasQuery) { | 
| +        int delta = base._queryStart - ref._queryStart; | 
| +        var newUri = base._uri.substring(0, base._queryStart) + | 
| +                     ref._uri.substring(ref._queryStart); | 
| +        return new _SimpleUri(newUri, | 
| +           base._schemeEnd, | 
| +           base._hostStart, | 
| +           base._portStart, | 
| +           base._pathStart, | 
| +           ref._queryStart + delta, | 
| +           ref._fragmentStart + delta, | 
| +           base._schemeCache); | 
| +      } | 
| +      if (ref.hasFragment) { | 
| +        int delta = base._fragmentStart - ref._fragmentStart; | 
| +        var newUri = base._uri.substring(0, base._fragmentStart) + | 
| +                     ref._uri.substring(ref._fragmentStart); | 
| +        return new _SimpleUri(newUri, | 
| +           base._schemeEnd, | 
| +           base._hostStart, | 
| +           base._portStart, | 
| +           base._pathStart, | 
| +           base._queryStart, | 
| +           ref._fragmentStart + delta, | 
| +           base._schemeCache); | 
| +      } | 
| +      return base.removeFragment(); | 
| +    } | 
| +    if (ref.hasAbsolutePath) { | 
| +      var delta = base._pathStart - ref._pathStart; | 
| +      var newUri = base._uri.substring(0, base._pathStart) + | 
| +                   ref._uri.substring(ref._pathStart); | 
| +      return new _SimpleUri(newUri, | 
| +        base._schemeEnd, | 
| +        base._hostStart, | 
| +        base._portStart, | 
| +        base._pathStart, | 
| +        ref._queryStart + delta, | 
| +        ref._fragmentStart + delta, | 
| +        base._schemeCache); | 
| +    } | 
| +    if (base.hasEmptyPath && base.hasAuthority) { | 
| +      // ref has relative non-empty path. | 
| +      // Add a "/" in front, then leading "/../" segments are folded to "/". | 
| +      int refStart = ref._pathStart; | 
| +      while (ref._uri.startsWith("../", refStart)) { | 
| +        refStart += 3; | 
| +      } | 
| +      var delta = base._pathStart - refStart + 1; | 
| +      var newUri = "${base._uri.substring(0, base._pathStart)}/" | 
| +                   "${ref._uri.substring(refStart)}"; | 
| +      return new _SimpleUri(newUri, | 
| +        base._schemeEnd, | 
| +        base._hostStart, | 
| +        base._portStart, | 
| +        base._pathStart, | 
| +        ref._queryStart + delta, | 
| +        ref._fragmentStart + delta, | 
| +        base._schemeCache); | 
| +    } | 
| +    // Merge paths. | 
| +    if (base._uri.startsWith("../", base._pathStart)) { | 
| +      // Complex rare case, go slow. | 
| +      return _toNonSimple().resolveUri(ref); | 
| +    } | 
| + | 
| +    // The RFC 3986 algorithm merges the base path without its final segment | 
| +    // (anything after the final "/", or everything if the base path doesn't | 
| +    // contain any "/"), and the reference path. | 
| +    // Then it removes "." and ".." segments using the remove-dot-segment | 
| +    // algorithm. | 
| +    // This code combines the two steps. It is simplified by knowing that | 
| +    // the base path contains no "." or ".." segments, and the reference | 
| +    // path can only contain leading ".." segments. | 
| + | 
| +    String baseUri = base._uri; | 
| +    String refUri = ref._uri; | 
| +    int baseStart = base._pathStart; | 
| +    int baseEnd = base._queryStart; | 
| +    int refStart = ref._pathStart; | 
| +    int refEnd = ref._queryStart; | 
| +    int backCount = 1; | 
| + | 
| +    int slashCount = 0; | 
| + | 
| +    // Count leading ".." segments in reference path. | 
| +    while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) { | 
| +      refStart += 3; | 
| +      backCount += 1; | 
| +    } | 
| + | 
| +    // Extra slash inserted between base and reference path parts if | 
| +    // the base path contains any slashes. | 
| +    // (We could use a slash from the base path in most cases, but not if | 
| +    // we remove the entire base path). | 
| +    String insert = ""; | 
| +    while (baseEnd > baseStart) { | 
| +      baseEnd--; | 
| +      int char = baseUri.codeUnitAt(baseEnd); | 
| +      if (char == _SLASH) { | 
| +        insert = "/"; | 
| +        backCount--; | 
| +        if (backCount == 0) break; | 
| +      } | 
| +    } | 
| +    // If the base URI has no scheme or authority (`_pathStart == 0`) | 
| +    // and a relative path, and we reached the beginning of the path, | 
| +    // we have a special case. | 
| +    if (baseEnd == 0 && !base.hasAbsolutePath) { | 
| +      // Non-RFC 3986 behavior when resolving a purely relative path on top of | 
| +      // another relative path: Don't make the result absolute. | 
| +      insert = ""; | 
| +    } | 
| + | 
| +    var delta = baseEnd - refStart + insert.length; | 
| +    var newUri = "${base._uri.substring(0, baseEnd)}$insert" | 
| +               "${ref._uri.substring(refStart)}"; | 
| + | 
| +    return new _SimpleUri(newUri, | 
| +      base._schemeEnd, | 
| +      base._hostStart, | 
| +      base._portStart, | 
| +      base._pathStart, | 
| +      ref._queryStart + delta, | 
| +      ref._fragmentStart + delta, | 
| +      base._schemeCache); | 
| +  } | 
| + | 
| +  String toFilePath({bool windows}) { | 
| +    if (_schemeEnd >= 0 && !_isFile) { | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a file path from a $scheme URI"); | 
| +    } | 
| +    if (_queryStart < _uri.length) { | 
| +      if (_queryStart < _fragmentStart) { | 
| +        throw new UnsupportedError( | 
| +            "Cannot extract a file path from a URI with a query component"); | 
| +      } | 
| +      throw new UnsupportedError( | 
| +          "Cannot extract a file path from a URI with a fragment component"); | 
| +    } | 
| +    if (windows == null) windows = _Uri._isWindows; | 
| +    return windows ? _Uri._toWindowsFilePath(this) : _toFilePath(); | 
| +  } | 
| + | 
| +  String _toFilePath() { | 
| +    if (_hostStart < _portStart) { | 
| +      // Has authority and non-empty host. | 
| +      throw new UnsupportedError( | 
| +        "Cannot extract a non-Windows file path from a file URI " | 
| +        "with an authority"); | 
| +    } | 
| +    return this.path; | 
| +  } | 
| + | 
| +  UriData get data { | 
| +    assert(scheme != "data"); | 
| +    return null; | 
| +  } | 
| + | 
| +  int get hashCode => _hashCodeCache ??= _uri.hashCode; | 
| + | 
| +  bool operator==(Object other) { | 
| +    if (identical(this, other)) return true; | 
| +    if (other is Uri) return _uri == other.toString(); | 
| +    return false; | 
| +  } | 
| + | 
| +  Uri _toNonSimple() { | 
| +    return new _Uri._internal( | 
| +      this.scheme, | 
| +      this.userInfo, | 
| +      this.hasAuthority ? this.host: null, | 
| +      this.hasPort ? this.port : null, | 
| +      this.path, | 
| +      this.hasQuery ? this.query : null, | 
| +      this.hasFragment ? this.fragment : null | 
| +    ); | 
| +  } | 
| + | 
| +  String toString() => _uri; | 
| +} | 
| + | 
| +/// Checks whether [text] starts with "data:" at position [start]. | 
| +/// | 
| +/// The text must be long enough to allow reading five characters | 
| +/// from the [start] position. | 
| +/// | 
| +/// Returns an integer value which is zero if text starts with all-lowercase | 
| +/// "data:" and 0x20 if the text starts with "data:" that isn't all lower-case. | 
| +/// All other values means the text starts with some other character. | 
| +int _startsWithData(String text, int start) { | 
| +  // Multiply by 3 to avoid a non-colon character making delta be 0x20. | 
| +  int delta = (text.codeUnitAt(start + 4) ^ _COLON) * 3; | 
| +  delta |= text.codeUnitAt(start)     ^ 0x64 /*d*/; | 
| +  delta |= text.codeUnitAt(start + 1) ^ 0x61 /*a*/; | 
| +  delta |= text.codeUnitAt(start + 2) ^ 0x74 /*t*/; | 
| +  delta |= text.codeUnitAt(start + 3) ^ 0x61 /*a*/; | 
| +  return delta; | 
| } | 
| + | 
| +/// Helper function returning the length of a string, or `0` for `null`. | 
| +int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | 
|  |