Chromium Code Reviews| Index: sdk/lib/uri/uri.dart |
| diff --git a/sdk/lib/core/uri.dart b/sdk/lib/uri/uri.dart |
| similarity index 82% |
| rename from sdk/lib/core/uri.dart |
| rename to sdk/lib/uri/uri.dart |
| index 10aa827cb190c6e93a8889cad67312629145d5f5..664eb4202d28f74e53e1f54deda68bc1194a62a7 100644 |
| --- a/sdk/lib/core/uri.dart |
| +++ b/sdk/lib/uri/uri.dart |
| @@ -2,8 +2,16 @@ |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| -part of dart.core; |
| +/** |
| + * URI related classes and functionality. |
| + */ |
| +library dart.uri; |
| +import "dart:convert" show UTF8, LATIN1, BASE64, Encoding, |
| + ChunkedConversionSink, ByteConversionSink, |
| + StringConversionSink; |
| +import "dart:typed_data" show Uint8List; |
| +import "dart:collection" show UnmodifiableListView, UnmodifiableMapView; |
| /** |
| * A parsed URI, such as a URL. |
| * |
| @@ -16,8 +24,18 @@ part of dart.core; |
| * [libtour]: http://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. |
| + int _port; |
| + // The path. Always non-null. |
| + String _path; |
| + |
| /** |
| - * The scheme component of the URI. |
| + * Returns the scheme component. |
| * |
| * Returns the empty string if there is no scheme component. |
| * |
| @@ -29,213 +47,6 @@ class Uri { |
| 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; |
| - |
| - /// 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, 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` 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); |
| - } |
| - |
| - /** |
| * Returns the authority component. |
| * |
| * The authority is formatted from the [userInfo], [host] and [port] |
| @@ -251,6 +62,14 @@ 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 |
| @@ -307,6 +126,9 @@ 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]. |
| @@ -315,6 +137,9 @@ 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. |
| * |
| @@ -324,6 +149,16 @@ 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` |
| @@ -386,6 +221,9 @@ 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 = ""; |
| @@ -550,39 +388,197 @@ class Uri { |
| 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); |
| + 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); |
| + } |
| + |
| + /// 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, 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` 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); |
| } |
| - // Report a parse failure. |
| - static void _fail(String uri, int index, String message) { |
| - throw new FormatException(message, uri, index); |
| + /** |
| + * 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, |
| @@ -950,7 +946,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); |
| @@ -964,7 +960,7 @@ 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 = ""; |
| } |
| @@ -974,7 +970,7 @@ class Uri { |
| path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| scheme, hasAuthority); |
| } else { |
| - path = this._path; |
| + path = this.path; |
| if ((isFile || (hasAuthority && !path.isEmpty)) && |
| !path.startsWith('/')) { |
| path = "/" + path; |
| @@ -983,14 +979,14 @@ class Uri { |
| if (query != null || queryParameters != null) { |
| query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| - } else { |
| - query = this._query; |
| + } else if (this.hasQuery) { |
| + query = this.query; |
| } |
| if (fragment != null) { |
| fragment = _makeFragment(fragment, 0, fragment.length); |
| - } else { |
| - fragment = this._fragment; |
| + } else if (this.hasFragment) { |
| + fragment = this.fragment; |
| } |
| return new Uri._internal( |
| @@ -1004,8 +1000,7 @@ class Uri { |
| */ |
| Uri removeFragment() { |
| if (!this.hasFragment) return this; |
| - return new Uri._internal(scheme, _userInfo, _host, _port, |
| - _path, _query, null); |
| + return new Uri._internal(scheme, userInfo, host, port, path, query, null); |
| } |
| /** |
| @@ -2322,23 +2317,40 @@ class Uri { |
| */ |
| static String _uriDecode(String text, |
| {bool plusToSpace: false, |
| - Encoding encoding: UTF8}) { |
| + Encoding encoding: UTF8, |
| + int start: 0, |
| + int end}) { |
| + if (end == null) end = text.length; |
| // 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 && simple; i++) { |
| var codeUnit = text.codeUnitAt(i); |
| simple = codeUnit != _PERCENT && codeUnit != _PLUS; |
| } |
| List<int> bytes; |
| if (simple) { |
| if (encoding == UTF8 || encoding == LATIN1) { |
| - return text; |
| - } else { |
| + return text.substring(start, end); |
| + } else if (start == 0 && end == text.length) { |
| bytes = text.codeUnits; |
| + } else { |
| + var decoder = encoding.decoder; |
| + var result; |
| + var conversionSink = decoder.startChunkedConversion( |
| + new ChunkedConversionSink.withCallback((list) { |
| + result = list.join(); |
| + })); |
| + if (conversionSink is ByteConversionSink) { |
| + conversionSink.addSlice(text.codeUnits, start, end, true); |
| + } else { |
| + conversionSink.add(text.codeUnits.sublist(start, end)); |
| + conversionSink.close(); |
| + } |
| + return result; |
| } |
| } 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"); |
| @@ -2610,4 +2622,518 @@ class Uri { |
| 0xfffe, // 0x60 - 0x6f 0111111111111111 |
| // pqrstuvwxyz ~ |
| 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
| + |
| +} |
| + |
| +// -------------------------------------------------------------------- |
| +// Data URI |
| +// -------------------------------------------------------------------- |
| + |
| +/** |
| + * A representation of a `data:` URI. |
| + * |
| + * Data URIs are non-hierarchial URIs that contain can contain any 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 DataUri { |
| + 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 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= |
| + * ^ ^ ^ ^ ^ |
| + * |
| + */ |
| + List<int> _separatorIndices; |
| + |
| + DataUri._(this._text, |
| + this._separatorIndices); |
| + |
| + /** The entire content of the data URI, including the leading `data:`. */ |
|
Lasse Reichstein Nielsen
2015/10/28 13:55:47
This getter is identical to toString. Should I jus
|
| + String get text => _separatorIndices[0] == _noScheme ? "data:$_text" : _text; |
| + |
| + /** |
| + * Creates a `data:` URI containing the contents as percent-encoded text. |
| + */ |
| + factory DataUri.fromString(String content, |
| + {mimeType: "text/plain", |
| + Iterable<DataUriParameter> parameters}) { |
| + StringBuffer buffer = new StringBuffer(); |
| + List indices = [_noScheme]; |
| + _writeUri(mimeType, parameters, buffer, indices); |
| + indices.add(buffer.length); |
| + buffer.write(','); |
| + buffer.write(Uri.encodeComponent(content)); |
| + return new DataUri._(buffer.toString(), indices); |
| + } |
| + |
| + /** |
| + * Creates a `data:` URI string containing the base-64 encoded content bytes. |
| + * |
| + * It defaults to having the mime-type `application/octet-stream`. |
| + */ |
| + factory DataUri.fromBytes(List<int> bytes, |
| + {mimeType: "application/octet-stream", |
| + Iterable<DataUriParameter> parameters}) { |
| + StringBuffer buffer = new StringBuffer(); |
| + List indices = [_noScheme]; |
| + _writeUri(mimeType, parameters, buffer, indices); |
| + indices.add(buffer.length); |
| + buffer.write(';base64,'); |
| + indices.add(buffer.length - 1); |
| + BASE64.encoder.startChunkedConversion( |
| + new StringConversionSink.fromStringSink(buffer)) |
| + .addSlice(bytes, 0, bytes.length, true); |
| + return new DataUri._(buffer.toString(), indices); |
| + } |
| + |
| + /** |
| + * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. |
| + * |
| + * The [uri] must have scheme `data` and no authority, query or fragment, |
| + * and the path must be valid as a data URI. |
| + */ |
| + factory DataUri.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.hasQuery) { |
| + throw new ArgumentError.value(uri, "uri", |
| + "Data uri must not have a query part"); |
| + } |
| + if (uri.hasFragment) { |
| + throw new ArgumentError.value(uri, "uri", |
| + "Data uri must not have a fragment part"); |
| + } |
| + return _parse(uri.path, 0); |
| + } |
| + |
| + /** |
| + * 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, |
| + Iterable<DataUriParameter> parameters, |
| + StringBuffer buffer, List indices) { |
| + if (mimeType == null) { |
| + mimeType = "text/plain"; |
| + } |
| + if (mimeType.isEmpty || |
| + identical(mimeType, "text/plain") || |
| + 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))); |
| + buffer.write("/"); |
| + buffer.write(Uri._uriEncode(_tokenCharTable, |
| + mimeType.substring(slashIndex + 1))); |
| + } |
| + if (parameters != null) { |
| + for (var parameter in parameters) { |
| + if (indices != null) indices.add(buffer.length); |
| + buffer.write(';'); |
| + // Encode any non-RFC2045-token character as well as '%' and '#'. |
| + buffer.write(Uri._uriEncode(_tokenCharTable, parameter.key)); |
| + if (indices != null) indices.add(buffer.length); |
| + buffer.write('='); |
| + buffer.write(Uri._uriEncode(_tokenCharTable, parameter.value)); |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * 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; |
| + } |
| + |
| + /** |
| + * Creates a [Uri] with the content of [DataUri.fromString]. |
| + * |
| + * The resulting URI will have `data` as scheme and the remainder |
| + * of the data URI as path. |
| + * |
| + * Equivalent to creating a `DataUri` using `new DataUri.fromString` and |
| + * calling `toUri` on the result. |
| + */ |
| + static Uri uriFromString(String content, |
| + {mimeType: "text/plain", |
| + Iterable<DataUriParameter> parameters}) { |
| + var buffer = new StringBuffer(); |
| + _writeUri(mimeType, parameters, buffer, null); |
| + buffer.write(','); |
| + buffer.write(Uri.encodeComponent(content)); |
| + return new Uri(scheme: "data", path: buffer.toString()); |
| + } |
| + |
| + /** |
| + * Creates a [Uri] with the content of [DataUri.fromBytes]. |
| + * |
| + * The resulting URI will have `data` as scheme and the remainder |
| + * of the data URI as path. |
| + * |
| + * Equivalent to creating a `DataUri` using `new DataUri.fromBytes` and |
| + * calling `toUri` on the result. |
| + */ |
| + static Uri uriFromBytes(List<int> bytes, |
| + {mimeType: "text/plain", |
| + Iterable<DataUriParameter> parameters}) { |
| + var buffer = new StringBuffer(); |
| + _writeUri(mimeType, parameters, buffer, null); |
| + buffer.write(';base64,'); |
| + BASE64.encoder.startChunkedConversion(buffer) |
| + .addSlice(bytes, 0, bytes.length, true); |
| + return new Uri(scheme: "data", path: buffer.toString()); |
| + } |
| + |
| + /** |
| + * Parses a string as a `data` URI. |
| + */ |
| + static DataUri parse(String uri) { |
| + if (!uri.startsWith("data:")) { |
| + throw new FormatException("Does not start with 'data:'", uri, 0); |
| + } |
| + return _parse(uri, 5); |
| + } |
| + |
| + /** |
| + * Converts a `DataUri` to a [Uri]. |
| + * |
| + * Returns a `Uri` with scheme `data` and the remainder of the data URI |
| + * as path. |
| + */ |
| + Uri toUri() { |
| + String content = _text; |
| + int colonIndex = _separatorIndices[0]; |
| + if (colonIndex >= 0) { |
| + content = _text.substring(colonIndex + 1); |
| + } |
| + return new Uri._internal("data", null, null, null, content, null, null); |
| + } |
| + |
| + /** |
| + * The MIME type of the data URI. |
| + * |
| + * A data URI consists of a "media type" followed by data. |
| + * The mediatype starts with a MIME type and can be followed by |
| + * extra parameters. |
| + * |
| + * Example: |
| + * |
| + * data:text/plain;encoding=utf-8,Hello%20World! |
| + * |
| + * This data URI has the media type `text/plain;encoding=utf-8`, which is the |
| + * MIME type `text/plain` with the parameter `encoding` 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: start, end: end); |
| + } |
| + |
| + /** |
| + * 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() { |
|
Lasse Reichstein Nielsen
2015/10/28 13:55:47
This sounds like it should be a getter, but the co
|
| + String text = _text; |
| + int start = _separatorIndices.last + 1; |
| + if (isBase64) { |
| + if (text.endsWith("%3D")) { |
| + return BASE64.decode(Uri._uriDecode(text, start: start, |
| + encoding: LATIN1)); |
| + } |
| + return BASE64.decode(text.substring(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 = _hexDigit(text.codeUnitAt(i + 1)); |
| + var digit2 = _hexDigit(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; |
| + } |
| + |
| + // Converts a UTF-16 code-unit to its value as a hex digit. |
| + // Returns -1 for non-hex digits. |
| + int _hexDigit(int char) { |
| + const int char_0 = 0x30; |
| + const int char_a = 0x61; |
| + |
| + int digit = char ^ char_0; |
| + if (digit <= 9) return digit; |
| + char = ((char | 0x20) - char_a) & 0xFFFF; |
| + if (char < 6) return 10 + char; |
| + return -1; |
| + } |
| + |
| + /** |
| + * 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 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: UTF8}) { |
| + String text = _text; |
| + int start = _separatorIndices.last + 1; |
| + if (isBase64) { |
| + var converter = BASE64.decoder.fuse(encoding.decoder); |
| + if (text.endsWith("%3D")) { |
| + return converter.convert(Uri._uriDecode(text, start: start, |
| + encoding: LATIN1)); |
| + } |
| + return converter.convert(text.substring(start)); |
| + } |
| + return Uri._uriDecode(text, start: start, encoding: encoding); |
| + } |
| + |
| + /** |
| + * An iterable over the parameters of the data URI. |
| + * |
| + * A data URI may contain parameters between the the MIMI type and the |
| + * data. This iterates through those parameters, returning each as a |
| + * [DataUriParameter] pair of key and value. |
| + */ |
| + Iterable<DataUriParameter> get parameters sync* { |
| + 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: start, end: equals); |
| + String value = Uri._uriDecode(_text, start: equals + 1, end: end); |
| + yield new DataUriParameter(key, value); |
| + } |
| + } |
| + |
| + static DataUri _parse(String text, int start) { |
| + 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 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 DataUri._(text, indices); |
| + } |
| + |
| + String toString() => 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 |
| +} |
| + |
| +/** |
| + * A parameter of a data URI. |
| + * |
| + * A parameter is a key and a value. |
| + * |
| + * The key and value are the actual values to be encoded into the URI. |
| + * They will be escaped if necessary when creating a data URI, |
| + * and have been unescaped when extracted from a data URI. |
| + */ |
| +class DataUriParameter { |
| + /** Parameter key. */ |
| + final String key; |
| + /** Parameter value. */ |
| + final String value; |
| + DataUriParameter(this.key, this.value); |
| + |
| + /** |
| + * Creates an iterable of parameters from a map from key to value. |
| + * |
| + * Parameter keys are not required to be unique in a data URI, but |
| + * when they are, a map can be used to represent the parameters, and |
| + * this function provides a way to access the map pairs as parameter |
| + * values. |
| + */ |
| + static Iterable<DataUriParameter> fromMap(Map<String, String> headers) sync* { |
| + for (String key in headers.keys) { |
| + yield new DataUriParameter(key, headers[key]); |
| + } |
| + } |
| } |