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

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

Issue 1381033002: Add data-URI support class to dart:core (next to Uri). (Closed) Base URL: https://github.com/dart-lang/sdk.git@master
Patch Set: Added library now working (thanks fschneider). Back to the main content. Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sdk/lib/core/uri.dart ('k') | sdk/lib/uri/uri_sources.gypi » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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]);
+ }
+ }
}
« no previous file with comments | « sdk/lib/core/uri.dart ('k') | sdk/lib/uri/uri_sources.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698