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; |