Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(786)

Unified Diff: sdk/lib/core/uri.dart

Issue 2220373002: Reapply fast-URI patch. (Closed) Base URL: https://github.com/dart-lang/sdk.git@master
Patch Set: Fix bug in SimpleUri.replace where ? was included in the result's query. Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sdk/lib/_internal/js_runtime/lib/core_patch.dart ('k') | tests/compiler/dart2js/compiler_helper.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
« no previous file with comments | « sdk/lib/_internal/js_runtime/lib/core_patch.dart ('k') | tests/compiler/dart2js/compiler_helper.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698