Index: tool/input_sdk/lib/core/uri.dart |
diff --git a/tool/input_sdk/lib/core/uri.dart b/tool/input_sdk/lib/core/uri.dart |
index a6c69bb457b96053d6e49ee901febecd40338263..0cfecebe472a9a36983842d3ce30ffdf6d95deb8 100644 |
--- a/tool/input_sdk/lib/core/uri.dart |
+++ b/tool/input_sdk/lib/core/uri.dart |
@@ -12,30 +12,238 @@ part of dart.core; |
* * [URIs][uris] in the [library tour][libtour] |
* * [RFC-3986](http://tools.ietf.org/html/rfc3986) |
* |
- * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#ch03-uri |
- * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html |
+ * [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 host name of the URI. |
- // Set to `null` if there is no authority in a URI. |
- final String _host; |
- // The port. Set to null if there is no port. Normalized to null if |
- // the port is the default port for the scheme. |
- // Set to the value of the default port if an empty port was supplied. |
- num _port; |
- // The path. Always non-null. |
- String _path; |
- |
/** |
- * Returns the scheme component. |
+ * 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; |
+ |
+ /** |
+ * 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 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); |
+ |
+ /** |
+ * Creates a new URI from its components. |
+ * |
+ * Each component is set through a named argument. Any number of |
+ * components can be provided. The [path] and [query] components can be set |
+ * using either of two different named arguments. |
+ * |
+ * The scheme component is set through [scheme]. The scheme is |
+ * normalized to all lowercase letters. If the scheme is omitted or empty, |
+ * the URI will not have a scheme part. |
+ * |
+ * The user info part of the authority component is set through |
+ * [userInfo]. It defaults to the empty string, which will be omitted |
+ * from the string representation of the URI. |
+ * |
+ * The host part of the authority component is set through |
+ * [host]. The host can either be a hostname, an IPv4 address or an |
+ * IPv6 address, contained in '[' and ']'. If the host contains a |
+ * ':' character, the '[' and ']' are added if not already provided. |
+ * The host is normalized to all lowercase letters. |
+ * |
+ * The port part of the authority component is set through |
+ * [port]. |
+ * If [port] is omitted or `null`, it implies the default port for |
+ * the URI's scheme, and is equivalent to passing that port explicitly. |
+ * The recognized schemes, and their default ports, are "http" (80) and |
+ * "https" (443). All other schemes are considered as having zero as the |
+ * default port. |
+ * |
+ * If any of `userInfo`, `host` or `port` are provided, |
+ * the URI has an autority according to [hasAuthority]. |
+ * |
+ * The path component is set through either [path] or |
+ * [pathSegments]. |
+ * When [path] is used, it should be a valid URI path, |
+ * but invalid characters, except the general delimiters ':/@[]?#', |
+ * will be escaped if necessary. |
+ * When [pathSegments] is used, each of the provided segments |
+ * is first percent-encoded and then joined using the forward slash |
+ * separator. |
+ * |
+ * The percent-encoding of the path segments encodes all |
+ * characters except for the unreserved characters and the following |
+ * list of characters: `!$&'()*+,;=:@`. If the other components |
+ * necessitate an absolute path, a leading slash `/` is prepended if |
+ * not already there. |
+ * |
+ * The query component is set through either [query] or [queryParameters]. |
+ * When [query] is used, the provided string should be a valid URI query, |
+ * but invalid characters, other than general delimiters, |
+ * will be escaped if necessary. |
+ * When [queryParameters] is used the query is built from the |
+ * provided map. Each key and value in the map is percent-encoded |
+ * and joined using equal and ampersand characters. |
+ * A value in the map must be either a string, or an [Iterable] of strings, |
+ * where the latter corresponds to multiple values for the same key. |
+ * |
+ * The percent-encoding of the keys and values encodes all characters |
+ * except for the unreserved characters, and replaces spaces with `+`. |
+ * If `query` is the empty string, it is equivalent to omitting it. |
+ * To have an actual empty query part, |
+ * use an empty map for `queryParameters`. |
+ * |
+ * If both `query` and `queryParameters` are omitted or `null`, |
+ * the URI has no query part. |
+ * |
+ * The fragment component is set through [fragment]. |
+ * It should be a valid URI fragment, but invalid characters other than |
+ * general delimiters, are escaped if necessary. |
+ * If `fragment` is omitted or `null`, the URI has no fragment part. |
+ */ |
+ 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); |
+ } |
+ |
+ /** |
+ * Creates a new `http` URI from authority, path and query. |
+ * |
+ * Examples: |
+ * |
+ * ``` |
+ * // http://example.org/path?q=dart. |
+ * new Uri.http("google.com", "/search", { "q" : "dart" }); |
+ * |
+ * // http://user:pass@localhost:8080 |
+ * new Uri.http("user:pass@localhost:8080", ""); |
+ * |
+ * // http://example.org/a%20b |
+ * new Uri.http("example.org", "a b"); |
+ * |
+ * // http://example.org/a%252F |
+ * new Uri.http("example.org", "/a%2F"); |
+ * ``` |
+ * |
+ * The `scheme` is always set to `http`. |
+ * |
+ * The `userInfo`, `host` and `port` components are set from the |
+ * [authority] argument. If `authority` is `null` or empty, |
+ * the created `Uri` has no authority, and isn't directly usable |
+ * as an HTTP URL, which must have a non-empty host. |
+ * |
+ * The `path` component is set from the [unencodedPath] |
+ * argument. The path passed must not be encoded as this constructor |
+ * encodes the path. |
+ * |
+ * The `query` component is set from the optional [queryParameters] |
+ * argument. |
+ */ |
+ factory Uri.http(String authority, |
+ String unencodedPath, |
+ [Map<String, String> queryParameters]) { |
+ return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
+ } |
+ |
+ /** |
+ * Creates a new `https` URI from authority, path and query. |
+ * |
+ * This constructor is the same as [Uri.http] except for the scheme |
+ * which is set to `https`. |
+ */ |
+ factory Uri.https(String authority, |
+ String unencodedPath, |
+ [Map<String, String> queryParameters]) { |
+ return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
+ } |
+ |
+ /** |
* Returns the authority component. |
* |
* The authority is formatted from the [userInfo], [host] and [port] |
@@ -51,14 +259,6 @@ class Uri { |
} |
/** |
- * 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. |
- */ |
- final String _userInfo; |
- |
- /** |
* Returns the user info part of the authority component. |
* |
* Returns the empty string if there is no user info in the |
@@ -74,6 +274,10 @@ class Uri { |
* |
* 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 { |
if (_host == null) return ""; |
@@ -111,9 +315,6 @@ class Uri { |
*/ |
String get path => _path; |
- // The query content, or null if there is no query. |
- final String _query; |
- |
/** |
* Returns the query component. The returned query is encoded. To get |
* direct access to the decoded query use [queryParameters]. |
@@ -122,9 +323,6 @@ class Uri { |
*/ |
String get query => (_query == null) ? "" : _query; |
- // The fragment content, or null if there is no fragment. |
- final String _fragment; |
- |
/** |
* Returns the fragment identifier component. |
* |
@@ -134,26 +332,18 @@ class Uri { |
String get fragment => (_fragment == null) ? "" : _fragment; |
/** |
- * Cache the computed return value of [pathSegements]. |
- */ |
- List<String> _pathSegments; |
- |
- /** |
- * Cache the computed return value of [queryParameters]. |
- */ |
- Map<String, String> _queryParameters; |
- |
- /** |
* Creates a new `Uri` object by parsing a URI string. |
* |
+ * 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, |
- * invalid characters will be percent escaped where possible. |
- * The resulting `Uri` will represent a valid URI or URI reference. |
+ * a [FormatException] is thrown. |
*/ |
- static Uri parse(String uri) { |
- // This parsing will not validate percent-encoding, IPv6, etc. When done |
- // it will call `new Uri(...)` which will perform these validations. |
- // This is purely splitting up the URI string into components. |
+ 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 ] |
@@ -204,26 +394,24 @@ class Uri { |
// query = *( pchar / "/" / "?" ) |
// |
// fragment = *( pchar / "/" / "?" ) |
- bool isRegName(int ch) { |
- return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
- } |
const int EOI = -1; |
String scheme = ""; |
String userinfo = ""; |
String host = null; |
- num port = null; |
+ int port = null; |
String path = null; |
String query = null; |
String fragment = null; |
+ if (end == null) end = uri.length; |
- int index = 0; |
- int pathStart = 0; |
+ int index = start; |
+ int pathStart = start; |
// End of input-marker. |
int char = EOI; |
void parseAuth() { |
- if (index == uri.length) { |
+ if (index == end) { |
char = EOI; |
return; |
} |
@@ -231,7 +419,7 @@ class Uri { |
int lastColon = -1; |
int lastAt = -1; |
char = uri.codeUnitAt(index); |
- while (index < uri.length) { |
+ while (index < end) { |
char = uri.codeUnitAt(index); |
if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { |
break; |
@@ -245,7 +433,7 @@ class Uri { |
lastColon = -1; |
int endBracket = uri.indexOf(']', index + 1); |
if (endBracket == -1) { |
- index = uri.length; |
+ index = end; |
char = EOI; |
break; |
} else { |
@@ -277,7 +465,7 @@ class Uri { |
hostEnd = lastColon; |
} |
host = _makeHost(uri, hostStart, hostEnd, true); |
- if (index < uri.length) { |
+ if (index < end) { |
char = uri.codeUnitAt(index); |
} |
} |
@@ -298,22 +486,22 @@ class Uri { |
// 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 < uri.length) { |
+ while (i < end) { |
char = uri.codeUnitAt(i); |
if (char == _QUESTION || char == _NUMBER_SIGN) { |
state = NOT_IN_PATH; |
break; |
} |
if (char == _SLASH) { |
- state = (i == 0) ? ALLOW_AUTH : IN_PATH; |
+ state = (i == start) ? ALLOW_AUTH : IN_PATH; |
break; |
} |
if (char == _COLON) { |
- if (i == 0) _fail(uri, 0, "Invalid empty scheme"); |
- scheme = _makeScheme(uri, i); |
+ if (i == start) _fail(uri, start, "Invalid empty scheme"); |
+ scheme = _makeScheme(uri, start, i); |
i++; |
pathStart = i; |
- if (i == uri.length) { |
+ if (i == end) { |
char = EOI; |
state = NOT_IN_PATH; |
} else { |
@@ -338,7 +526,7 @@ class Uri { |
// 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 == uri.length) { |
+ if (index == end) { |
char = EOI; |
state = NOT_IN_PATH; |
} else { |
@@ -360,7 +548,7 @@ class Uri { |
if (state == IN_PATH) { |
// Characters from pathStart to index (inclusive) are known |
// to be part of the path. |
- while (++index < uri.length) { |
+ while (++index < end) { |
char = uri.codeUnitAt(index); |
if (char == _QUESTION || char == _NUMBER_SIGN) { |
break; |
@@ -371,20 +559,25 @@ class Uri { |
} |
assert(state == NOT_IN_PATH); |
- bool isFile = (scheme == "file"); |
- bool ensureLeadingSlash = host != null; |
- path = _makePath(uri, pathStart, index, null, ensureLeadingSlash, isFile); |
+ bool hasAuthority = (host != null); |
+ path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); |
if (char == _QUESTION) { |
- int numberSignIndex = uri.indexOf('#', index + 1); |
+ 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, uri.length, null); |
+ query = _makeQuery(uri, index + 1, end, null); |
} else { |
query = _makeQuery(uri, index + 1, numberSignIndex, null); |
- fragment = _makeFragment(uri, numberSignIndex + 1, uri.length); |
+ fragment = _makeFragment(uri, numberSignIndex + 1, end); |
} |
} else if (char == _NUMBER_SIGN) { |
- fragment = _makeFragment(uri, index + 1, uri.length); |
+ fragment = _makeFragment(uri, index + 1, end); |
} |
return new Uri._internal(scheme, |
userinfo, |
@@ -400,159 +593,6 @@ class Uri { |
throw new FormatException(message, uri, index); |
} |
- /// 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); |
- |
- /** |
- * Creates a new URI from its components. |
- * |
- * Each component is set through a named argument. Any number of |
- * components can be provided. The [path] and [query] components can be set |
- * using either of two different named arguments. |
- * |
- * The scheme component is set through [scheme]. The scheme is |
- * normalized to all lowercase letters. If the scheme is omitted or empty, |
- * the URI will not have a scheme part. |
- * |
- * The user info part of the authority component is set through |
- * [userInfo]. It defaults to the empty string, which will be omitted |
- * from the string representation of the URI. |
- * |
- * The host part of the authority component is set through |
- * [host]. The host can either be a hostname, an IPv4 address or an |
- * IPv6 address, contained in '[' and ']'. If the host contains a |
- * ':' character, the '[' and ']' are added if not already provided. |
- * The host is normalized to all lowercase letters. |
- * |
- * The port part of the authority component is set through |
- * [port]. |
- * If [port] is omitted or `null`, it implies the default port for |
- * the URI's scheme, and is equivalent to passing that port explicitly. |
- * The recognized schemes, and their default ports, are "http" (80) and |
- * "https" (443). All other schemes are considered as having zero as the |
- * default port. |
- * |
- * If any of `userInfo`, `host` or `port` are provided, |
- * the URI will have an autority according to [hasAuthority]. |
- * |
- * The path component is set through either [path] or |
- * [pathSegments]. When [path] is used, it should be a valid URI path, |
- * but invalid characters, except the general delimiters ':/@[]?#', |
- * will be escaped if necessary. |
- * When [pathSegments] is used, each of the provided segments |
- * is first percent-encoded and then joined using the forward slash |
- * separator. The percent-encoding of the path segments encodes all |
- * characters except for the unreserved characters and the following |
- * list of characters: `!$&'()*+,;=:@`. If the other components |
- * calls for an absolute path a leading slash `/` is prepended if |
- * not already there. |
- * |
- * The query component is set through either [query] or |
- * [queryParameters]. When [query] is used the provided string should |
- * be a valid URI query, but invalid characters other than general delimiters, |
- * will be escaped if necessary. |
- * When [queryParameters] is used the query is built from the |
- * provided map. Each key and value in the map is percent-encoded |
- * and joined using equal and ampersand characters. The |
- * percent-encoding of the keys and values encodes all characters |
- * except for the unreserved characters. |
- * If `query` is the empty string, it is equivalent to omitting it. |
- * To have an actual empty query part, |
- * use an empty list for `queryParameters`. |
- * If both `query` and `queryParameters` are omitted or `null`, the |
- * URI will have no query part. |
- * |
- * The fragment component is set through [fragment]. |
- * It should be a valid URI fragment, but invalid characters other than |
- * general delimiters, will be escaped if necessary. |
- * If `fragment` is omitted or `null`, the URI will have no fragment part. |
- */ |
- factory Uri({String scheme : "", |
- String userInfo : "", |
- String host, |
- int port, |
- String path, |
- Iterable<String> pathSegments, |
- String query, |
- Map<String, String> queryParameters, |
- String fragment}) { |
- scheme = _makeScheme(scheme, _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 ensureLeadingSlash = host != null; |
- path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
- ensureLeadingSlash, isFile); |
- return new Uri._internal(scheme, userInfo, host, port, |
- path, query, fragment); |
- } |
- |
- /** |
- * Creates a new `http` URI from authority, path and query. |
- * |
- * Examples: |
- * |
- * ``` |
- * // http://example.org/path?q=dart. |
- * new Uri.http("google.com", "/search", { "q" : "dart" }); |
- * |
- * // http://user:pass@localhost:8080 |
- * new Uri.http("user:pass@localhost:8080", ""); |
- * |
- * // http://example.org/a%20b |
- * new Uri.http("example.org", "a b"); |
- * |
- * // http://example.org/a%252F |
- * new Uri.http("example.org", "/a%2F"); |
- * ``` |
- * |
- * The `scheme` is always set to `http`. |
- * |
- * The `userInfo`, `host` and `port` components are set from the |
- * [authority] argument. If `authority` is `null` or empty, |
- * the created `Uri` will have no authority, and will not be directly usable |
- * as an HTTP URL, which must have a non-empty host. |
- * |
- * The `path` component is set from the [unencodedPath] |
- * argument. The path passed must not be encoded as this constructor |
- * encodes the path. |
- * |
- * The `query` component is set from the optional [queryParameters] |
- * argument. |
- */ |
- factory Uri.http(String authority, |
- String unencodedPath, |
- [Map<String, String> queryParameters]) { |
- return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
- } |
- |
- /** |
- * Creates a new `https` URI from authority, path and query. |
- * |
- * This constructor is the same as [Uri.http] except for the scheme |
- * which is set to `https`. |
- */ |
- factory Uri.https(String authority, |
- String unencodedPath, |
- [Map<String, String> queryParameters]) { |
- return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
- } |
- |
static Uri _makeHttpUri(String scheme, |
String authority, |
String unencodedPath, |
@@ -694,8 +734,90 @@ class Uri { |
* If the path passed is not a legal file path [ArgumentError] is thrown. |
*/ |
factory Uri.file(String path, {bool windows}) { |
- windows = windows == null ? Uri._isWindows : windows; |
- return windows ? _makeWindowsFileUrl(path) : _makeFileUri(path); |
+ 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. |
+ * |
+ * 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 |
+ * are instead encoded using [BASE64]. |
+ * |
+ * If [encoding] is not provided and [parameters] has a `charset` entry, |
+ * that name is looked up using [Encoding.getByName], |
+ * and if the lookup returns an encoding, that encoding is used to convert |
+ * [content] to bytes. |
+ * If providing both an [encoding] and a charset [parameter], they should |
+ * agree, otherwise decoding won't be able to use the charset parameter |
+ * to determine the encoding. |
+ * |
+ * If [mimeType] and/or [parameters] are supplied, they are added to the |
+ * created URI. If any of these contain characters that are not allowed |
+ * in the data URI, the character is percent-escaped. If the character is |
+ * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
+ * encoded. An omitted [mimeType] in a data URI means `text/plain`, just |
+ * as an omitted `charset` parameter defaults to meaning `US-ASCII`. |
+ * |
+ * To read the content back, use [UriData.contentAsString]. |
+ */ |
+ factory Uri.dataFromString(String content, |
+ {String mimeType, |
+ Encoding encoding, |
+ Map<String, String> parameters, |
+ bool base64: false}) { |
+ UriData data = new UriData.fromString(content, |
+ mimeType: mimeType, |
+ encoding: encoding, |
+ parameters: parameters, |
+ base64: base64); |
+ return data.uri; |
+ } |
+ |
+ /** |
+ * Creates a `data:` URI containing an encoding of [bytes]. |
+ * |
+ * Defaults to Base64 encoding the bytes, but if [percentEncoded] |
+ * is `true`, the bytes will instead be percent encoded (any non-ASCII |
+ * or non-valid-ASCII-character byte is replaced by a percent encoding). |
+ * |
+ * To read the bytes back, use [UriData.contentAsBytes]. |
+ * |
+ * It defaults to having the mime-type `application/octet-stream`. |
+ * The [mimeType] and [parameters] are added to the created URI. |
+ * If any of these contain characters that are not allowed |
+ * in the data URI, the character is percent-escaped. If the character is |
+ * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
+ * encoded. |
+ */ |
+ factory Uri.dataFromBytes(List<int> bytes, |
+ {mimeType: "application/octet-stream", |
+ Map<String, String> parameters, |
+ percentEncoded: false}) { |
+ UriData data = new UriData.fromBytes(bytes, |
+ mimeType: mimeType, |
+ parameters: parameters, |
+ percentEncoded: percentEncoded); |
+ return data.uri; |
} |
/** |
@@ -727,7 +849,7 @@ class Uri { |
static _checkWindowsPathReservedCharacters(List<String> segments, |
bool argumentError, |
[int firstSegment = 0]) { |
- segments.skip(firstSegment).forEach((segment) { |
+ for (var segment in segments.skip(firstSegment)) { |
if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { |
if (argumentError) { |
throw new ArgumentError("Illegal character in path"); |
@@ -735,7 +857,7 @@ class Uri { |
throw new UnsupportedError("Illegal character in path"); |
} |
} |
- }); |
+ } |
} |
static _checkWindowsDriveLetter(int charCode, bool argumentError) { |
@@ -752,35 +874,41 @@ class Uri { |
} |
} |
- static _makeFileUri(String path) { |
- String sep = "/"; |
- if (path.startsWith(sep)) { |
+ 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: path.split(sep)); |
+ return new Uri(scheme: "file", pathSegments: segments); |
} else { |
// Relative URI. |
- return new Uri(pathSegments: path.split(sep)); |
+ return new Uri(pathSegments: segments); |
} |
} |
- static _makeWindowsFileUrl(String path) { |
- if (path.startsWith("\\\\?\\")) { |
- if (path.startsWith("\\\\?\\UNC\\")) { |
- path = "\\${path.substring(7)}"; |
+ 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( |
- "Windows paths with \\\\?\\ prefix must be absolute"); |
+ r"Windows paths with \\?\ prefix must be absolute"); |
} |
} |
} else { |
- path = path.replaceAll("/", "\\"); |
+ path = path.replaceAll("/", r'\'); |
} |
- String sep = "\\"; |
- if (path.length > 1 && path[1] == ":") { |
+ 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( |
@@ -788,25 +916,37 @@ class Uri { |
} |
// 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); |
} |
- if (path.length > 0 && path[0] == sep) { |
- if (path.length > 1 && path[1] == sep) { |
+ if (path.startsWith(sep)) { |
+ if (path.startsWith(sep, 1)) { |
// Absolute file:// URI with host. |
- int pathStart = path.indexOf("\\", 2); |
+ int pathStart = path.indexOf(r'\', 2); |
String hostPart = |
- pathStart == -1 ? path.substring(2) : path.substring(2, pathStart); |
+ (pathStart < 0) ? path.substring(2) : path.substring(2, pathStart); |
String pathPart = |
- pathStart == -1 ? "" : path.substring(pathStart + 1); |
+ (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); |
} |
@@ -814,6 +954,11 @@ class Uri { |
// 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); |
} |
} |
@@ -865,14 +1010,14 @@ class Uri { |
String path, |
Iterable<String> pathSegments, |
String query, |
- Map<String, String> queryParameters, |
+ 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, scheme.length); |
+ scheme = _makeScheme(scheme, 0, scheme.length); |
schemeChanged = true; |
} else { |
scheme = this.scheme; |
@@ -881,7 +1026,7 @@ class Uri { |
if (userInfo != null) { |
userInfo = _makeUserInfo(userInfo, 0, userInfo.length); |
} else { |
- userInfo = this.userInfo; |
+ userInfo = this._userInfo; |
} |
if (port != null) { |
port = _makePort(port, scheme); |
@@ -895,33 +1040,33 @@ class Uri { |
if (host != null) { |
host = _makeHost(host, 0, host.length, false); |
} else if (this.hasAuthority) { |
- host = this.host; |
+ host = this._host; |
} else if (userInfo.isNotEmpty || port != null || isFile) { |
host = ""; |
} |
- bool ensureLeadingSlash = (host != null); |
+ bool hasAuthority = host != null; |
if (path != null || pathSegments != null) { |
path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
- ensureLeadingSlash, isFile); |
+ scheme, hasAuthority); |
} else { |
- path = this.path; |
- if ((isFile || (ensureLeadingSlash && !path.isEmpty)) && |
+ path = this._path; |
+ if ((isFile || (hasAuthority && !path.isEmpty)) && |
!path.startsWith('/')) { |
- path = "/$path"; |
+ path = "/" + path; |
} |
} |
if (query != null || queryParameters != null) { |
query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
- } else if (this.hasQuery) { |
- query = this.query; |
+ } else { |
+ query = this._query; |
} |
if (fragment != null) { |
fragment = _makeFragment(fragment, 0, fragment.length); |
- } else if (this.hasFragment) { |
- fragment = this.fragment; |
+ } else { |
+ fragment = this._fragment; |
} |
return new Uri._internal( |
@@ -929,49 +1074,112 @@ class Uri { |
} |
/** |
- * 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. |
+ * 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() { |
+ if (!this.hasFragment) return this; |
+ return new Uri._internal(scheme, _userInfo, _host, _port, |
+ _path, _query, null); |
+ } |
+ |
+ /** |
+ * Returns the URI path split into its segments. Each of the segments in the |
+ * returned list have been decoded. If the path is empty the empty list will |
+ * be returned. A leading slash `/` does not affect the segments returned. |
* |
* The returned list is unmodifiable and will throw [UnsupportedError] on any |
* calls that would mutate it. |
*/ |
List<String> get pathSegments { |
- if (_pathSegments == null) { |
- var pathToSplit = !path.isEmpty && path.codeUnitAt(0) == _SLASH |
- ? path.substring(1) |
- : path; |
- _pathSegments = new UnmodifiableListView( |
- pathToSplit == "" ? const<String>[] |
- : new List<String>.from( |
- pathToSplit.split("/") |
- .map(Uri.decodeComponent), |
- growable: false)); |
- } |
- return _pathSegments; |
+ var result = _pathSegments; |
+ if (result != null) return result; |
+ |
+ 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; |
} |
/** |
* 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. |
+ * 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 and will throw [UnsupportedError] on any |
- * calls that would mutate it. |
+ * The returned map is unmodifiable. |
*/ |
Map<String, String> get queryParameters { |
if (_queryParameters == null) { |
- _queryParameters = new UnmodifiableMapView(splitQueryString(query)); |
+ _queryParameters = |
+ new UnmodifiableMapView<String, String>(splitQueryString(query)); |
} |
return _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 { |
+ if (_queryParameterLists == null) { |
+ Map queryParameterLists = _splitQueryStringAll(query); |
+ for (var key in queryParameterLists.keys) { |
+ queryParameterLists[key] = |
+ new List<String>.unmodifiable(queryParameterLists[key]); |
+ } |
+ _queryParameterLists = |
+ new Map<String, List<String>>.unmodifiable(queryParameterLists); |
+ } |
+ return _queryParameterLists; |
+ } |
+ |
+ /** |
+ * 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() { |
+ String path = _normalizePath(_path, scheme, hasAuthority); |
+ if (identical(path, _path)) return this; |
+ return this.replace(path: path); |
+ } |
+ |
static int _makePort(int port, String scheme) { |
// Perform scheme specific normalization. |
if (port != null && port == _defaultPort(scheme)) return null; |
@@ -979,7 +1187,7 @@ class Uri { |
} |
/** |
- * Check and normalize a most name. |
+ * 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 |
@@ -1101,24 +1309,24 @@ class Uri { |
* |
* Schemes are converted to lower case. They cannot contain escapes. |
*/ |
- static String _makeScheme(String scheme, int end) { |
- if (end == 0) return ""; |
- final int firstCodeUnit = scheme.codeUnitAt(0); |
+ static String _makeScheme(String scheme, int start, int end) { |
+ if (start == end) return ""; |
+ final int firstCodeUnit = scheme.codeUnitAt(start); |
if (!_isAlphabeticCharacter(firstCodeUnit)) { |
- _fail(scheme, 0, "Scheme not starting with alphabetic character"); |
+ _fail(scheme, start, "Scheme not starting with alphabetic character"); |
} |
- bool allLowercase = firstCodeUnit >= _LOWER_CASE_A; |
- for (int i = 0; i < end; i++) { |
+ 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 (codeUnit < _LOWER_CASE_A || codeUnit > _LOWER_CASE_Z) { |
- allLowercase = false; |
+ if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { |
+ containsUpperCase = true; |
} |
} |
- scheme = scheme.substring(0, end); |
- if (!allLowercase) scheme = scheme.toLowerCase(); |
+ scheme = scheme.substring(start, end); |
+ if (containsUpperCase) scheme = scheme.toLowerCase(); |
return scheme; |
} |
@@ -1129,8 +1337,10 @@ class Uri { |
static String _makePath(String path, int start, int end, |
Iterable<String> pathSegments, |
- bool ensureLeadingSlash, |
- bool isFile) { |
+ 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'); |
@@ -1139,19 +1349,33 @@ class Uri { |
if (path != null) { |
result = _normalize(path, start, end, _pathCharOrSlashTable); |
} else { |
- result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); |
+ result = pathSegments.map((s) => |
+ _uriEncode(_pathCharTable, s, UTF8, false)).join("/"); |
} |
if (result.isEmpty) { |
if (isFile) return "/"; |
- } else if ((isFile || ensureLeadingSlash) && |
- result.codeUnitAt(0) != _SLASH) { |
- return "/$result"; |
+ } else if (ensureLeadingSlash && !result.startsWith('/')) { |
+ result = "/" + result; |
} |
+ result = _normalizePath(result, scheme, hasAuthority); |
return result; |
} |
- static String _makeQuery(String query, int start, int end, |
- Map<String, String> queryParameters) { |
+ /// 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); |
+ } |
+ |
+ 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'); |
@@ -1159,17 +1383,27 @@ class Uri { |
if (query != null) return _normalize(query, start, end, _queryCharTable); |
var result = new StringBuffer(); |
- var first = true; |
- queryParameters.forEach((key, value) { |
- if (!first) { |
- result.write("&"); |
- } |
- first = false; |
+ var separator = ""; |
+ |
+ void writeParameter(String key, String value) { |
+ result.write(separator); |
+ separator = "&"; |
result.write(Uri.encodeQueryComponent(key)); |
- if (value != null && !value.isEmpty) { |
+ if (value != null && value.isNotEmpty) { |
result.write("="); |
result.write(Uri.encodeQueryComponent(value)); |
} |
+ } |
+ |
+ 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(); |
} |
@@ -1179,20 +1413,7 @@ class Uri { |
return _normalize(fragment, start, end, _queryCharTable); |
} |
- static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
- |
- static bool _isHexDigit(int char) { |
- if (_NINE >= char) return _ZERO <= char; |
- char |= 0x20; |
- return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char; |
- } |
- |
- static int _hexValue(int char) { |
- assert(_isHexDigit(char)); |
- if (_NINE >= char) return char - _ZERO; |
- char |= 0x20; |
- return char - (_LOWER_CASE_A - 10); |
- } |
+ static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
/** |
* Performs RFC 3986 Percent-Encoding Normalization. |
@@ -1214,10 +1435,12 @@ class Uri { |
} |
int firstDigit = source.codeUnitAt(index + 1); |
int secondDigit = source.codeUnitAt(index + 2); |
- if (!_isHexDigit(firstDigit) || !_isHexDigit(secondDigit)) { |
+ int firstDigitValue = _parseHexDigit(firstDigit); |
+ int secondDigitValue = _parseHexDigit(secondDigit); |
+ if (firstDigitValue < 0 || secondDigitValue < 0) { |
return "%"; // Marks the escape as invalid. |
} |
- int value = _hexValue(firstDigit) * 16 + _hexValue(secondDigit); |
+ int value = firstDigitValue * 16 + secondDigitValue; |
if (_isUnreservedChar(value)) { |
if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { |
value |= 0x20; |
@@ -1233,21 +1456,27 @@ class Uri { |
return null; |
} |
- static bool _isUnreservedChar(int ch) { |
- return ch < 127 && |
- ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
+ // 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); |
+ } |
+ return -1; |
} |
- static String _escapeChar(char) { |
+ static String _escapeChar(int char) { |
assert(char <= 0x10ffff); // It's a valid unicode code point. |
- const hexDigits = "0123456789ABCDEF"; |
- List codeUnits; |
+ 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); |
+ codeUnits[1] = _hexDigits.codeUnitAt(char >> 4); |
+ codeUnits[2] = _hexDigits.codeUnitAt(char & 0xf); |
} 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. |
@@ -1265,8 +1494,8 @@ class Uri { |
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); |
+ 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. |
} |
@@ -1358,8 +1587,7 @@ class Uri { |
*/ |
bool get isAbsolute => scheme != "" && fragment == ""; |
- String _merge(String base, String reference) { |
- if (base.isEmpty) return "/$reference"; |
+ String _mergePaths(String base, String reference) { |
// Optimize for the case: absolute base, reference beginning with "../". |
int backCount = 0; |
int refStart = 0; |
@@ -1388,25 +1616,40 @@ class Uri { |
baseEnd = newEnd; |
backCount--; |
} |
- return base.substring(0, baseEnd + 1) + |
- reference.substring(refStart - 3 * backCount); |
+ return base.replaceRange(baseEnd + 1, null, |
+ reference.substring(refStart - 3 * backCount)); |
} |
- bool _hasDotSegments(String path) { |
- if (path.length > 0 && path.codeUnitAt(0) == _DOT) return true; |
+ /// 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; |
} |
- String _removeDotSegments(String path) { |
- if (!_hasDotSegments(path)) return 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.isEmpty && |
- ((output.length != 1) || (output[0] != ""))) output.removeLast(); |
+ if (output.isNotEmpty) { |
+ output.removeLast(); |
+ if (output.isEmpty) { |
+ output.add(""); |
+ } |
+ } |
appendSlash = true; |
} else if ("." == segment) { |
appendSlash = true; |
@@ -1418,6 +1661,42 @@ class Uri { |
return output.join("/"); |
} |
+ /// 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("/"); |
+ } |
+ |
/** |
* Resolve [reference] as an URI relative to `this`. |
* |
@@ -1437,9 +1716,14 @@ class Uri { |
* |
* Returns the resolved URI. |
* |
- * The algorithm for resolving a reference is described in |
- * [RFC-3986 Section 5] |
- * (http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). |
+ * 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) { |
// From RFC 3986. |
@@ -1470,6 +1754,9 @@ class Uri { |
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) { |
@@ -1478,16 +1765,32 @@ class Uri { |
targetQuery = this._query; |
} |
} else { |
- if (reference.path.startsWith("/")) { |
+ if (reference.hasAbsolutePath) { |
targetPath = _removeDotSegments(reference.path); |
} else { |
- targetPath = _removeDotSegments(_merge(this._path, reference.path)); |
+ // 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); |
+ } |
+ } |
} |
if (reference.hasQuery) targetQuery = reference.query; |
} |
- targetUserInfo = this._userInfo; |
- targetHost = this._host; |
- targetPort = this._port; |
} |
} |
String fragment = reference.hasFragment ? reference.fragment : null; |
@@ -1501,6 +1804,11 @@ class Uri { |
} |
/** |
+ * Returns whether the URI has a [scheme] component. |
+ */ |
+ bool get hasScheme => scheme.isNotEmpty; |
+ |
+ /** |
* Returns whether the URI has an [authority] component. |
*/ |
bool get hasAuthority => _host != null; |
@@ -1526,6 +1834,16 @@ class Uri { |
bool get hasFragment => _fragment != null; |
/** |
+ * Returns whether the URI has an empty path. |
+ */ |
+ bool get hasEmptyPath => _path.isEmpty; |
+ |
+ /** |
+ * Returns whether the URI has an absolute path (starting with '/'). |
+ */ |
+ bool get hasAbsolutePath => _path.startsWith('/'); |
+ |
+ /** |
* Returns the origin of the URI in the form scheme://host:port for the |
* schemes http and https. |
* |
@@ -1679,6 +1997,16 @@ class Uri { |
} |
} |
+ /** |
+ * 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() { |
StringBuffer sb = new StringBuffer(); |
_addIfNonEmpty(sb, scheme, scheme, ':'); |
@@ -1697,16 +2025,16 @@ class Uri { |
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; |
+ 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; |
} |
int get hashCode { |
@@ -1747,7 +2075,7 @@ class Uri { |
* a [Uri]. |
*/ |
static String encodeComponent(String component) { |
- return _uriEncode(_unreserved2396Table, component); |
+ return _uriEncode(_unreserved2396Table, component, UTF8, false); |
} |
/** |
@@ -1785,8 +2113,7 @@ class Uri { |
*/ |
static String encodeQueryComponent(String component, |
{Encoding encoding: UTF8}) { |
- return _uriEncode( |
- _unreservedTable, component, encoding: encoding, spaceToPlus: true); |
+ return _uriEncode(_unreservedTable, component, encoding, true); |
} |
/** |
@@ -1803,7 +2130,8 @@ class Uri { |
* decoded component. |
*/ |
static String decodeComponent(String encodedComponent) { |
- return _uriDecode(encodedComponent); |
+ return _uriDecode(encodedComponent, 0, encodedComponent.length, |
+ UTF8, false); |
} |
/** |
@@ -1817,7 +2145,8 @@ class Uri { |
static String decodeQueryComponent( |
String encodedComponent, |
{Encoding encoding: UTF8}) { |
- return _uriDecode(encodedComponent, plusToSpace: true, encoding: encoding); |
+ return _uriDecode(encodedComponent, 0, encodedComponent.length, |
+ encoding, true); |
} |
/** |
@@ -1830,7 +2159,7 @@ class Uri { |
* the encodeURI function . |
*/ |
static String encodeFull(String uri) { |
- return _uriEncode(_encodeFullTable, uri); |
+ return _uriEncode(_encodeFullTable, uri, UTF8, false); |
} |
/** |
@@ -1842,16 +2171,14 @@ class Uri { |
* [Uri.parse] before decoding the separate components. |
*/ |
static String decodeFull(String uri) { |
- return _uriDecode(uri); |
+ return _uriDecode(uri, 0, uri.length, UTF8, false); |
} |
/** |
* 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] |
+ * 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 |
@@ -1878,6 +2205,46 @@ class Uri { |
}); |
} |
+ 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); |
+ } |
+ result.putIfAbsent(key, _createList).add(value); |
+ } |
+ |
+ 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++; |
+ } |
+ parsePair(start, equalsIndex, i); |
+ return result; |
+ } |
+ |
/** |
* 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). |
@@ -1999,8 +2366,7 @@ class Uri { |
} else if (parts.length != 8) { |
error('an address without a wildcard must contain exactly 8 parts'); |
} |
- // TODO(ajohnsen): Consider using Uint8List. |
- List bytes = new List<int>(16); |
+ List<int> bytes = new Uint8List(16); |
for (int i = 0, index = 0; i < parts.length; i++) { |
int value = parts[i]; |
if (value == -1) { |
@@ -2046,39 +2412,12 @@ class Uri { |
static const int _LOWER_CASE_Z = 0x7A; |
static const int _BAR = 0x7C; |
- /** |
- * This is the internal implementation of JavaScript's encodeURI function. |
- * It encodes all characters in the string [text] except for those |
- * that appear in [canonicalTable], and returns the escaped string. |
- */ |
- static String _uriEncode(List<int> canonicalTable, |
- String text, |
- {Encoding encoding: UTF8, |
- bool spaceToPlus: false}) { |
- byteToHex(byte, buffer) { |
- const String hex = '0123456789ABCDEF'; |
- buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); |
- buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); |
- } |
+ static const String _hexDigits = "0123456789ABCDEF"; |
- // Encode the string into bytes then generate an ASCII only string |
- // by percent encoding selected bytes. |
- StringBuffer result = new StringBuffer(); |
- var bytes = encoding.encode(text); |
- for (int i = 0; i < bytes.length; i++) { |
- int byte = bytes[i]; |
- if (byte < 128 && |
- ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
- result.writeCharCode(byte); |
- } else if (spaceToPlus && byte == _SPACE) { |
- result.writeCharCode(_PLUS); |
- } else { |
- result.writeCharCode(_PERCENT); |
- byteToHex(byte, result); |
- } |
- } |
- return result.toString(); |
- } |
+ external static String _uriEncode(List<int> canonicalTable, |
+ String text, |
+ Encoding encoding, |
+ bool spaceToPlus); |
/** |
* Convert a byte (2 character hex sequence) in string [s] starting |
@@ -2116,24 +2455,35 @@ class Uri { |
* decode the byte-list using [encoding]. The default encodingis UTF-8. |
*/ |
static String _uriDecode(String text, |
- {bool plusToSpace: false, |
- Encoding encoding: UTF8}) { |
+ int start, |
+ int end, |
+ Encoding encoding, |
+ bool plusToSpace) { |
+ assert(0 <= start); |
+ assert(start <= end); |
+ assert(end <= text.length); |
+ assert(encoding != null); |
// First check whether there is any characters which need special handling. |
bool simple = true; |
- for (int i = 0; i < text.length && simple; i++) { |
+ for (int i = start; i < end; i++) { |
var codeUnit = text.codeUnitAt(i); |
- simple = codeUnit != _PERCENT && codeUnit != _PLUS; |
+ if (codeUnit > 127 || |
+ codeUnit == _PERCENT || |
+ (plusToSpace && codeUnit == _PLUS)) { |
+ simple = false; |
+ break; |
+ } |
} |
List<int> bytes; |
if (simple) { |
- if (encoding == UTF8 || encoding == LATIN1) { |
- return text; |
+ if (UTF8 == encoding || LATIN1 == encoding || ASCII == encoding) { |
+ return text.substring(start, end); |
} else { |
- bytes = text.codeUnits; |
+ bytes = text.substring(start, end).codeUnits; |
} |
} else { |
bytes = new List(); |
- for (int i = 0; i < text.length; i++) { |
+ for (int i = start; i < end; i++) { |
var codeUnit = text.codeUnitAt(i); |
if (codeUnit > 127) { |
throw new ArgumentError("Illegal percent encoding in URI"); |
@@ -2154,9 +2504,15 @@ class Uri { |
return encoding.decode(bytes); |
} |
- static bool _isAlphabeticCharacter(int codeUnit) |
- => (codeUnit >= _LOWER_CASE_A && codeUnit <= _LOWER_CASE_Z) || |
- (codeUnit >= _UPPER_CASE_A && codeUnit <= _UPPER_CASE_Z); |
+ static bool _isAlphabeticCharacter(int codeUnit) { |
+ var lowerCase = codeUnit | 0x20; |
+ return (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_Z); |
+ } |
+ |
+ static bool _isUnreservedChar(int char) { |
+ return char < 127 && |
+ ((_unreservedTable[char >> 4] & (1 << (char & 0x0f))) != 0); |
+ } |
// Tables of char-codes organized as a bit vector of 128 bits where |
// each bit indicate whether a character code on the 0-127 needs to |
@@ -2405,4 +2761,594 @@ class Uri { |
0xfffe, // 0x60 - 0x6f 0111111111111111 |
// pqrstuvwxyz ~ |
0x47ff]; // 0x70 - 0x7f 1111111111100010 |
+ |
+} |
+ |
+// -------------------------------------------------------------------- |
+// Data URI |
+// -------------------------------------------------------------------- |
+ |
+/** |
+ * A way to access the structure of a `data:` URI. |
+ * |
+ * Data URIs are non-hierarchical URIs that can contain any binary data. |
+ * They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397). |
+ * |
+ * This class allows parsing the URI text and extracting individual parts of the |
+ * URI, as well as building the URI text from structured parts. |
+ */ |
+class UriData { |
+ static const int _noScheme = -1; |
+ /** |
+ * Contains the text content of a `data:` URI, with or without a |
+ * leading `data:`. |
+ * |
+ * If [_separatorIndices] starts with `4` (the index of the `:`), then |
+ * there is a leading `data:`, otherwise [_separatorIndices] starts with |
+ * `-1`. |
+ */ |
+ final String _text; |
+ |
+ /** |
+ * List of the separators (';', '=' and ',') in the text. |
+ * |
+ * Starts with the index of the `:` in `data:` of the mimeType. |
+ * That is always either -1 or 4, depending on whether `_text` includes the |
+ * `data:` scheme or not. |
+ * |
+ * The first speparator ends the mime type. We don't bother with finding |
+ * the '/' inside the mime type. |
+ * |
+ * Each two separators after that marks a parameter key and value. |
+ * |
+ * If there is a single separator left, it ends the "base64" marker. |
+ * |
+ * So the following separators are found for a text: |
+ * |
+ * data:text/plain;foo=bar;base64,ARGLEBARGLE= |
+ * ^ ^ ^ ^ ^ |
+ * |
+ */ |
+ final List<int> _separatorIndices; |
+ |
+ /** |
+ * Cache of the result returned by [uri]. |
+ */ |
+ Uri _uriCache; |
+ |
+ UriData._(this._text, this._separatorIndices, this._uriCache); |
+ |
+ /** |
+ * Creates a `data:` URI containing the [content] string. |
+ * |
+ * Equivalent to `new Uri.dataFromString(...).data`, but may |
+ * be more efficient if the [uri] itself isn't used. |
+ */ |
+ factory UriData.fromString(String content, |
+ {String mimeType, |
+ Encoding encoding, |
+ Map<String, String> parameters, |
+ bool base64: false}) { |
+ StringBuffer buffer = new StringBuffer(); |
+ List<int> indices = [_noScheme]; |
+ String charsetName; |
+ String encodingName; |
+ if (parameters != null) charsetName = parameters["charset"]; |
+ if (encoding == null) { |
+ if (charsetName != null) { |
+ encoding = Encoding.getByName(charsetName); |
+ } |
+ } else if (charsetName == null) { |
+ // Non-null only if parameters does not contain "charset". |
+ encodingName = encoding.name; |
+ } |
+ encoding ??= ASCII; |
+ _writeUri(mimeType, encodingName, parameters, buffer, indices); |
+ indices.add(buffer.length); |
+ if (base64) { |
+ buffer.write(';base64,'); |
+ indices.add(buffer.length - 1); |
+ buffer.write(encoding.fuse(BASE64).encode(content)); |
+ } else { |
+ buffer.write(','); |
+ _uriEncodeBytes(_uricTable, encoding.encode(content), buffer); |
+ } |
+ return new UriData._(buffer.toString(), indices, null); |
+ } |
+ |
+ /** |
+ * Creates a `data:` URI containing an encoding of [bytes]. |
+ * |
+ * Equivalent to `new Uri.dataFromBytes(...).data`, but may |
+ * be more efficient if the [uri] itself isn't used. |
+ */ |
+ factory UriData.fromBytes(List<int> bytes, |
+ {mimeType: "application/octet-stream", |
+ Map<String, String> parameters, |
+ percentEncoded: false}) { |
+ StringBuffer buffer = new StringBuffer(); |
+ List<int> indices = [_noScheme]; |
+ _writeUri(mimeType, null, parameters, buffer, indices); |
+ indices.add(buffer.length); |
+ if (percentEncoded) { |
+ buffer.write(','); |
+ _uriEncodeBytes(_uricTable, bytes, buffer); |
+ } else { |
+ buffer.write(';base64,'); |
+ indices.add(buffer.length - 1); |
+ BASE64.encoder |
+ .startChunkedConversion( |
+ new StringConversionSink.fromStringSink(buffer)) |
+ .addSlice(bytes, 0, bytes.length, true); |
+ } |
+ |
+ return new UriData._(buffer.toString(), indices, null); |
+ } |
+ |
+ /** |
+ * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. |
+ * |
+ * The [uri] must have scheme `data` and no authority or fragment, |
+ * and the path (concatenated with the query, if there is one) must be valid |
+ * as data URI content with the same rules as [parse]. |
+ */ |
+ factory UriData.fromUri(Uri uri) { |
+ if (uri.scheme != "data") { |
+ throw new ArgumentError.value(uri, "uri", |
+ "Scheme must be 'data'"); |
+ } |
+ if (uri.hasAuthority) { |
+ throw new ArgumentError.value(uri, "uri", |
+ "Data uri must not have authority"); |
+ } |
+ if (uri.hasFragment) { |
+ throw new ArgumentError.value(uri, "uri", |
+ "Data uri must not have a fragment part"); |
+ } |
+ if (!uri.hasQuery) { |
+ return _parse(uri.path, 0, uri); |
+ } |
+ // Includes path and query (and leading "data:"). |
+ return _parse("$uri", 5, uri); |
+ } |
+ |
+ /** |
+ * Writes the initial part of a `data:` uri, from after the "data:" |
+ * until just before the ',' before the data, or before a `;base64,` |
+ * marker. |
+ * |
+ * Of an [indices] list is passed, separator indices are stored in that |
+ * list. |
+ */ |
+ static void _writeUri(String mimeType, |
+ String charsetName, |
+ Map<String, String> parameters, |
+ StringBuffer buffer, List indices) { |
+ if (mimeType == null || mimeType == "text/plain") { |
+ mimeType = ""; |
+ } |
+ if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { |
+ buffer.write(mimeType); // Common cases need no escaping. |
+ } else { |
+ int slashIndex = _validateMimeType(mimeType); |
+ if (slashIndex < 0) { |
+ throw new ArgumentError.value(mimeType, "mimeType", |
+ "Invalid MIME type"); |
+ } |
+ buffer.write(Uri._uriEncode(_tokenCharTable, |
+ mimeType.substring(0, slashIndex), |
+ UTF8, false)); |
+ buffer.write("/"); |
+ buffer.write(Uri._uriEncode(_tokenCharTable, |
+ mimeType.substring(slashIndex + 1), |
+ UTF8, false)); |
+ } |
+ if (charsetName != null) { |
+ if (indices != null) { |
+ indices..add(buffer.length) |
+ ..add(buffer.length + 8); |
+ } |
+ buffer.write(";charset="); |
+ buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
+ } |
+ parameters?.forEach((var key, var value) { |
+ if (key.isEmpty) { |
+ throw new ArgumentError.value("", "Parameter names must not be empty"); |
+ } |
+ if (value.isEmpty) { |
+ throw new ArgumentError.value("", "Parameter values must not be empty", |
+ 'parameters["$key"]'); |
+ } |
+ 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)); |
+ if (indices != null) indices.add(buffer.length); |
+ buffer.write('='); |
+ buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
+ }); |
+ } |
+ |
+ /** |
+ * Checks mimeType is valid-ish (`token '/' token`). |
+ * |
+ * Returns the index of the slash, or -1 if the mime type is not |
+ * considered valid. |
+ * |
+ * Currently only looks for slashes, all other characters will be |
+ * percent-encoded as UTF-8 if necessary. |
+ */ |
+ static int _validateMimeType(String mimeType) { |
+ int slashIndex = -1; |
+ for (int i = 0; i < mimeType.length; i++) { |
+ var char = mimeType.codeUnitAt(i); |
+ if (char != Uri._SLASH) continue; |
+ if (slashIndex < 0) { |
+ slashIndex = i; |
+ continue; |
+ } |
+ return -1; |
+ } |
+ return slashIndex; |
+ } |
+ |
+ /** |
+ * Parses a string as a `data` URI. |
+ * |
+ * The string must have the format: |
+ * |
+ * ``` |
+ * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' data |
+ * ```` |
+ * |
+ * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, |
+ * and `data` is a sequnce 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 |
+ * to be converted to the corresponding string. |
+ * |
+ * Parsing doesn't check the validity of any part, it just checks that the |
+ * input has the correct structure with the correct sequence of `/`, `;`, `=` |
+ * 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. |
+ */ |
+ static UriData parse(String uri) { |
+ if (!uri.startsWith("data:")) { |
+ throw new FormatException("Does not start with 'data:'", uri, 0); |
+ } |
+ return _parse(uri, 5, null); |
+ } |
+ |
+ /** |
+ * The [Uri] that this `UriData` is giving access to. |
+ * |
+ * Returns a `Uri` with scheme `data` and the remainder of the data URI |
+ * as path. |
+ */ |
+ Uri get uri { |
+ if (_uriCache != null) return _uriCache; |
+ String path = _text; |
+ String query = null; |
+ int colonIndex = _separatorIndices[0]; |
+ int queryIndex = _text.indexOf('?', colonIndex + 1); |
+ int end = null; |
+ if (queryIndex >= 0) { |
+ query = _text.substring(queryIndex + 1); |
+ end = queryIndex; |
+ } |
+ path = _text.substring(colonIndex + 1, end); |
+ // TODO(lrn): This is probably too simple. We should ensure URI |
+ // normalization before passing in the raw strings, maybe using |
+ // Uri._makePath, Uri._makeQuery. |
+ _uriCache = new Uri._internal("data", "", null, null, path, query, null); |
+ return _uriCache; |
+ } |
+ |
+ /** |
+ * The MIME type of the data URI. |
+ * |
+ * A data URI consists of a "media type" followed by data. |
+ * The media type starts with a MIME type and can be followed by |
+ * extra parameters. |
+ * |
+ * Example: |
+ * |
+ * data:text/plain;charset=utf-8,Hello%20World! |
+ * |
+ * This data URI has the media type `text/plain;charset=utf-8`, which is the |
+ * MIME type `text/plain` with the parameter `charset` with value `utf-8`. |
+ * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. |
+ * |
+ * If the first part of the data URI is empty, it defaults to `text/plain`. |
+ */ |
+ String get mimeType { |
+ int start = _separatorIndices[0] + 1; |
+ int end = _separatorIndices[1]; |
+ if (start == end) return "text/plain"; |
+ return Uri._uriDecode(_text, start, end, UTF8, false); |
+ } |
+ |
+ /** |
+ * The charset parameter of the media type. |
+ * |
+ * If the parameters of the media type contains a `charset` parameter |
+ * then this returns its value, otherwise it returns `US-ASCII`, |
+ * which is the default charset for data URIs. |
+ */ |
+ String get charset { |
+ int parameterStart = 1; |
+ int parameterEnd = _separatorIndices.length - 1; // The ',' before data. |
+ if (isBase64) { |
+ // There is a ";base64" separator, so subtract one for that as well. |
+ parameterEnd -= 1; |
+ } |
+ for (int i = parameterStart; i < parameterEnd; i += 2) { |
+ 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 "US-ASCII"; |
+ } |
+ |
+ /** |
+ * Whether the data is Base64 encoded or not. |
+ */ |
+ bool get isBase64 => _separatorIndices.length.isOdd; |
+ |
+ /** |
+ * The content part of the data URI, as its actual representation. |
+ * |
+ * This string may contain percent escapes. |
+ */ |
+ String get contentText => _text.substring(_separatorIndices.last + 1); |
+ |
+ /** |
+ * The content part of the data URI as bytes. |
+ * |
+ * If the data is Base64 encoded, it will be decoded to bytes. |
+ * |
+ * If the data is not Base64 encoded, it will be decoded by unescaping |
+ * percent-escaped characters and returning byte values of each unescaped |
+ * character. The bytes will not be, e.g., UTF-8 decoded. |
+ */ |
+ List<int> contentAsBytes() { |
+ String text = _text; |
+ int start = _separatorIndices.last + 1; |
+ if (isBase64) { |
+ return BASE64.decoder.convert(text, start); |
+ } |
+ |
+ // Not base64, do percent-decoding and return the remaining bytes. |
+ // Compute result size. |
+ const int percent = 0x25; |
+ int length = text.length - start; |
+ for (int i = start; i < text.length; i++) { |
+ var codeUnit = text.codeUnitAt(i); |
+ if (codeUnit == percent) { |
+ i += 2; |
+ length -= 2; |
+ } |
+ } |
+ // Fill result array. |
+ Uint8List result = new Uint8List(length); |
+ if (length == text.length) { |
+ result.setRange(0, length, text.codeUnits, start); |
+ return result; |
+ } |
+ int index = 0; |
+ for (int i = start; i < text.length; i++) { |
+ var codeUnit = text.codeUnitAt(i); |
+ if (codeUnit != percent) { |
+ 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)); |
+ if (digit1 >= 0 && digit2 >= 0) { |
+ int byte = digit1 * 16 + digit2; |
+ result[index++] = byte; |
+ i += 2; |
+ continue; |
+ } |
+ } |
+ throw new FormatException("Invalid percent escape", text, i); |
+ } |
+ } |
+ assert(index == result.length); |
+ return result; |
+ } |
+ |
+ /** |
+ * Returns a string created from the content of the data URI. |
+ * |
+ * 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 |
+ * the [ASCII] encoding, which is the default encoding for data URIs |
+ * that do not specify an encoding. |
+ * |
+ * If the content is not Base64 encoded, it will first have percent-escapes |
+ * converted to bytes and then the character codes and byte values are |
+ * decoded using [encoding]. |
+ */ |
+ String contentAsString({Encoding encoding}) { |
+ if (encoding == null) { |
+ var charset = this.charset; // Returns "US-ASCII" if not present. |
+ encoding = Encoding.getByName(charset); |
+ if (encoding == null) { |
+ throw new UnsupportedError("Unknown charset: $charset"); |
+ } |
+ } |
+ String text = _text; |
+ int start = _separatorIndices.last + 1; |
+ if (isBase64) { |
+ var converter = BASE64.decoder.fuse(encoding.decoder); |
+ return converter.convert(text.substring(start)); |
+ } |
+ return Uri._uriDecode(text, start, text.length, encoding, false); |
+ } |
+ |
+ /** |
+ * A map representing the parameters of the media type. |
+ * |
+ * A data URI may contain parameters between the the MIME type and the |
+ * data. This converts these parameters to a map from parameter name |
+ * to parameter value. |
+ * The map only contains parameters that actually occur in the URI. |
+ * The `charset` parameter has a default value even if it doesn't occur |
+ * in the URI, which is reflected by the [charset] getter. This means that |
+ * [charset] may return a value even if `parameters["charset"]` is `null`. |
+ * |
+ * If the values contain non-ASCII values or percent escapes, they default |
+ * to being decoded as UTF-8. |
+ */ |
+ Map<String, String> get parameters { |
+ var result = <String, String>{}; |
+ for (int i = 3; i < _separatorIndices.length; i += 2) { |
+ 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); |
+ result[key] = value; |
+ } |
+ return result; |
+ } |
+ |
+ static UriData _parse(String text, int start, Uri sourceUri) { |
+ assert(start == 0 || start == 5); |
+ assert((start == 5) == text.startsWith("data:")); |
+ |
+ /// Character codes. |
+ const int comma = 0x2c; |
+ const int slash = 0x2f; |
+ const int semicolon = 0x3b; |
+ const int equals = 0x3d; |
+ List<int> indices = [start - 1]; |
+ int slashIndex = -1; |
+ var char; |
+ int i = start; |
+ for (; i < text.length; i++) { |
+ char = text.codeUnitAt(i); |
+ if (char == comma || char == semicolon) break; |
+ if (char == slash) { |
+ if (slashIndex < 0) { |
+ slashIndex = i; |
+ continue; |
+ } |
+ throw new FormatException("Invalid MIME type", text, i); |
+ } |
+ } |
+ if (slashIndex < 0 && i > start) { |
+ // An empty MIME type is allowed, but if non-empty it must contain |
+ // exactly one slash. |
+ throw new FormatException("Invalid MIME type", text, i); |
+ } |
+ while (char != comma) { |
+ // Parse parameters and/or "base64". |
+ indices.add(i); |
+ i++; |
+ int equalsIndex = -1; |
+ for (; i < text.length; i++) { |
+ char = text.codeUnitAt(i); |
+ if (char == equals) { |
+ if (equalsIndex < 0) equalsIndex = i; |
+ } else if (char == semicolon || char == comma) { |
+ break; |
+ } |
+ } |
+ if (equalsIndex >= 0) { |
+ indices.add(equalsIndex); |
+ } else { |
+ // Have to be final "base64". |
+ var lastSeparator = indices.last; |
+ if (char != comma || |
+ i != lastSeparator + 7 /* "base64,".length */ || |
+ !text.startsWith("base64", lastSeparator + 1)) { |
+ throw new FormatException("Expecting '='", text, i); |
+ } |
+ break; |
+ } |
+ } |
+ indices.add(i); |
+ return new UriData._(text, indices, sourceUri); |
+ } |
+ |
+ /** |
+ * Like [Uri._uriEncode] but takes the input as bytes, not a string. |
+ * |
+ * Encodes into [buffer] instead of creating its own buffer. |
+ */ |
+ static void _uriEncodeBytes(List<int> canonicalTable, |
+ List<int> bytes, |
+ StringSink buffer) { |
+ // Encode the string into bytes then generate an ASCII only string |
+ // by percent encoding selected bytes. |
+ int byteOr = 0; |
+ for (int i = 0; i < bytes.length; i++) { |
+ int byte = bytes[i]; |
+ byteOr |= byte; |
+ if (byte < 128 && |
+ ((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)); |
+ } |
+ } |
+ if ((byteOr & ~0xFF) != 0) { |
+ for (int i = 0; i < bytes.length; i++) { |
+ var byte = bytes[i]; |
+ if (byte < 0 || byte > 255) { |
+ throw new ArgumentError.value(byte, "non-byte value"); |
+ } |
+ } |
+ } |
+ } |
+ |
+ String toString() => |
+ (_separatorIndices[0] == _noScheme) ? "data:$_text" : _text; |
+ |
+ // Table of the `token` characters of RFC 2045 in a URI. |
+ // |
+ // A token is any US-ASCII character except SPACE, control characters and |
+ // `tspecial` characters. The `tspecial` category is: |
+ // '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='. |
+ // |
+ // In a data URI, we also need to escape '%' and '#' characters. |
+ static const _tokenCharTable = const [ |
+ // LSB MSB |
+ // | | |
+ 0x0000, // 0x00 - 0x0f 00000000 00000000 |
+ 0x0000, // 0x10 - 0x1f 00000000 00000000 |
+ // ! $ &' *+ -. |
+ 0x6cd2, // 0x20 - 0x2f 01001011 00110110 |
+ // 01234567 89 |
+ 0x03ff, // 0x30 - 0x3f 11111111 11000000 |
+ // ABCDEFG HIJKLMNO |
+ 0xfffe, // 0x40 - 0x4f 01111111 11111111 |
+ // PQRSTUVW XYZ ^_ |
+ 0xc7ff, // 0x50 - 0x5f 11111111 11100011 |
+ // `abcdefg hijklmno |
+ 0xffff, // 0x60 - 0x6f 11111111 11111111 |
+ // pqrstuvw xyz{|}~ |
+ 0x7fff]; // 0x70 - 0x7f 11111111 11111110 |
+ |
+ // All non-escape RFC-2396 uric characters. |
+ // |
+ // uric = reserved | unreserved | escaped |
+ // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," |
+ // unreserved = alphanum | mark |
+ // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" |
+ // |
+ // This is the same characters as in a URI query (which is URI pchar plus '?') |
+ static const _uricTable = Uri._queryCharTable; |
} |