content: // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
  // for details. All rights reserved. Use of this source code is governed by a
  // BSD-style license that can be found in the LICENSE file.
  
  part of dart.core;
  
  /**
   * A parsed URI, such as a URL.
   *
   * **See also:**
   *
   * * [URIs][uris] in the [library tour][libtour]
   * * [RFC-3986](http://tools.ietf.org/html/rfc3986)
   *
   * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#ch03-uri
   * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html
   */
  class Uri {
    final String _host;
    int _port;
    String _path;
  
    /**
     * Returns the scheme component.
     *
     * Returns the empty string if there is no scheme component.
     */
    final String scheme;
  
    /**
     * 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 {
      if (!hasAuthority) return "";
      var sb = new StringBuffer();
      _writeAuthority(sb);
      return sb.toString();
    }
  
    /**
     * Returns the user info part of the authority component.
     *
     * Returns the empty string if there is no user info in the
     * authority component.
     */
    final String userInfo;
  
    /**
     * 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.
     */
    String get host {
      if (_host != null && _host.startsWith('[')) {
        return _host.substring(1, _host.length - 1);
      }
      return _host;
    }
  
    /**
     * Returns the port part of the authority component.
     *
     * Returns 0 if there is no port in the authority component.
     */
    int get port {
      if (_port == 0) {
        if (scheme == "http") return 80;
        if (scheme == "https") return 443;
      }
      return _port;
    }
  
    /**
     * 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 => _path;
  
    /**
     * 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.
     */
    final String query;
  
    /**
     * Returns the fragment identifier component.
     *
     * Returns the empty string if there is no fragment identifier
     * component.
     */
    final String fragment;
  
    /**
     * Cache the computed return value of [pathSegements].
     */
    List<String> _pathSegments;
  
    /**
     * Cache the computed return value of [queryParameters].
     */
    Map<String, String> _queryParameters;
  
    /**
     * Creates a new URI object by parsing a URI string.
     */
    static Uri parse(String uri) => new Uri._fromMatch(_splitRe.firstMatch(uri));
  
    Uri._fromMatch(Match m) :
      this(scheme: _makeScheme(_emptyIfNull(m[_COMPONENT_SCHEME])),
           userInfo: _emptyIfNull(m[_COMPONENT_USER_INFO]),
           host: _eitherOf(
           m[_COMPONENT_HOST], m[_COMPONENT_HOST_IPV6]),
           port: _parseIntOrZero(m[_COMPONENT_PORT]),
           path: _emptyIfNull(m[_COMPONENT_PATH]),
           query: _emptyIfNull(m[_COMPONENT_QUERY_DATA]),
           fragment: _emptyIfNull(m[_COMPONENT_FRAGMENT]));
  
    /**
     * Creates a new URI from its components.
     *
     * Each component is set through a named argument. Any number of
     * components can be provided. The default value for the components
     * not provided is the empry string, except for [port] which has a
     * default value of 0. The [path] and [query] components can be set
     * using two different named arguments.
     *
     * The scheme component is set through [scheme]. The scheme is
     * normalized to all lowercase letters.
     *
     * The user info part of the authority component is set through
     * [userInfo].
     *
     * The host part of the authority component is set through
     * [host]. The host can either be a hostname, an IPv4 address or an
     * IPv6 address, contained in '[' and ']'. If the host contains a
     * ':' character, the '[' and ']' are added if not already provided.
     *
     * The port part of the authority component is set through
     * [port]. The port is normalized for scheme http and https where
     * port 80 and port 443 respectively is set.
     *
     * The path component is set through either [path] or
     * [pathSegments]. When [path] is used, the provided string is
     * expected to be fully percent-encoded, and is used in its literal
     * form. When [pathSegments] is used, each of the provided segments
     * is percent-encoded and joined using the forward slash
     * separator. The percent-encoding of the path segments encodes all
     * characters except for the unreserved characters and the following
     * list of characters: `!$&'()*+,;=:@`. If the other components
     * calls for an absolute path a leading slash `/` is prepended if
     * not already there.
     *
     * The query component is set through either [query] or
     * [queryParameters]. When [query] is used the provided string is
     * expected to be fully percent-encoded and is used in its literal
     * form. When [queryParameters] is used the query is built from the
     * provided map. Each key and value in the map is percent-encoded
     * and joined using equal and ampersand characters. The
     * percent-encoding of the keys and values encodes all characters
     * except for the unreserved characters.
     *
     * The fragment component is set through [fragment].
     */
    Uri({String scheme,
         this.userInfo: "",
         String host: "",
         port: 0,
         String path,
         Iterable<String> pathSegments,
         String query,
         Map<String, String> queryParameters,
         fragment: ""}) :
        scheme = _makeScheme(scheme),
        _host = _makeHost(host),
        query = _makeQuery(query, queryParameters),
        fragment = _makeFragment(fragment) {
      // Perform scheme specific normalization.
      if (scheme == "http" && port == 80) {
        _port = 0;
      } else if (scheme == "https" && port == 443) {
        _port = 0;
      } else {
        _port = port;
      }
      // Fill the path.
      _path = _makePath(path, pathSegments);
    }
  
    /**
     * Creates a new `http` URI from authority, path and query.
     *
     * Examples:
     *
     *     // Create the URI http://example.org/path?q=abc.
     *     new Uri.http("google.com", "/search", { "q" : "dart" });http://example.org/path?q=abc.
     *     new Uri.http("user:pass@localhost:8080, "");  // http://user:pass@localhost:8080/
     *     new Uri.http("example.org, "a b");  // http://example.org/a%20b
     *     new Uri.http("example.org, "/a%2F");  // http://example.org/a%25%2F
     *
     * The `scheme` is always set to `http`.
     *
     * The `userInfo`, `host` and `port` components are set from the
     * [authority] argument.
     *
     * The `path` component is set from the [unencodedPath]
     * argument. The path passed must not be encoded as this constructor
     * encodes the path.
     *
     * The `query` component is set from the optional [queryParameters]
     * argument.
     */
    factory Uri.http(String authority,
                     String unencodedPath,
                     [Map<String, String> queryParameters]) {
      return _makeHttpUri("http", authority, unencodedPath, queryParameters);
    }
  
    /**
     * Creates a new `https` URI from authority, path and query.
     *
     * This constructor is the same as [Uri.http] except for the scheme
     * which is set to `https`.
     */
    factory Uri.https(String authority,
                      String unencodedPath,
                      [Map<String, String> queryParameters]) {
      return _makeHttpUri("https", authority, unencodedPath, queryParameters);
    }
  
    static Uri _makeHttpUri(String scheme,
                            String authority,
                            String unencodedPath,
                            Map<String, String> queryParameters) {
      var userInfo = "";
      var host = "";
      var port = 0;
  
      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.");
        }
        parseIPv6Address(authority.substring(hostStart + 1, hostEnd));
        hostEnd++;  // Skip the closing bracket.
        if (hostEnd != authority.length &&
            authority.codeUnitAt(hostEnd) != _COLON) {
          throw new FormatException("Invalid end of authority");
        }
      }
      // 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 (resulting URI in comment):
     *
     *     new Uri.file("xxx/yyy");  // xxx/yyy
     *     new Uri.file("xxx/yyy/");  // xxx/yyy/
     *     new Uri.file("/xxx/yyy");  // file:///xxx/yyy
     *     new Uri.file("/xxx/yyy/");  // file:///xxx/yyy/
     *     new Uri.file("C:");  // C:
     *
     * Examples using Windows semantics (resulting URI in comment):
     *
     *     new Uri.file(r"xxx\yyy");  // xxx/yyy
     *     new Uri.file(r"xxx\yyy\");  // xxx/yyy/
     *     new Uri.file(r"\xxx\yyy");  // file:///xxx/yyy
     *     new Uri.file(r"\xxx\yyy/");  // file:///xxx/yyy/
     *     new Uri.file(r"C:\xxx\yyy");  // file:///C:/xxx/yyy
     *     new Uri.file(r"C:xxx\yyy");  // Throws as path with drive letter
     *                                  // is not absolute.
     *     new Uri.file(r"\\server\share\file");  // file://server/share/file
     *     new Uri.file(r"C:");  // Throws as path with drive letter
     *                           // is not absolute.
     *
     * 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) : _makeFileUri(path);
    }
  
    /**
     * Returns the natural base URI for the current platform.
     *
     * When running in a browser this is the current URL (from
     * `window.location.href`).
     *
     * When not running in a browser this is the file URI referencing
     * the current working directory.
     */
    external static Uri get base;
  
    external static bool get _isWindows;
  
    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");
          }
        }
      });
    }
  
    static _checkWindowsPathReservedCharacters(List<String> segments,
                                               bool argumentError,
                                               [int firstSegment = 0]) {
      segments.skip(firstSegment).forEach((segment) {
        if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) {
          if (argumentError) {
            throw new ArgumentError("Illegal character in path}");
          } else {
            throw new UnsupportedError("Illegal character in path}");
          }
        }
      });
    }
  
    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));
      }
    }
  
    static _makeFileUri(String path) {
      String sep = "/";
      if (path.length > 0 && path[0] == sep) {
        // Absolute file:// URI.
        return new Uri(scheme: "file", pathSegments: path.split(sep));
      } else {
        // Relative URI.
        return new Uri(pathSegments: path.split(sep));
      }
    }
  
    static _makeWindowsFileUrl(String path) {
      if (path.startsWith("\\\\?\\")) {
        if (path.startsWith("\\\\?\\UNC\\")) {
          path = "\\${path.substring(7)}";
        } else {
          path = path.substring(4);
          if (path.length < 3 ||
              path.codeUnitAt(1) != _COLON ||
              path.codeUnitAt(2) != _BACKSLASH) {
            throw new ArgumentError(
                "Windows paths with \\\\?\\ prefix must be absolute");
          }
        }
      } else {
        path = path.replaceAll("/", "\\");
      }
      String sep = "\\";
      if (path.length > 1 && path[1] == ":") {
        _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);
        _checkWindowsPathReservedCharacters(pathSegments, true, 1);
        return new Uri(scheme: "file", pathSegments: pathSegments);
      }
  
      if (path.length > 0 && path[0] == sep) {
        if (path.length > 1 && path[1] == sep) {
          // Absolute file:// URI with host.
          int pathStart = path.indexOf("\\", 2);
          String hostPart =
              pathStart == -1 ? path.substring(2) : path.substring(2, pathStart);
          String pathPart =
              pathStart == -1 ? "" : path.substring(pathStart + 1);
          var pathSegments = pathPart.split(sep);
          _checkWindowsPathReservedCharacters(pathSegments, true);
          return new Uri(
              scheme: "file", host: hostPart, pathSegments: pathSegments);
        } else {
          // Absolute file:// URI.
          var pathSegments = path.split(sep);
          _checkWindowsPathReservedCharacters(pathSegments, true);
          return new Uri(scheme: "file", pathSegments: pathSegments);
        }
      } else {
        // Relative URI.
        var pathSegments = path.split(sep);
        _checkWindowsPathReservedCharacters(pathSegments, true);
        return new Uri(pathSegments: pathSegments);
      }
    }
  
    /**
     * 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 {
      if (_pathSegments == null) {
        var pathToSplit = !path.isEmpty && path.codeUnitAt(0) == _SLASH
                          ? path.substring(1)
                          : path;
        _pathSegments = new UnmodifiableListView(
          pathToSplit == "" ? const<String>[]
                            : pathToSplit.split("/")
                                         .map(Uri.decodeComponent)
                                         .toList(growable: false));
      }
      return _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.
     *
     * The returned map is unmodifiable and will throw [UnsupportedError] on any
     * calls that would mutate it.
     */
    Map<String, String> get queryParameters {
      if (_queryParameters == null) {
        _queryParameters = new _UnmodifiableMap(splitQueryString(query));
      }
      return _queryParameters;
    }
  
    static String _makeHost(String host) {
      if (host == null || host.isEmpty) return host;
      if (host.codeUnitAt(0) == _LEFT_BRACKET) {
        if (host.codeUnitAt(host.length - 1) != _RIGHT_BRACKET) {
          throw new FormatException('Missing end `]` to match `[` in host');
        }
        parseIPv6Address(host.substring(1, host.length - 1));
        return host;
      }
      for (int i = 0; i < host.length; i++) {
        if (host.codeUnitAt(i) == _COLON) {
          parseIPv6Address(host);
          return '[$host]';
        }
      }
      return host;
    }
  
    static String _makeScheme(String scheme) {
      bool isSchemeLowerCharacter(int ch) {
        return ch < 128 &&
               ((_schemeLowerTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
      }
  
      bool isSchemeCharacter(int ch) {
        return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
      }
  
      if (scheme == null) return "";
      bool allLowercase = true;
      int length = scheme.length;
      for (int i = 0; i < length; i++) {
        int codeUnit = scheme.codeUnitAt(i);
        if (i == 0 && !_isAlphabeticCharacter(codeUnit)) {
          // First code unit must be an alphabetic character.
          throw new ArgumentError('Illegal scheme: $scheme');
        }
        if (!isSchemeLowerCharacter(codeUnit)) {
          if (isSchemeCharacter(codeUnit)) {
            allLowercase = false;
          } else {
            throw new ArgumentError('Illegal scheme: $scheme');
          }
        }
      }
  
      return allLowercase ? scheme : scheme.toLowerCase();
    }
  
    String _makePath(String path, Iterable<String> pathSegments) {
      if (path == null && pathSegments == null) return "";
      if (path != null && pathSegments != null) {
        throw new ArgumentError('Both path and pathSegments specified');
      }
      var result;
      if (path != null) {
        result = _normalize(path);
      } else {
        result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/");
      }
      if ((hasAuthority || (scheme == "file")) &&
          result.isNotEmpty && !result.startsWith("/")) {
        return "/$result";
      }
      return result;
    }
  
    static String _makeQuery(String query, Map<String, String> queryParameters) {
      if (query == null && queryParameters == null) return "";
      if (query != null && queryParameters != null) {
        throw new ArgumentError('Both query and queryParameters specified');
      }
      if (query != null) return _normalize(query);
  
      var result = new StringBuffer();
      var first = true;
      queryParameters.forEach((key, value) {
        if (!first) {
          result.write("&");
        }
        first = false;
        result.write(Uri.encodeQueryComponent(key));
        if (value != null && !value.isEmpty) {
          result.write("=");
          result.write(Uri.encodeQueryComponent(value));
        }
      });
      return result.toString();
    }
  
    static String _makeFragment(String fragment) {
      if (fragment == null) return "";
      return _normalize(fragment);
    }
  
    static String _normalize(String component) {
      bool isNormalizedHexDigit(int digit) {
        return (_ZERO <= digit && digit <= _NINE) ||
            (_UPPER_CASE_A <= digit && digit <= _UPPER_CASE_F);
      }
  
      bool isLowerCaseHexDigit(int digit) {
        return _LOWER_CASE_A <= digit && digit <= _LOWER_CASE_F;
      }
  
      bool isUnreserved(int ch) {
        return ch < 128 &&
               ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
      }
  
      int normalizeHexDigit(int index) {
        var codeUnit = component.codeUnitAt(index);
        if (isLowerCaseHexDigit(codeUnit)) {
          return codeUnit - 0x20;
        } else if (!isNormalizedHexDigit(codeUnit)) {
          throw new ArgumentError("Invalid URI component: $component");
        } else {
          return codeUnit;
        }
      }
  
      int decodeHexDigitPair(int index) {
        int byte = 0;
        for (int i = 0; i < 2; i++) {
          var codeUnit = component.codeUnitAt(index + i);
          if (_ZERO <= codeUnit && codeUnit <= _NINE) {
            byte = byte * 16 + codeUnit - _ZERO;
          } else {
            // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
            codeUnit |= 0x20;
            if (_LOWER_CASE_A <= codeUnit &&
                codeUnit <= _LOWER_CASE_F) {
              byte = byte * 16 + codeUnit - _LOWER_CASE_A + 10;
            } else {
              throw new ArgumentError(
                  "Invalid percent-encoding in URI component: $component");
            }
          }
        }
        return byte;
      }
  
      // Start building the normalized component string.
      StringBuffer result;
      int length = component.length;
      int index = 0;
      int prevIndex = 0;
  
      // Copy a part of the component string to the result.
      void fillResult() {
        if (result == null) {
          assert(prevIndex == 0);
          result = new StringBuffer(component.substring(prevIndex, index));
        } else {
          result.write(component.substring(prevIndex, index));
        }
      }
  
      while (index < length) {
  
        // Normalize percent encoding to uppercase and don't encode
        // unreserved characters.
        if (component.codeUnitAt(index) == _PERCENT) {
          if (length < index + 2) {
              throw new ArgumentError(
                  "Invalid percent-encoding in URI component: $component");
          }
  
          var codeUnit1 = component.codeUnitAt(index + 1);
          var codeUnit2 = component.codeUnitAt(index + 2);
          var decodedCodeUnit = decodeHexDigitPair(index + 1);
          if (isNormalizedHexDigit(codeUnit1) &&
              isNormalizedHexDigit(codeUnit2) &&
              !isUnreserved(decodedCodeUnit)) {
            index += 3;
          } else {
            fillResult();
            if (isUnreserved(decodedCodeUnit)) {
              result.writeCharCode(decodedCodeUnit);
            } else {
              result.write("%");
              result.writeCharCode(normalizeHexDigit(index + 1));
              result.writeCharCode(normalizeHexDigit(index + 2));
            }
            index += 3;
            prevIndex = index;
          }
        } else {
          index++;
        }
      }
      if (result != null && prevIndex != index) fillResult();
      assert(index == length);
  
      if (result == null) return component;
      return result.toString();
    }
  
    static String _emptyIfNull(String val) => val != null ? val : '';
  
    static int _parseIntOrZero(String val) {
      if (val != null && val != '') {
        return int.parse(val);
      } else {
        return 0;
      }
    }
  
    static String _eitherOf(String val1, String val2) {
      if (val1 != null) return val1;
      if (val2 != null) return val2;
      return '';
    }
  
    // NOTE: This code was ported from: closure-library/closure/goog/uri/utils.js
    static final RegExp _splitRe = new RegExp(
        '^'
        '(?:'
          '([^:/?#]+)'                    // scheme - ignore special characters
                                          // used by other URL parts such as :,
                                          // ?, /, #, and .
        ':)?'
        '(?://'
          '(?:([^/?#]*)@)?'               // userInfo
          '(?:'
            r'([\w\d\-\u0100-\uffff.%]*)'
                                          // host - restrict to letters,
                                          // digits, dashes, dots, percent
                                          // escapes, and unicode characters.
            '|'
            // TODO(ajohnsen): Only allow a max number of parts?
            r'\[([A-Fa-f0-9:.]*)\])'
                                          // IPv6 host - restrict to hex,
                                          // dot and colon.
          '(?::([0-9]+))?'                // port
        ')?'
        r'([^?#[]+)?'                     // path
        r'(?:\?([^#]*))?'                 // query
        '(?:#(.*))?'                      // fragment
        r'$');
  
    static const _COMPONENT_SCHEME = 1;
    static const _COMPONENT_USER_INFO = 2;
    static const _COMPONENT_HOST = 3;
    static const _COMPONENT_HOST_IPV6 = 4;
    static const _COMPONENT_PORT = 5;
    static const _COMPONENT_PATH = 6;
    static const _COMPONENT_QUERY_DATA = 7;
    static const _COMPONENT_FRAGMENT = 8;
  
    /**
     * Returns whether the URI is absolute.
     */
    bool get isAbsolute => scheme != "" && fragment == "";
  
    String _merge(String base, String reference) {
      if (base == "") return "/$reference";
      return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference";
    }
  
    bool _hasDotSegments(String path) {
      if (path.length > 0 && path.codeUnitAt(0) == _COLON) return true;
      int index = path.indexOf("/.");
      return index != -1;
    }
  
    String _removeDotSegments(String path) {
      if (!_hasDotSegments(path)) return path;
      List<String> output = [];
      bool appendSlash = false;
      for (String segment in path.split("/")) {
        appendSlash = false;
        if (segment == "..") {
          if (!output.isEmpty &&
              ((output.length != 1) || (output[0] != ""))) output.removeLast();
          appendSlash = true;
        } else if ("." == segment) {
          appendSlash = true;
        } else {
          output.add(segment);
        }
      }
      if (appendSlash) output.add("");
      return output.join("/");
    }
  
    /**
     * 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));
    }
  
    /**
     * Resolve [reference] as an URI relative to `this`.
     *
     * Returns the resolved URI.
     *
     * The algorithm for resolving a reference is described in
     * [RFC-3986 Section 5]
     * (http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123").
     */
    Uri resolveUri(Uri reference) {
      // From RFC 3986.
      String targetScheme;
      String targetUserInfo;
      String targetHost;
      int targetPort;
      String targetPath;
      String targetQuery;
      if (reference.scheme != "") {
        targetScheme = reference.scheme;
        targetUserInfo = reference.userInfo;
        targetHost = reference.host;
        targetPort = reference.port;
        targetPath = _removeDotSegments(reference.path);
        targetQuery = reference.query;
      } else {
        if (reference.hasAuthority) {
          targetUserInfo = reference.userInfo;
          targetHost = reference.host;
          targetPort = reference.port;
          targetPath = _removeDotSegments(reference.path);
          targetQuery = reference.query;
        } else {
          if (reference.path == "") {
            targetPath = this.path;
            if (reference.query != "") {
              targetQuery = reference.query;
            } else {
              targetQuery = this.query;
            }
          } else {
            if (reference.path.startsWith("/")) {
              targetPath = _removeDotSegments(reference.path);
            } else {
              targetPath = _removeDotSegments(_merge(this.path, reference.path));
            }
            targetQuery = reference.query;
          }
          targetUserInfo = this.userInfo;
          targetHost = this.host;
          targetPort = this.port;
        }
        targetScheme = this.scheme;
      }
      return new Uri(scheme: targetScheme,
                     userInfo: targetUserInfo,
                     host: targetHost,
                     port: targetPort,
                     path: targetPath,
                     query: targetQuery,
                     fragment: reference.fragment);
    }
  
    /**
     * Returns whether the URI has an [authority] component.
     */
    bool get hasAuthority => host != "";
  
    /**
     * 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");
      }
      if (_port == 0) return "$scheme://$_host";
      return "$scheme://$_host:$_port";
    }
  
    /**
     * 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].
     */
    String toFilePath({bool windows}) {
      if (scheme != "" && scheme != "file") {
        throw new UnsupportedError(
            "Cannot extract a file path from a $scheme URI");
      }
      if (scheme != "" && scheme != "file") {
        throw new UnsupportedError(
            "Cannot extract a file path from a $scheme URI");
      }
      if (query != "") {
        throw new UnsupportedError(
            "Cannot extract a file path from a URI with a query component");
      }
      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() : _toFilePath();
    }
  
    String _toFilePath() {
      if (host != "") {
        throw new UnsupportedError(
            "Cannot extract a non-Windows file path from a file URI "
            "with an authority");
      }
      _checkNonWindowsPathReservedCharacters(pathSegments, false);
      var result = new StringBuffer();
      if (_isPathAbsolute) result.write("/");
      result.writeAll(pathSegments, "/");
      return result.toString();
    }
  
    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;
      } else {
        _checkWindowsPathReservedCharacters(segments, false);
      }
      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();
    }
  
    bool get _isPathAbsolute {
      if (path == null || path.isEmpty) return false;
      return path.startsWith('/');
    }
  
    void _writeAuthority(StringSink ss) {
      _addIfNonEmpty(ss, userInfo, userInfo, "@");
      ss.write(_host == null ? "null" : _host);
      if (_port != 0) {
        ss.write(":");
        ss.write(_port.toString());
      }
    }
  
    String toString() {
      StringBuffer sb = new StringBuffer();
      _addIfNonEmpty(sb, scheme, scheme, ':');
      if (hasAuthority || (scheme == "file")) {
        sb.write("//");
        _writeAuthority(sb);
      }
      sb.write(path);
      _addIfNonEmpty(sb, query, "?", query);
      _addIfNonEmpty(sb, fragment, "#", fragment);
      return sb.toString();
    }
  
    bool operator==(other) {
      if (other is! Uri) return false;
      Uri uri = other;
      return scheme == uri.scheme &&
          userInfo == uri.userInfo &&
          host == uri.host &&
          port == uri.port &&
          path == uri.path &&
          query == uri.query &&
          fragment == uri.fragment;
    }
  
    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;
      }
      return combine(scheme, combine(userInfo, combine(host, combine(port,
          combine(path, combine(query, combine(fragment, 1)))))));
    }
  
    static void _addIfNonEmpty(StringBuffer sb, String test,
                               String first, String second) {
      if ("" != test) {
        sb.write(first);
        sb.write(second);
      }
    }
  
    /**
     * 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);
    }
  
    /**
     * Encode the string [component] according to the HTML 4.01 rules
     * for encoding the posting of a HTML form as a query string
     * component.
     *
     * Spaces will be replaced with plus and all characters except for
     * uppercase and lowercase letters, decimal digits and the
     * characters `-._~`. Note that the set of characters encoded is a
     * superset of what HTML 4.01 says as 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) {
      return _uriEncode(_unreservedTable, component, spaceToPlus: true);
    }
  
    /**
     * 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);
    }
  
    /**
     * 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, plusToSpace: true, encoding: encoding);
    }
  
    /**
     * 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);
    }
  
    /**
     * 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);
    }
  
    /**
     * 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;
      });
    }
  
    /**
     * 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');
      }
      var bytes = host.split('.');
      if (bytes.length != 4) {
        error('IPv4 address should contain exactly 4 parts');
      }
      // 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();
    }
  
    /**
     * 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.
     *
     * 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) {
      // 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) {
        throw new FormatException('Illegal IPv6 address, $msg');
      }
      int parseHex(int start, int end) {
        if (end - start > 4) {
          error('an IPv6 part can only contain a maximum of 4 hex digits');
        }
        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`');
        }
        return value;
      }
      if (host.length < 2) error('address is too short');
      List<int> parts = [];
      bool wildcardSeen = false;
      int partStart = 0;
      // Parse all parts, except a potential last one.
      for (int i = 0; i < host.length; i++) {
        if (host.codeUnitAt(i) == _COLON) {
          if (i == 0) {
            // If we see a `:` in the beginning, expect wildcard.
            i++;
            if (host.codeUnitAt(i) != _COLON) {
              error('invalid start colon.');
            }
            partStart = i;
          }
          if (i == partStart) {
            // Wildcard. We only allow one.
            if (wildcardSeen) {
              error('only one wildcard `::` is allowed');
            }
            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;
        }
      }
      if (parts.length == 0) error('too few parts');
      bool atEnd = partStart == host.length;
      bool isLastWildcard = parts.last == -1;
      if (atEnd && !isLastWildcard) {
        error('expected a part after last `:`');
      }
      if (!atEnd) {
        try {
          parts.add(parseHex(partStart, host.length));
        } catch (e) {
          // Failed to parse the last chunk as hex. Try IPv4.
          try {
            List<int> last = parseIPv4Address(host.substring(partStart));
            parts.add(last[0] << 8 | last[1]);
            parts.add(last[2] << 8 | last[3]);
          } catch (e) {
            error('invalid end of IPv6 address.');
          }
        }
      }
      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');
      }
      // TODO(ajohnsen): Consider using Uint8List.
      return parts
          .expand((value) {
            if (value == -1) {
              return new List.filled((9 - parts.length) * 2, 0);
            } else {
              return [(value >> 8) & 0xFF, value & 0xFF];
            }
          })
          .toList();
    }
  
    // Frequently used character codes.
    static const int _DOUBLE_QUOTE = 0x22;
    static const int _PERCENT = 0x25;
    static const int _ASTERISK = 0x2A;
    static const int _PLUS = 0x2B;
    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;
  
    /**
     * This is the internal implementation of JavaScript's encodeURI function.
     * It encodes all characters in the string [text] except for those
     * that appear in [canonicalTable], and returns the escaped string.
     */
    static String _uriEncode(List<int> canonicalTable,
                      String text,
                      {bool spaceToPlus: false}) {
      byteToHex(int v) {
        final String hex = '0123456789ABCDEF';
        return '%${hex[v >> 4]}${hex[v & 0x0f]}';
      }
  
      StringBuffer result = new StringBuffer();
      for (int i = 0; i < text.length; i++) {
        int ch = text.codeUnitAt(i);
        if (ch < 128 && ((canonicalTable[ch >> 4] & (1 << (ch & 0x0f))) != 0)) {
          result.write(text[i]);
        } else if (spaceToPlus && text[i] == " ") {
          result.write("+");
        } else {
          if (ch >= 0xD800 && ch < 0xDC00) {
            // Low surrogate. We expect a next char high surrogate.
            ++i;
            int nextCh = text.length == i ? 0 : text.codeUnitAt(i);
            if (nextCh >= 0xDC00 && nextCh < 0xE000) {
              // convert the pair to a U+10000 codepoint
              ch = 0x10000 + ((ch - 0xD800) << 10) + (nextCh - 0xDC00);
            } else {
              throw new ArgumentError('Malformed URI');
            }
          }
          // TODO(floitsch): don't allocate a new string.
          for (int codepoint in UTF8.encode(new String.fromCharCode(ch))) {
            result.write(byteToHex(codepoint));
          }
        }
      }
      return result.toString();
    }
  
    /**
     * Convert a byte (2 character hex sequence) in string [s] starting
     * at position [pos] to its ordinal value
     */
    static int _hexCharPairToByte(String s, int pos) {
      int byte = 0;
      for (int i = 0; i < 2; i++) {
        var charCode = s.codeUnitAt(pos + i);
        if (0x30 <= charCode && charCode <= 0x39) {
          byte = byte * 16 + charCode - 0x30;
        } else {
          // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
          charCode |= 0x20;
          if (0x61 <= charCode && charCode <= 0x66) {
            byte = byte * 16 + charCode - 0x57;
          } else {
            throw new ArgumentError("Invalid URL encoding");
          }
        }
      }
      return byte;
    }
  
    /**
     * Uri-decode a percent-encoded string.
     *
     * It unescapes the string [text] and returns the unescaped string.
     *
     * This function is similar to the JavaScript-function `decodeURI`.
     *
     * If [plusToSpace] is `true`, plus characters will be converted to spaces.
     *
     * The decoder will create a byte-list of the percent-encoded parts, and then
     * decode the byte-list using [encoding]. The default encodingis UTF-8.
     */
    static String _uriDecode(String text,
                             {bool plusToSpace: false,
                              Encoding encoding: UTF8}) {
      StringBuffer result = new StringBuffer();
      List<int> codepoints = new List<int>();
      for (int i = 0; i < text.length;) {
        int ch = text.codeUnitAt(i);
        if (ch != _PERCENT) {
          if (plusToSpace && ch == _PLUS) {
            result.write(" ");
          } else {
            result.writeCharCode(ch);
          }
          i++;
        } else {
          codepoints.clear();
          while (ch == _PERCENT) {
            if (++i > text.length - 2) {
              throw new ArgumentError('Truncated URI');
            }
            codepoints.add(_hexCharPairToByte(text, i));
            i += 2;
            if (i == text.length) break;
            ch = text.codeUnitAt(i);
          }
          result.write(encoding.decode(codepoints));
        }
      }
      return result.toString();
    }
  
    static bool _isAlphabeticCharacter(int codeUnit)
      => (codeUnit >= _LOWER_CASE_A && codeUnit <= _LOWER_CASE_Z) ||
         (codeUnit >= _UPPER_CASE_A && codeUnit <= _UPPER_CASE_Z);
  
    // Tables of char-codes organized as a bit vector of 128 bits where
    // each bit indicate whether a character code on the 0-127 needs to
    // be escaped or not.
  
    // The unreserved characters of RFC 3986.
    static const _unreservedTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //                           -.
        0x6000,   // 0x20 - 0x2f  0000000000000110
                  //              0123456789
        0x03ff,   // 0x30 - 0x3f  1111111111000000
                  //               ABCDEFGHIJKLMNO
        0xfffe,   // 0x40 - 0x4f  0111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  
    // The unreserved characters of RFC 2396.
    static const _unreserved2396Table = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //               !     '()*  -.
        0x6782,   // 0x20 - 0x2f  0100000111100110
                  //              0123456789
        0x03ff,   // 0x30 - 0x3f  1111111111000000
                  //               ABCDEFGHIJKLMNO
        0xfffe,   // 0x40 - 0x4f  0111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  
    // Table of reserved characters specified by ECMAScript 5.
    static const _encodeFullTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //               ! #$ &'()*+,-./
        0xf7da,   // 0x20 - 0x2f  0101101111101111
                  //              0123456789:; = ?
        0xafff,   // 0x30 - 0x3f  1111111111110101
                  //              @ABCDEFGHIJKLMNO
        0xffff,   // 0x40 - 0x4f  1111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  
    // Characters allowed in the scheme.
    static const _schemeTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //                         + -.
        0x6800,   // 0x20 - 0x2f  0000000000010110
                  //              0123456789
        0x03ff,   // 0x30 - 0x3f  1111111111000000
                  //               ABCDEFGHIJKLMNO
        0xfffe,   // 0x40 - 0x4f  0111111111111111
                  //              PQRSTUVWXYZ
        0x07ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz
        0x07ff];  // 0x70 - 0x7f  1111111111100010
  
    // Characters allowed in scheme except for upper case letters.
    static const _schemeLowerTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //                         + -.
        0x6800,   // 0x20 - 0x2f  0000000000010110
                  //              0123456789
        0x03ff,   // 0x30 - 0x3f  1111111111000000
                  //
        0x0000,   // 0x40 - 0x4f  0111111111111111
                  //
        0x0000,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz
        0x07ff];  // 0x70 - 0x7f  1111111111100010
  
    // Sub delimiter characters combined with unreserved as of 3986.
    // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
    //             / "*" / "+" / "," / ";" / "="
    // RFC 3986 section 2.3.
    // unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
    static const _subDelimitersTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //               !  $ &'()*+,-.
        0x7fd2,   // 0x20 - 0x2f  0100101111111110
                  //              0123456789 ; =
        0x2bff,   // 0x30 - 0x3f  1111111111010100
                  //               ABCDEFGHIJKLMNO
        0xfffe,   // 0x40 - 0x4f  0111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  
    // Characters allowed in the path as of RFC 3986.
    // RFC 3986 section 3.3.
    // pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
    static const _pathCharTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //               !  $ &'()*+,-.
        0x7fd2,   // 0x20 - 0x2f  0100101111111110
                  //              0123456789:; =
        0x2fff,   // 0x30 - 0x3f  1111111111110100
                  //              @ABCDEFGHIJKLMNO
        0xffff,   // 0x40 - 0x4f  1111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  
    // Characters allowed in the query as of RFC 3986.
    // RFC 3986 section 3.4.
    // query = *( pchar / "/" / "?" )
    static const _queryCharTable = const [
                  //             LSB            MSB
                  //              |              |
        0x0000,   // 0x00 - 0x0f  0000000000000000
        0x0000,   // 0x10 - 0x1f  0000000000000000
                  //               !  $ &'()*+,-./
        0xffd2,   // 0x20 - 0x2f  0100101111111111
                  //              0123456789:; = ?
        0xafff,   // 0x30 - 0x3f  1111111111110101
                  //              @ABCDEFGHIJKLMNO
        0xffff,   // 0x40 - 0x4f  1111111111111111
                  //              PQRSTUVWXYZ    _
        0x87ff,   // 0x50 - 0x5f  1111111111100001
                  //               abcdefghijklmno
        0xfffe,   // 0x60 - 0x6f  0111111111111111
                  //              pqrstuvwxyz   ~
        0x47ff];  // 0x70 - 0x7f  1111111111100010
  }
  
  class _UnmodifiableMap<K, V> implements Map<K, V> {
    final Map _map;
    const _UnmodifiableMap(this._map);
  
    bool containsValue(Object value) => _map.containsValue(value);
    bool containsKey(Object key) => _map.containsKey(key);
    V operator [](Object key) => _map[key];
    void operator []=(K key, V value) {
      throw new UnsupportedError("Cannot modify an unmodifiable map");
    }
    V putIfAbsent(K key, V ifAbsent()) {
      throw new UnsupportedError("Cannot modify an unmodifiable map");
    }
    addAll(Map other) {
      throw new UnsupportedError("Cannot modify an unmodifiable map");
    }
    V remove(Object key) {
      throw new UnsupportedError("Cannot modify an unmodifiable map");
    }
    void clear() {
      throw new UnsupportedError("Cannot modify an unmodifiable map");
    }
    void forEach(void f(K key, V value)) => _map.forEach(f);
    Iterable<K> get keys => _map.keys;
    Iterable<V> get values => _map.values;
    int get length => _map.length;
    bool get isEmpty => _map.isEmpty;
    bool get isNotEmpty => _map.isNotEmpty;
  }
  