Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of dart.core; | 5 /** |
| 6 * URI related classes and functionality. | |
| 7 */ | |
| 8 library dart.uri; | |
| 6 | 9 |
| 10 import "dart:convert" show UTF8, LATIN1, BASE64, Encoding, | |
| 11 ChunkedConversionSink, ByteConversionSink, | |
| 12 StringConversionSink; | |
| 13 import "dart:typed_data" show Uint8List; | |
| 14 import "dart:collection" show UnmodifiableListView, UnmodifiableMapView; | |
| 7 /** | 15 /** |
| 8 * A parsed URI, such as a URL. | 16 * A parsed URI, such as a URL. |
| 9 * | 17 * |
| 10 * **See also:** | 18 * **See also:** |
| 11 * | 19 * |
| 12 * * [URIs][uris] in the [library tour][libtour] | 20 * * [URIs][uris] in the [library tour][libtour] |
| 13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) | 21 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) |
| 14 * | 22 * |
| 15 * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#c h03-uri | 23 * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#c h03-uri |
| 16 * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.htm l | 24 * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.htm l |
| 17 */ | 25 */ |
| 18 class Uri { | 26 class Uri { |
| 27 // The host name of the URI. | |
| 28 // Set to `null` if there is no authority in a URI. | |
| 29 final String _host; | |
| 30 // The port. Set to null if there is no port. Normalized to null if | |
| 31 // the port is the default port for the scheme. | |
| 32 // Set to the value of the default port if an empty port was supplied. | |
| 33 int _port; | |
| 34 // The path. Always non-null. | |
| 35 String _path; | |
| 36 | |
| 19 /** | 37 /** |
| 20 * The scheme component of the URI. | 38 * Returns the scheme component. |
| 21 * | 39 * |
| 22 * Returns the empty string if there is no scheme component. | 40 * Returns the empty string if there is no scheme component. |
| 23 * | 41 * |
| 24 * A URI scheme is case insensitive. | 42 * A URI scheme is case insensitive. |
| 25 * The returned scheme is canonicalized to lowercase letters. | 43 * The returned scheme is canonicalized to lowercase letters. |
| 26 */ | 44 */ |
| 27 // We represent the missing scheme as an empty string. | 45 // We represent the missing scheme as an empty string. |
| 28 // A valid scheme cannot be empty. | 46 // A valid scheme cannot be empty. |
| 29 final String scheme; | 47 final String scheme; |
| 30 | 48 |
| 31 /** | 49 /** |
| 32 * The user-info part of the authority. | |
| 33 * | |
| 34 * Does not distinguish between an empty user-info and an absent one. | |
| 35 * The value is always non-null. | |
| 36 * Is considered absent if [_host] is `null`. | |
| 37 */ | |
| 38 final String _userInfo; | |
| 39 | |
| 40 /** | |
| 41 * The host name of the URI. | |
| 42 * | |
| 43 * Set to `null` if there is no authority in the URI. | |
| 44 * The host name is the only mandatory part of an authority, so we use | |
| 45 * it to mark whether an authority part was present or not. | |
| 46 */ | |
| 47 final String _host; | |
| 48 | |
| 49 /** | |
| 50 * The port number part of the authority. | |
| 51 * | |
| 52 * The port. Set to null if there is no port. Normalized to null if | |
| 53 * the port is the default port for the scheme. | |
| 54 */ | |
| 55 int _port; | |
| 56 | |
| 57 /** | |
| 58 * The path of the URI. | |
| 59 * | |
| 60 * Always non-null. | |
| 61 */ | |
| 62 String _path; | |
| 63 | |
| 64 // The query content, or null if there is no query. | |
| 65 final String _query; | |
| 66 | |
| 67 // The fragment content, or null if there is no fragment. | |
| 68 final String _fragment; | |
| 69 | |
| 70 /** | |
| 71 * Cache the computed return value of [pathSegements]. | |
| 72 */ | |
| 73 List<String> _pathSegments; | |
| 74 | |
| 75 /** | |
| 76 * Cache the computed return value of [queryParameters]. | |
| 77 */ | |
| 78 Map<String, String> _queryParameters; | |
| 79 | |
| 80 /// Internal non-verifying constructor. Only call with validated arguments. | |
| 81 Uri._internal(this.scheme, | |
| 82 this._userInfo, | |
| 83 this._host, | |
| 84 this._port, | |
| 85 this._path, | |
| 86 this._query, | |
| 87 this._fragment); | |
| 88 | |
| 89 /** | |
| 90 * Creates a new URI from its components. | |
| 91 * | |
| 92 * Each component is set through a named argument. Any number of | |
| 93 * components can be provided. The [path] and [query] components can be set | |
| 94 * using either of two different named arguments. | |
| 95 * | |
| 96 * The scheme component is set through [scheme]. The scheme is | |
| 97 * normalized to all lowercase letters. If the scheme is omitted or empty, | |
| 98 * the URI will not have a scheme part. | |
| 99 * | |
| 100 * The user info part of the authority component is set through | |
| 101 * [userInfo]. It defaults to the empty string, which will be omitted | |
| 102 * from the string representation of the URI. | |
| 103 * | |
| 104 * The host part of the authority component is set through | |
| 105 * [host]. The host can either be a hostname, an IPv4 address or an | |
| 106 * IPv6 address, contained in '[' and ']'. If the host contains a | |
| 107 * ':' character, the '[' and ']' are added if not already provided. | |
| 108 * The host is normalized to all lowercase letters. | |
| 109 * | |
| 110 * The port part of the authority component is set through | |
| 111 * [port]. | |
| 112 * If [port] is omitted or `null`, it implies the default port for | |
| 113 * the URI's scheme, and is equivalent to passing that port explicitly. | |
| 114 * The recognized schemes, and their default ports, are "http" (80) and | |
| 115 * "https" (443). All other schemes are considered as having zero as the | |
| 116 * default port. | |
| 117 * | |
| 118 * If any of `userInfo`, `host` or `port` are provided, | |
| 119 * the URI will have an autority according to [hasAuthority]. | |
| 120 * | |
| 121 * The path component is set through either [path] or | |
| 122 * [pathSegments]. When [path] is used, it should be a valid URI path, | |
| 123 * but invalid characters, except the general delimiters ':/@[]?#', | |
| 124 * will be escaped if necessary. | |
| 125 * When [pathSegments] is used, each of the provided segments | |
| 126 * is first percent-encoded and then joined using the forward slash | |
| 127 * separator. The percent-encoding of the path segments encodes all | |
| 128 * characters except for the unreserved characters and the following | |
| 129 * list of characters: `!$&'()*+,;=:@`. If the other components | |
| 130 * calls for an absolute path a leading slash `/` is prepended if | |
| 131 * not already there. | |
| 132 * | |
| 133 * The query component is set through either [query] or | |
| 134 * [queryParameters]. When [query] is used the provided string should | |
| 135 * be a valid URI query, but invalid characters other than general delimiters, | |
| 136 * will be escaped if necessary. | |
| 137 * When [queryParameters] is used the query is built from the | |
| 138 * provided map. Each key and value in the map is percent-encoded | |
| 139 * and joined using equal and ampersand characters. The | |
| 140 * percent-encoding of the keys and values encodes all characters | |
| 141 * except for the unreserved characters. | |
| 142 * If `query` is the empty string, it is equivalent to omitting it. | |
| 143 * To have an actual empty query part, | |
| 144 * use an empty list for `queryParameters`. | |
| 145 * If both `query` and `queryParameters` are omitted or `null`, the | |
| 146 * URI will have no query part. | |
| 147 * | |
| 148 * The fragment component is set through [fragment]. | |
| 149 * It should be a valid URI fragment, but invalid characters other than | |
| 150 * general delimiters, will be escaped if necessary. | |
| 151 * If `fragment` is omitted or `null`, the URI will have no fragment part. | |
| 152 */ | |
| 153 factory Uri({String scheme : "", | |
| 154 String userInfo : "", | |
| 155 String host, | |
| 156 int port, | |
| 157 String path, | |
| 158 Iterable<String> pathSegments, | |
| 159 String query, | |
| 160 Map<String, String> queryParameters, | |
| 161 String fragment}) { | |
| 162 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | |
| 163 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
| 164 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
| 165 // Special case this constructor for backwards compatibility. | |
| 166 if (query == "") query = null; | |
| 167 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
| 168 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
| 169 port = _makePort(port, scheme); | |
| 170 bool isFile = (scheme == "file"); | |
| 171 if (host == null && | |
| 172 (userInfo.isNotEmpty || port != null || isFile)) { | |
| 173 host = ""; | |
| 174 } | |
| 175 bool hasAuthority = (host != null); | |
| 176 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
| 177 scheme, hasAuthority); | |
| 178 if (scheme.isEmpty && host == null && !path.startsWith('/')) { | |
| 179 path = _normalizeRelativePath(path); | |
| 180 } else { | |
| 181 path = _removeDotSegments(path); | |
| 182 } | |
| 183 return new Uri._internal(scheme, userInfo, host, port, | |
| 184 path, query, fragment); | |
| 185 } | |
| 186 | |
| 187 /** | |
| 188 * Creates a new `http` URI from authority, path and query. | |
| 189 * | |
| 190 * Examples: | |
| 191 * | |
| 192 * ``` | |
| 193 * // http://example.org/path?q=dart. | |
| 194 * new Uri.http("google.com", "/search", { "q" : "dart" }); | |
| 195 * | |
| 196 * // http://user:pass@localhost:8080 | |
| 197 * new Uri.http("user:pass@localhost:8080", ""); | |
| 198 * | |
| 199 * // http://example.org/a%20b | |
| 200 * new Uri.http("example.org", "a b"); | |
| 201 * | |
| 202 * // http://example.org/a%252F | |
| 203 * new Uri.http("example.org", "/a%2F"); | |
| 204 * ``` | |
| 205 * | |
| 206 * The `scheme` is always set to `http`. | |
| 207 * | |
| 208 * The `userInfo`, `host` and `port` components are set from the | |
| 209 * [authority] argument. If `authority` is `null` or empty, | |
| 210 * the created `Uri` will have no authority, and will not be directly usable | |
| 211 * as an HTTP URL, which must have a non-empty host. | |
| 212 * | |
| 213 * The `path` component is set from the [unencodedPath] | |
| 214 * argument. The path passed must not be encoded as this constructor | |
| 215 * encodes the path. | |
| 216 * | |
| 217 * The `query` component is set from the optional [queryParameters] | |
| 218 * argument. | |
| 219 */ | |
| 220 factory Uri.http(String authority, | |
| 221 String unencodedPath, | |
| 222 [Map<String, String> queryParameters]) { | |
| 223 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
| 224 } | |
| 225 | |
| 226 /** | |
| 227 * Creates a new `https` URI from authority, path and query. | |
| 228 * | |
| 229 * This constructor is the same as [Uri.http] except for the scheme | |
| 230 * which is set to `https`. | |
| 231 */ | |
| 232 factory Uri.https(String authority, | |
| 233 String unencodedPath, | |
| 234 [Map<String, String> queryParameters]) { | |
| 235 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | |
| 236 } | |
| 237 | |
| 238 /** | |
| 239 * Returns the authority component. | 50 * Returns the authority component. |
| 240 * | 51 * |
| 241 * The authority is formatted from the [userInfo], [host] and [port] | 52 * The authority is formatted from the [userInfo], [host] and [port] |
| 242 * parts. | 53 * parts. |
| 243 * | 54 * |
| 244 * Returns the empty string if there is no authority component. | 55 * Returns the empty string if there is no authority component. |
| 245 */ | 56 */ |
| 246 String get authority { | 57 String get authority { |
| 247 if (!hasAuthority) return ""; | 58 if (!hasAuthority) return ""; |
| 248 var sb = new StringBuffer(); | 59 var sb = new StringBuffer(); |
| 249 _writeAuthority(sb); | 60 _writeAuthority(sb); |
| 250 return sb.toString(); | 61 return sb.toString(); |
| 251 } | 62 } |
| 252 | 63 |
| 253 /** | 64 /** |
| 65 * The user-info part of the authority. | |
| 66 * | |
| 67 * Does not distinguish between an empty user-info and an absent one. | |
| 68 * The value is always non-null. | |
| 69 */ | |
| 70 final String _userInfo; | |
| 71 | |
| 72 /** | |
| 254 * Returns the user info part of the authority component. | 73 * Returns the user info part of the authority component. |
| 255 * | 74 * |
| 256 * Returns the empty string if there is no user info in the | 75 * Returns the empty string if there is no user info in the |
| 257 * authority component. | 76 * authority component. |
| 258 */ | 77 */ |
| 259 String get userInfo => _userInfo; | 78 String get userInfo => _userInfo; |
| 260 | 79 |
| 261 /** | 80 /** |
| 262 * Returns the host part of the authority component. | 81 * Returns the host part of the authority component. |
| 263 * | 82 * |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 300 /** | 119 /** |
| 301 * Returns the path component. | 120 * Returns the path component. |
| 302 * | 121 * |
| 303 * The returned path is encoded. To get direct access to the decoded | 122 * The returned path is encoded. To get direct access to the decoded |
| 304 * path use [pathSegments]. | 123 * path use [pathSegments]. |
| 305 * | 124 * |
| 306 * Returns the empty string if there is no path component. | 125 * Returns the empty string if there is no path component. |
| 307 */ | 126 */ |
| 308 String get path => _path; | 127 String get path => _path; |
| 309 | 128 |
| 129 // The query content, or null if there is no query. | |
| 130 final String _query; | |
| 131 | |
| 310 /** | 132 /** |
| 311 * Returns the query component. The returned query is encoded. To get | 133 * Returns the query component. The returned query is encoded. To get |
| 312 * direct access to the decoded query use [queryParameters]. | 134 * direct access to the decoded query use [queryParameters]. |
| 313 * | 135 * |
| 314 * Returns the empty string if there is no query component. | 136 * Returns the empty string if there is no query component. |
| 315 */ | 137 */ |
| 316 String get query => (_query == null) ? "" : _query; | 138 String get query => (_query == null) ? "" : _query; |
| 317 | 139 |
| 140 // The fragment content, or null if there is no fragment. | |
| 141 final String _fragment; | |
| 142 | |
| 318 /** | 143 /** |
| 319 * Returns the fragment identifier component. | 144 * Returns the fragment identifier component. |
| 320 * | 145 * |
| 321 * Returns the empty string if there is no fragment identifier | 146 * Returns the empty string if there is no fragment identifier |
| 322 * component. | 147 * component. |
| 323 */ | 148 */ |
| 324 String get fragment => (_fragment == null) ? "" : _fragment; | 149 String get fragment => (_fragment == null) ? "" : _fragment; |
| 325 | 150 |
| 326 /** | 151 /** |
| 152 * Cache the computed return value of [pathSegements]. | |
| 153 */ | |
| 154 List<String> _pathSegments; | |
| 155 | |
| 156 /** | |
| 157 * Cache the computed return value of [queryParameters]. | |
| 158 */ | |
| 159 Map<String, String> _queryParameters; | |
| 160 | |
| 161 /** | |
| 327 * Creates a new `Uri` object by parsing a URI string. | 162 * Creates a new `Uri` object by parsing a URI string. |
| 328 * | 163 * |
| 329 * If [start] and [end] are provided, only the substring from `start` | 164 * If [start] and [end] are provided, only the substring from `start` |
| 330 * to `end` is parsed as a URI. | 165 * to `end` is parsed as a URI. |
| 331 * | 166 * |
| 332 * If the string is not valid as a URI or URI reference, | 167 * If the string is not valid as a URI or URI reference, |
| 333 * a [FormatException] is thrown. | 168 * a [FormatException] is thrown. |
| 334 */ | 169 */ |
| 335 static Uri parse(String uri, [int start = 0, int end]) { | 170 static Uri parse(String uri, [int start = 0, int end]) { |
| 336 // This parsing will not validate percent-encoding, IPv6, etc. | 171 // This parsing will not validate percent-encoding, IPv6, etc. |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 379 // segment = *pchar | 214 // segment = *pchar |
| 380 // segment-nz = 1*pchar | 215 // segment-nz = 1*pchar |
| 381 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 216 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
| 382 // ; non-zero-length segment without any colon ":" | 217 // ; non-zero-length segment without any colon ":" |
| 383 // | 218 // |
| 384 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 219 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
| 385 // | 220 // |
| 386 // query = *( pchar / "/" / "?" ) | 221 // query = *( pchar / "/" / "?" ) |
| 387 // | 222 // |
| 388 // fragment = *( pchar / "/" / "?" ) | 223 // fragment = *( pchar / "/" / "?" ) |
| 224 bool isRegName(int ch) { | |
| 225 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | |
| 226 } | |
| 389 const int EOI = -1; | 227 const int EOI = -1; |
| 390 | 228 |
| 391 String scheme = ""; | 229 String scheme = ""; |
| 392 String userinfo = ""; | 230 String userinfo = ""; |
| 393 String host = null; | 231 String host = null; |
| 394 int port = null; | 232 int port = null; |
| 395 String path = null; | 233 String path = null; |
| 396 String query = null; | 234 String query = null; |
| 397 String fragment = null; | 235 String fragment = null; |
| 398 if (end == null) end = uri.length; | 236 if (end == null) end = uri.length; |
| (...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 578 path, | 416 path, |
| 579 query, | 417 query, |
| 580 fragment); | 418 fragment); |
| 581 } | 419 } |
| 582 | 420 |
| 583 // Report a parse failure. | 421 // Report a parse failure. |
| 584 static void _fail(String uri, int index, String message) { | 422 static void _fail(String uri, int index, String message) { |
| 585 throw new FormatException(message, uri, index); | 423 throw new FormatException(message, uri, index); |
| 586 } | 424 } |
| 587 | 425 |
| 426 /// Internal non-verifying constructor. Only call with validated arguments. | |
| 427 Uri._internal(this.scheme, | |
| 428 this._userInfo, | |
| 429 this._host, | |
| 430 this._port, | |
| 431 this._path, | |
| 432 this._query, | |
| 433 this._fragment); | |
| 434 | |
| 435 /** | |
| 436 * Creates a new URI from its components. | |
| 437 * | |
| 438 * Each component is set through a named argument. Any number of | |
| 439 * components can be provided. The [path] and [query] components can be set | |
| 440 * using either of two different named arguments. | |
| 441 * | |
| 442 * The scheme component is set through [scheme]. The scheme is | |
| 443 * normalized to all lowercase letters. If the scheme is omitted or empty, | |
| 444 * the URI will not have a scheme part. | |
| 445 * | |
| 446 * The user info part of the authority component is set through | |
| 447 * [userInfo]. It defaults to the empty string, which will be omitted | |
| 448 * from the string representation of the URI. | |
| 449 * | |
| 450 * The host part of the authority component is set through | |
| 451 * [host]. The host can either be a hostname, an IPv4 address or an | |
| 452 * IPv6 address, contained in '[' and ']'. If the host contains a | |
| 453 * ':' character, the '[' and ']' are added if not already provided. | |
| 454 * The host is normalized to all lowercase letters. | |
| 455 * | |
| 456 * The port part of the authority component is set through | |
| 457 * [port]. | |
| 458 * If [port] is omitted or `null`, it implies the default port for | |
| 459 * the URI's scheme, and is equivalent to passing that port explicitly. | |
| 460 * The recognized schemes, and their default ports, are "http" (80) and | |
| 461 * "https" (443). All other schemes are considered as having zero as the | |
| 462 * default port. | |
| 463 * | |
| 464 * If any of `userInfo`, `host` or `port` are provided, | |
| 465 * the URI will have an autority according to [hasAuthority]. | |
| 466 * | |
| 467 * The path component is set through either [path] or | |
| 468 * [pathSegments]. When [path] is used, it should be a valid URI path, | |
| 469 * but invalid characters, except the general delimiters ':/@[]?#', | |
| 470 * will be escaped if necessary. | |
| 471 * When [pathSegments] is used, each of the provided segments | |
| 472 * is first percent-encoded and then joined using the forward slash | |
| 473 * separator. The percent-encoding of the path segments encodes all | |
| 474 * characters except for the unreserved characters and the following | |
| 475 * list of characters: `!$&'()*+,;=:@`. If the other components | |
| 476 * calls for an absolute path a leading slash `/` is prepended if | |
| 477 * not already there. | |
| 478 * | |
| 479 * The query component is set through either [query] or | |
| 480 * [queryParameters]. When [query] is used the provided string should | |
| 481 * be a valid URI query, but invalid characters other than general delimiters, | |
| 482 * will be escaped if necessary. | |
| 483 * When [queryParameters] is used the query is built from the | |
| 484 * provided map. Each key and value in the map is percent-encoded | |
| 485 * and joined using equal and ampersand characters. The | |
| 486 * percent-encoding of the keys and values encodes all characters | |
| 487 * except for the unreserved characters. | |
| 488 * If `query` is the empty string, it is equivalent to omitting it. | |
| 489 * To have an actual empty query part, | |
| 490 * use an empty list for `queryParameters`. | |
| 491 * If both `query` and `queryParameters` are omitted or `null`, the | |
| 492 * URI will have no query part. | |
| 493 * | |
| 494 * The fragment component is set through [fragment]. | |
| 495 * It should be a valid URI fragment, but invalid characters other than | |
| 496 * general delimiters, will be escaped if necessary. | |
| 497 * If `fragment` is omitted or `null`, the URI will have no fragment part. | |
| 498 */ | |
| 499 factory Uri({String scheme : "", | |
| 500 String userInfo : "", | |
| 501 String host, | |
| 502 int port, | |
| 503 String path, | |
| 504 Iterable<String> pathSegments, | |
| 505 String query, | |
| 506 Map<String, String> queryParameters, | |
| 507 String fragment}) { | |
| 508 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | |
| 509 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
| 510 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
| 511 // Special case this constructor for backwards compatibility. | |
| 512 if (query == "") query = null; | |
| 513 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
| 514 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
| 515 port = _makePort(port, scheme); | |
| 516 bool isFile = (scheme == "file"); | |
| 517 if (host == null && | |
| 518 (userInfo.isNotEmpty || port != null || isFile)) { | |
| 519 host = ""; | |
| 520 } | |
| 521 bool hasAuthority = (host != null); | |
| 522 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
| 523 scheme, hasAuthority); | |
| 524 if (scheme.isEmpty && host == null && !path.startsWith('/')) { | |
| 525 path = _normalizeRelativePath(path); | |
| 526 } else { | |
| 527 path = _removeDotSegments(path); | |
| 528 } | |
| 529 return new Uri._internal(scheme, userInfo, host, port, | |
| 530 path, query, fragment); | |
| 531 } | |
| 532 | |
| 533 /** | |
| 534 * Creates a new `http` URI from authority, path and query. | |
| 535 * | |
| 536 * Examples: | |
| 537 * | |
| 538 * ``` | |
| 539 * // http://example.org/path?q=dart. | |
| 540 * new Uri.http("google.com", "/search", { "q" : "dart" }); | |
| 541 * | |
| 542 * // http://user:pass@localhost:8080 | |
| 543 * new Uri.http("user:pass@localhost:8080", ""); | |
| 544 * | |
| 545 * // http://example.org/a%20b | |
| 546 * new Uri.http("example.org", "a b"); | |
| 547 * | |
| 548 * // http://example.org/a%252F | |
| 549 * new Uri.http("example.org", "/a%2F"); | |
| 550 * ``` | |
| 551 * | |
| 552 * The `scheme` is always set to `http`. | |
| 553 * | |
| 554 * The `userInfo`, `host` and `port` components are set from the | |
| 555 * [authority] argument. If `authority` is `null` or empty, | |
| 556 * the created `Uri` will have no authority, and will not be directly usable | |
| 557 * as an HTTP URL, which must have a non-empty host. | |
| 558 * | |
| 559 * The `path` component is set from the [unencodedPath] | |
| 560 * argument. The path passed must not be encoded as this constructor | |
| 561 * encodes the path. | |
| 562 * | |
| 563 * The `query` component is set from the optional [queryParameters] | |
| 564 * argument. | |
| 565 */ | |
| 566 factory Uri.http(String authority, | |
| 567 String unencodedPath, | |
| 568 [Map<String, String> queryParameters]) { | |
| 569 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
| 570 } | |
| 571 | |
| 572 /** | |
| 573 * Creates a new `https` URI from authority, path and query. | |
| 574 * | |
| 575 * This constructor is the same as [Uri.http] except for the scheme | |
| 576 * which is set to `https`. | |
| 577 */ | |
| 578 factory Uri.https(String authority, | |
| 579 String unencodedPath, | |
| 580 [Map<String, String> queryParameters]) { | |
| 581 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | |
| 582 } | |
| 583 | |
| 588 static Uri _makeHttpUri(String scheme, | 584 static Uri _makeHttpUri(String scheme, |
| 589 String authority, | 585 String authority, |
| 590 String unencodedPath, | 586 String unencodedPath, |
| 591 Map<String, String> queryParameters) { | 587 Map<String, String> queryParameters) { |
| 592 var userInfo = ""; | 588 var userInfo = ""; |
| 593 var host = null; | 589 var host = null; |
| 594 var port = null; | 590 var port = null; |
| 595 | 591 |
| 596 if (authority != null && authority.isNotEmpty) { | 592 if (authority != null && authority.isNotEmpty) { |
| 597 var hostStart = 0; | 593 var hostStart = 0; |
| (...skipping 345 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 943 if (scheme != null) { | 939 if (scheme != null) { |
| 944 scheme = _makeScheme(scheme, 0, scheme.length); | 940 scheme = _makeScheme(scheme, 0, scheme.length); |
| 945 schemeChanged = true; | 941 schemeChanged = true; |
| 946 } else { | 942 } else { |
| 947 scheme = this.scheme; | 943 scheme = this.scheme; |
| 948 } | 944 } |
| 949 bool isFile = (scheme == "file"); | 945 bool isFile = (scheme == "file"); |
| 950 if (userInfo != null) { | 946 if (userInfo != null) { |
| 951 userInfo = _makeUserInfo(userInfo, 0, userInfo.length); | 947 userInfo = _makeUserInfo(userInfo, 0, userInfo.length); |
| 952 } else { | 948 } else { |
| 953 userInfo = this._userInfo; | 949 userInfo = this.userInfo; |
| 954 } | 950 } |
| 955 if (port != null) { | 951 if (port != null) { |
| 956 port = _makePort(port, scheme); | 952 port = _makePort(port, scheme); |
| 957 } else { | 953 } else { |
| 958 port = this._port; | 954 port = this._port; |
| 959 if (schemeChanged) { | 955 if (schemeChanged) { |
| 960 // The default port might have changed. | 956 // The default port might have changed. |
| 961 port = _makePort(port, scheme); | 957 port = _makePort(port, scheme); |
| 962 } | 958 } |
| 963 } | 959 } |
| 964 if (host != null) { | 960 if (host != null) { |
| 965 host = _makeHost(host, 0, host.length, false); | 961 host = _makeHost(host, 0, host.length, false); |
| 966 } else if (this.hasAuthority) { | 962 } else if (this.hasAuthority) { |
| 967 host = this._host; | 963 host = this.host; |
| 968 } else if (userInfo.isNotEmpty || port != null || isFile) { | 964 } else if (userInfo.isNotEmpty || port != null || isFile) { |
| 969 host = ""; | 965 host = ""; |
| 970 } | 966 } |
| 971 | 967 |
| 972 bool hasAuthority = host != null; | 968 bool hasAuthority = host != null; |
| 973 if (path != null || pathSegments != null) { | 969 if (path != null || pathSegments != null) { |
| 974 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 970 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| 975 scheme, hasAuthority); | 971 scheme, hasAuthority); |
| 976 } else { | 972 } else { |
| 977 path = this._path; | 973 path = this.path; |
| 978 if ((isFile || (hasAuthority && !path.isEmpty)) && | 974 if ((isFile || (hasAuthority && !path.isEmpty)) && |
| 979 !path.startsWith('/')) { | 975 !path.startsWith('/')) { |
| 980 path = "/" + path; | 976 path = "/" + path; |
| 981 } | 977 } |
| 982 } | 978 } |
| 983 | 979 |
| 984 if (query != null || queryParameters != null) { | 980 if (query != null || queryParameters != null) { |
| 985 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 981 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| 986 } else { | 982 } else if (this.hasQuery) { |
| 987 query = this._query; | 983 query = this.query; |
| 988 } | 984 } |
| 989 | 985 |
| 990 if (fragment != null) { | 986 if (fragment != null) { |
| 991 fragment = _makeFragment(fragment, 0, fragment.length); | 987 fragment = _makeFragment(fragment, 0, fragment.length); |
| 992 } else { | 988 } else if (this.hasFragment) { |
| 993 fragment = this._fragment; | 989 fragment = this.fragment; |
| 994 } | 990 } |
| 995 | 991 |
| 996 return new Uri._internal( | 992 return new Uri._internal( |
| 997 scheme, userInfo, host, port, path, query, fragment); | 993 scheme, userInfo, host, port, path, query, fragment); |
| 998 } | 994 } |
| 999 | 995 |
| 1000 /** | 996 /** |
| 1001 * Returns a `Uri` that differs from this only in not having a fragment. | 997 * Returns a `Uri` that differs from this only in not having a fragment. |
| 1002 * | 998 * |
| 1003 * If this `Uri` does not have a fragment, it is itself returned. | 999 * If this `Uri` does not have a fragment, it is itself returned. |
| 1004 */ | 1000 */ |
| 1005 Uri removeFragment() { | 1001 Uri removeFragment() { |
| 1006 if (!this.hasFragment) return this; | 1002 if (!this.hasFragment) return this; |
| 1007 return new Uri._internal(scheme, _userInfo, _host, _port, | 1003 return new Uri._internal(scheme, userInfo, host, port, path, query, null); |
| 1008 _path, _query, null); | |
| 1009 } | 1004 } |
| 1010 | 1005 |
| 1011 /** | 1006 /** |
| 1012 * Returns the URI path split into its segments. Each of the | 1007 * Returns the URI path split into its segments. Each of the |
| 1013 * segments in the returned list have been decoded. If the path is | 1008 * segments in the returned list have been decoded. If the path is |
| 1014 * empty the empty list will be returned. A leading slash `/` does | 1009 * empty the empty list will be returned. A leading slash `/` does |
| 1015 * not affect the segments returned. | 1010 * not affect the segments returned. |
| 1016 * | 1011 * |
| 1017 * The returned list is unmodifiable and will throw [UnsupportedError] on any | 1012 * The returned list is unmodifiable and will throw [UnsupportedError] on any |
| 1018 * calls that would mutate it. | 1013 * calls that would mutate it. |
| (...skipping 1296 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 2315 * | 2310 * |
| 2316 * This function is similar to the JavaScript-function `decodeURI`. | 2311 * This function is similar to the JavaScript-function `decodeURI`. |
| 2317 * | 2312 * |
| 2318 * If [plusToSpace] is `true`, plus characters will be converted to spaces. | 2313 * If [plusToSpace] is `true`, plus characters will be converted to spaces. |
| 2319 * | 2314 * |
| 2320 * The decoder will create a byte-list of the percent-encoded parts, and then | 2315 * The decoder will create a byte-list of the percent-encoded parts, and then |
| 2321 * decode the byte-list using [encoding]. The default encodingis UTF-8. | 2316 * decode the byte-list using [encoding]. The default encodingis UTF-8. |
| 2322 */ | 2317 */ |
| 2323 static String _uriDecode(String text, | 2318 static String _uriDecode(String text, |
| 2324 {bool plusToSpace: false, | 2319 {bool plusToSpace: false, |
| 2325 Encoding encoding: UTF8}) { | 2320 Encoding encoding: UTF8, |
| 2321 int start: 0, | |
| 2322 int end}) { | |
| 2323 if (end == null) end = text.length; | |
| 2326 // First check whether there is any characters which need special handling. | 2324 // First check whether there is any characters which need special handling. |
| 2327 bool simple = true; | 2325 bool simple = true; |
| 2328 for (int i = 0; i < text.length && simple; i++) { | 2326 for (int i = start; i < end && simple; i++) { |
| 2329 var codeUnit = text.codeUnitAt(i); | 2327 var codeUnit = text.codeUnitAt(i); |
| 2330 simple = codeUnit != _PERCENT && codeUnit != _PLUS; | 2328 simple = codeUnit != _PERCENT && codeUnit != _PLUS; |
| 2331 } | 2329 } |
| 2332 List<int> bytes; | 2330 List<int> bytes; |
| 2333 if (simple) { | 2331 if (simple) { |
| 2334 if (encoding == UTF8 || encoding == LATIN1) { | 2332 if (encoding == UTF8 || encoding == LATIN1) { |
| 2335 return text; | 2333 return text.substring(start, end); |
| 2334 } else if (start == 0 && end == text.length) { | |
| 2335 bytes = text.codeUnits; | |
| 2336 } else { | 2336 } else { |
| 2337 bytes = text.codeUnits; | 2337 var decoder = encoding.decoder; |
| 2338 var result; | |
| 2339 var conversionSink = decoder.startChunkedConversion( | |
| 2340 new ChunkedConversionSink.withCallback((list) { | |
| 2341 result = list.join(); | |
| 2342 })); | |
| 2343 if (conversionSink is ByteConversionSink) { | |
| 2344 conversionSink.addSlice(text.codeUnits, start, end, true); | |
| 2345 } else { | |
| 2346 conversionSink.add(text.codeUnits.sublist(start, end)); | |
| 2347 conversionSink.close(); | |
| 2348 } | |
| 2349 return result; | |
| 2338 } | 2350 } |
| 2339 } else { | 2351 } else { |
| 2340 bytes = new List(); | 2352 bytes = new List(); |
| 2341 for (int i = 0; i < text.length; i++) { | 2353 for (int i = start; i < end; i++) { |
| 2342 var codeUnit = text.codeUnitAt(i); | 2354 var codeUnit = text.codeUnitAt(i); |
| 2343 if (codeUnit > 127) { | 2355 if (codeUnit > 127) { |
| 2344 throw new ArgumentError("Illegal percent encoding in URI"); | 2356 throw new ArgumentError("Illegal percent encoding in URI"); |
| 2345 } | 2357 } |
| 2346 if (codeUnit == _PERCENT) { | 2358 if (codeUnit == _PERCENT) { |
| 2347 if (i + 3 > text.length) { | 2359 if (i + 3 > text.length) { |
| 2348 throw new ArgumentError('Truncated URI'); | 2360 throw new ArgumentError('Truncated URI'); |
| 2349 } | 2361 } |
| 2350 bytes.add(_hexCharPairToByte(text, i + 1)); | 2362 bytes.add(_hexCharPairToByte(text, i + 1)); |
| 2351 i += 2; | 2363 i += 2; |
| (...skipping 251 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 2603 // 0123456789:; = ? | 2615 // 0123456789:; = ? |
| 2604 0xafff, // 0x30 - 0x3f 1111111111110101 | 2616 0xafff, // 0x30 - 0x3f 1111111111110101 |
| 2605 // @ABCDEFGHIJKLMNO | 2617 // @ABCDEFGHIJKLMNO |
| 2606 0xffff, // 0x40 - 0x4f 1111111111111111 | 2618 0xffff, // 0x40 - 0x4f 1111111111111111 |
| 2607 // PQRSTUVWXYZ _ | 2619 // PQRSTUVWXYZ _ |
| 2608 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2620 0x87ff, // 0x50 - 0x5f 1111111111100001 |
| 2609 // abcdefghijklmno | 2621 // abcdefghijklmno |
| 2610 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2622 0xfffe, // 0x60 - 0x6f 0111111111111111 |
| 2611 // pqrstuvwxyz ~ | 2623 // pqrstuvwxyz ~ |
| 2612 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2624 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
| 2625 | |
| 2613 } | 2626 } |
| 2627 | |
| 2628 // -------------------------------------------------------------------- | |
| 2629 // Data URI | |
| 2630 // -------------------------------------------------------------------- | |
| 2631 | |
| 2632 /** | |
| 2633 * A representation of a `data:` URI. | |
| 2634 * | |
| 2635 * Data URIs are non-hierarchial URIs that contain can contain any data. | |
| 2636 * They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397). | |
| 2637 * | |
| 2638 * This class allows parsing the URI text and extracting individual parts of the | |
| 2639 * URI, as well as building the URI text from structured parts. | |
| 2640 */ | |
| 2641 class DataUri { | |
| 2642 static const int _noScheme = -1; | |
| 2643 /** | |
| 2644 * Contains the text content of a `data:` URI, with or without a | |
| 2645 * leading `data:`. | |
| 2646 * | |
| 2647 * If [_separatorIndices] starts with `4` (the index of the `:`), then | |
| 2648 * there is a leading `data:`, otherwise _separatorIndices starts with | |
| 2649 * `-1`. | |
| 2650 */ | |
| 2651 final String _text; | |
| 2652 | |
| 2653 /** | |
| 2654 * List of the separators (';', '=' and ',') in the text. | |
| 2655 * | |
| 2656 * Starts with the index of the index of the `:` in `data:` of the mimeType. | |
| 2657 * That is always either -1 or 4, depending on whether `_text` includes the | |
| 2658 * `data:` scheme or not. | |
| 2659 * | |
| 2660 * The first speparator ends the mime type. We don't bother with finding | |
| 2661 * the '/' inside the mime type. | |
| 2662 * | |
| 2663 * Each two separators after that marks a parameter key and value. | |
| 2664 * | |
| 2665 * If there is a single separator left, it ends the "base64" marker. | |
| 2666 * | |
| 2667 * So the following separators are found for a text: | |
| 2668 * | |
| 2669 * data:text/plain;foo=bar;base64,ARGLEBARGLE= | |
| 2670 * ^ ^ ^ ^ ^ | |
| 2671 * | |
| 2672 */ | |
| 2673 List<int> _separatorIndices; | |
| 2674 | |
| 2675 DataUri._(this._text, | |
| 2676 this._separatorIndices); | |
| 2677 | |
| 2678 /** 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
| |
| 2679 String get text => _separatorIndices[0] == _noScheme ? "data:$_text" : _text; | |
| 2680 | |
| 2681 /** | |
| 2682 * Creates a `data:` URI containing the contents as percent-encoded text. | |
| 2683 */ | |
| 2684 factory DataUri.fromString(String content, | |
| 2685 {mimeType: "text/plain", | |
| 2686 Iterable<DataUriParameter> parameters}) { | |
| 2687 StringBuffer buffer = new StringBuffer(); | |
| 2688 List indices = [_noScheme]; | |
| 2689 _writeUri(mimeType, parameters, buffer, indices); | |
| 2690 indices.add(buffer.length); | |
| 2691 buffer.write(','); | |
| 2692 buffer.write(Uri.encodeComponent(content)); | |
| 2693 return new DataUri._(buffer.toString(), indices); | |
| 2694 } | |
| 2695 | |
| 2696 /** | |
| 2697 * Creates a `data:` URI string containing the base-64 encoded content bytes. | |
| 2698 * | |
| 2699 * It defaults to having the mime-type `application/octet-stream`. | |
| 2700 */ | |
| 2701 factory DataUri.fromBytes(List<int> bytes, | |
| 2702 {mimeType: "application/octet-stream", | |
| 2703 Iterable<DataUriParameter> parameters}) { | |
| 2704 StringBuffer buffer = new StringBuffer(); | |
| 2705 List indices = [_noScheme]; | |
| 2706 _writeUri(mimeType, parameters, buffer, indices); | |
| 2707 indices.add(buffer.length); | |
| 2708 buffer.write(';base64,'); | |
| 2709 indices.add(buffer.length - 1); | |
| 2710 BASE64.encoder.startChunkedConversion( | |
| 2711 new StringConversionSink.fromStringSink(buffer)) | |
| 2712 .addSlice(bytes, 0, bytes.length, true); | |
| 2713 return new DataUri._(buffer.toString(), indices); | |
| 2714 } | |
| 2715 | |
| 2716 /** | |
| 2717 * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. | |
| 2718 * | |
| 2719 * The [uri] must have scheme `data` and no authority, query or fragment, | |
| 2720 * and the path must be valid as a data URI. | |
| 2721 */ | |
| 2722 factory DataUri.fromUri(Uri uri) { | |
| 2723 if (uri.scheme != "data") { | |
| 2724 throw new ArgumentError.value(uri, "uri", | |
| 2725 "Scheme must be 'data'"); | |
| 2726 } | |
| 2727 if (uri.hasAuthority) { | |
| 2728 throw new ArgumentError.value(uri, "uri", | |
| 2729 "Data uri must not have authority"); | |
| 2730 } | |
| 2731 if (uri.hasQuery) { | |
| 2732 throw new ArgumentError.value(uri, "uri", | |
| 2733 "Data uri must not have a query part"); | |
| 2734 } | |
| 2735 if (uri.hasFragment) { | |
| 2736 throw new ArgumentError.value(uri, "uri", | |
| 2737 "Data uri must not have a fragment part"); | |
| 2738 } | |
| 2739 return _parse(uri.path, 0); | |
| 2740 } | |
| 2741 | |
| 2742 /** | |
| 2743 * Writes the initial part of a `data:` uri, from after the "data:" | |
| 2744 * until just before the ',' before the data, or before a `;base64,` | |
| 2745 * marker. | |
| 2746 * | |
| 2747 * Of an [indices] list is passed, separator indices are stored in that | |
| 2748 * list. | |
| 2749 */ | |
| 2750 static void _writeUri(String mimeType, | |
| 2751 Iterable<DataUriParameter> parameters, | |
| 2752 StringBuffer buffer, List indices) { | |
| 2753 if (mimeType == null) { | |
| 2754 mimeType = "text/plain"; | |
| 2755 } | |
| 2756 if (mimeType.isEmpty || | |
| 2757 identical(mimeType, "text/plain") || | |
| 2758 identical(mimeType, "application/octet-stream")) { | |
| 2759 buffer.write(mimeType); // Common cases need no escaping. | |
| 2760 } else { | |
| 2761 int slashIndex = _validateMimeType(mimeType); | |
| 2762 if (slashIndex < 0) { | |
| 2763 throw new ArgumentError.value(mimeType, "mimeType", | |
| 2764 "Invalid MIME type"); | |
| 2765 } | |
| 2766 buffer.write(Uri._uriEncode(_tokenCharTable, | |
| 2767 mimeType.substring(0, slashIndex))); | |
| 2768 buffer.write("/"); | |
| 2769 buffer.write(Uri._uriEncode(_tokenCharTable, | |
| 2770 mimeType.substring(slashIndex + 1))); | |
| 2771 } | |
| 2772 if (parameters != null) { | |
| 2773 for (var parameter in parameters) { | |
| 2774 if (indices != null) indices.add(buffer.length); | |
| 2775 buffer.write(';'); | |
| 2776 // Encode any non-RFC2045-token character as well as '%' and '#'. | |
| 2777 buffer.write(Uri._uriEncode(_tokenCharTable, parameter.key)); | |
| 2778 if (indices != null) indices.add(buffer.length); | |
| 2779 buffer.write('='); | |
| 2780 buffer.write(Uri._uriEncode(_tokenCharTable, parameter.value)); | |
| 2781 } | |
| 2782 } | |
| 2783 } | |
| 2784 | |
| 2785 /** | |
| 2786 * Checks mimeType is valid-ish (`token '/' token`). | |
| 2787 * | |
| 2788 * Returns the index of the slash, or -1 if the mime type is not | |
| 2789 * considered valid. | |
| 2790 * | |
| 2791 * Currently only looks for slashes, all other characters will be | |
| 2792 * percent-encoded as UTF-8 if necessary. | |
| 2793 */ | |
| 2794 static int _validateMimeType(String mimeType) { | |
| 2795 int slashIndex = -1; | |
| 2796 for (int i = 0; i < mimeType.length; i++) { | |
| 2797 var char = mimeType.codeUnitAt(i); | |
| 2798 if (char != Uri._SLASH) continue; | |
| 2799 if (slashIndex < 0) { | |
| 2800 slashIndex = i; | |
| 2801 continue; | |
| 2802 } | |
| 2803 return -1; | |
| 2804 } | |
| 2805 return slashIndex; | |
| 2806 } | |
| 2807 | |
| 2808 /** | |
| 2809 * Creates a [Uri] with the content of [DataUri.fromString]. | |
| 2810 * | |
| 2811 * The resulting URI will have `data` as scheme and the remainder | |
| 2812 * of the data URI as path. | |
| 2813 * | |
| 2814 * Equivalent to creating a `DataUri` using `new DataUri.fromString` and | |
| 2815 * calling `toUri` on the result. | |
| 2816 */ | |
| 2817 static Uri uriFromString(String content, | |
| 2818 {mimeType: "text/plain", | |
| 2819 Iterable<DataUriParameter> parameters}) { | |
| 2820 var buffer = new StringBuffer(); | |
| 2821 _writeUri(mimeType, parameters, buffer, null); | |
| 2822 buffer.write(','); | |
| 2823 buffer.write(Uri.encodeComponent(content)); | |
| 2824 return new Uri(scheme: "data", path: buffer.toString()); | |
| 2825 } | |
| 2826 | |
| 2827 /** | |
| 2828 * Creates a [Uri] with the content of [DataUri.fromBytes]. | |
| 2829 * | |
| 2830 * The resulting URI will have `data` as scheme and the remainder | |
| 2831 * of the data URI as path. | |
| 2832 * | |
| 2833 * Equivalent to creating a `DataUri` using `new DataUri.fromBytes` and | |
| 2834 * calling `toUri` on the result. | |
| 2835 */ | |
| 2836 static Uri uriFromBytes(List<int> bytes, | |
| 2837 {mimeType: "text/plain", | |
| 2838 Iterable<DataUriParameter> parameters}) { | |
| 2839 var buffer = new StringBuffer(); | |
| 2840 _writeUri(mimeType, parameters, buffer, null); | |
| 2841 buffer.write(';base64,'); | |
| 2842 BASE64.encoder.startChunkedConversion(buffer) | |
| 2843 .addSlice(bytes, 0, bytes.length, true); | |
| 2844 return new Uri(scheme: "data", path: buffer.toString()); | |
| 2845 } | |
| 2846 | |
| 2847 /** | |
| 2848 * Parses a string as a `data` URI. | |
| 2849 */ | |
| 2850 static DataUri parse(String uri) { | |
| 2851 if (!uri.startsWith("data:")) { | |
| 2852 throw new FormatException("Does not start with 'data:'", uri, 0); | |
| 2853 } | |
| 2854 return _parse(uri, 5); | |
| 2855 } | |
| 2856 | |
| 2857 /** | |
| 2858 * Converts a `DataUri` to a [Uri]. | |
| 2859 * | |
| 2860 * Returns a `Uri` with scheme `data` and the remainder of the data URI | |
| 2861 * as path. | |
| 2862 */ | |
| 2863 Uri toUri() { | |
| 2864 String content = _text; | |
| 2865 int colonIndex = _separatorIndices[0]; | |
| 2866 if (colonIndex >= 0) { | |
| 2867 content = _text.substring(colonIndex + 1); | |
| 2868 } | |
| 2869 return new Uri._internal("data", null, null, null, content, null, null); | |
| 2870 } | |
| 2871 | |
| 2872 /** | |
| 2873 * The MIME type of the data URI. | |
| 2874 * | |
| 2875 * A data URI consists of a "media type" followed by data. | |
| 2876 * The mediatype starts with a MIME type and can be followed by | |
| 2877 * extra parameters. | |
| 2878 * | |
| 2879 * Example: | |
| 2880 * | |
| 2881 * data:text/plain;encoding=utf-8,Hello%20World! | |
| 2882 * | |
| 2883 * This data URI has the media type `text/plain;encoding=utf-8`, which is the | |
| 2884 * MIME type `text/plain` with the parameter `encoding` with value `utf-8`. | |
| 2885 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. | |
| 2886 * | |
| 2887 * If the first part of the data URI is empty, it defaults to `text/plain`. | |
| 2888 */ | |
| 2889 String get mimeType { | |
| 2890 int start = _separatorIndices[0] + 1; | |
| 2891 int end = _separatorIndices[1]; | |
| 2892 if (start == end) return "text/plain"; | |
| 2893 return Uri._uriDecode(_text, start: start, end: end); | |
| 2894 } | |
| 2895 | |
| 2896 /** | |
| 2897 * Whether the data is base64 encoded or not. | |
| 2898 */ | |
| 2899 bool get isBase64 => _separatorIndices.length.isOdd; | |
| 2900 | |
| 2901 /** | |
| 2902 * The content part of the data URI, as its actual representation. | |
| 2903 * | |
| 2904 * This string may contain percent escapes. | |
| 2905 */ | |
| 2906 String get contentText => _text.substring(_separatorIndices.last + 1); | |
| 2907 | |
| 2908 /** | |
| 2909 * The content part of the data URI as bytes. | |
| 2910 * | |
| 2911 * If the data is base64 encoded, it will be decoded to bytes. | |
| 2912 * | |
| 2913 * If the data is not base64 encoded, it will be decoded by unescaping | |
| 2914 * percent-escaped characters and returning byte values of each unescaped | |
| 2915 * character. The bytes will not be, e.g., UTF-8 decoded. | |
| 2916 */ | |
| 2917 List<int> contentAsBytes() { | |
|
Lasse Reichstein Nielsen
2015/10/28 13:55:47
This sounds like it should be a getter, but the co
| |
| 2918 String text = _text; | |
| 2919 int start = _separatorIndices.last + 1; | |
| 2920 if (isBase64) { | |
| 2921 if (text.endsWith("%3D")) { | |
| 2922 return BASE64.decode(Uri._uriDecode(text, start: start, | |
| 2923 encoding: LATIN1)); | |
| 2924 } | |
| 2925 return BASE64.decode(text.substring(start)); | |
| 2926 } | |
| 2927 | |
| 2928 // Not base64, do percent-decoding and return the remaining bytes. | |
| 2929 // Compute result size. | |
| 2930 const int percent = 0x25; | |
| 2931 int length = text.length - start; | |
| 2932 for (int i = start; i < text.length; i++) { | |
| 2933 var codeUnit = text.codeUnitAt(i); | |
| 2934 if (codeUnit == percent) { | |
| 2935 i += 2; | |
| 2936 length -= 2; | |
| 2937 } | |
| 2938 } | |
| 2939 // Fill result array. | |
| 2940 Uint8List result = new Uint8List(length); | |
| 2941 if (length == text.length) { | |
| 2942 result.setRange(0, length, text.codeUnits, start); | |
| 2943 return result; | |
| 2944 } | |
| 2945 int index = 0; | |
| 2946 for (int i = start; i < text.length; i++) { | |
| 2947 var codeUnit = text.codeUnitAt(i); | |
| 2948 if (codeUnit != percent) { | |
| 2949 result[index++] = codeUnit; | |
| 2950 } else { | |
| 2951 if (i + 2 < text.length) { | |
| 2952 var digit1 = _hexDigit(text.codeUnitAt(i + 1)); | |
| 2953 var digit2 = _hexDigit(text.codeUnitAt(i + 2)); | |
| 2954 if (digit1 >= 0 && digit2 >= 0) { | |
| 2955 int byte = digit1 * 16 + digit2; | |
| 2956 result[index++] = byte; | |
| 2957 i += 2; | |
| 2958 continue; | |
| 2959 } | |
| 2960 } | |
| 2961 throw new FormatException("Invalid percent escape", text, i); | |
| 2962 } | |
| 2963 } | |
| 2964 assert(index == result.length); | |
| 2965 return result; | |
| 2966 } | |
| 2967 | |
| 2968 // Converts a UTF-16 code-unit to its value as a hex digit. | |
| 2969 // Returns -1 for non-hex digits. | |
| 2970 int _hexDigit(int char) { | |
| 2971 const int char_0 = 0x30; | |
| 2972 const int char_a = 0x61; | |
| 2973 | |
| 2974 int digit = char ^ char_0; | |
| 2975 if (digit <= 9) return digit; | |
| 2976 char = ((char | 0x20) - char_a) & 0xFFFF; | |
| 2977 if (char < 6) return 10 + char; | |
| 2978 return -1; | |
| 2979 } | |
| 2980 | |
| 2981 /** | |
| 2982 * Returns a string created from the content of the data URI. | |
| 2983 * | |
| 2984 * If the content is base64 encoded, it will be decoded to bytes and then | |
| 2985 * decoded to a string using [encoding]. | |
| 2986 * | |
| 2987 * If the content is not base64 encoded, it will first have percent-escapes | |
| 2988 * converted to bytes and then the character codes and byte values are | |
| 2989 * decoded using [encoding]. | |
| 2990 */ | |
| 2991 String contentAsString({Encoding encoding: UTF8}) { | |
| 2992 String text = _text; | |
| 2993 int start = _separatorIndices.last + 1; | |
| 2994 if (isBase64) { | |
| 2995 var converter = BASE64.decoder.fuse(encoding.decoder); | |
| 2996 if (text.endsWith("%3D")) { | |
| 2997 return converter.convert(Uri._uriDecode(text, start: start, | |
| 2998 encoding: LATIN1)); | |
| 2999 } | |
| 3000 return converter.convert(text.substring(start)); | |
| 3001 } | |
| 3002 return Uri._uriDecode(text, start: start, encoding: encoding); | |
| 3003 } | |
| 3004 | |
| 3005 /** | |
| 3006 * An iterable over the parameters of the data URI. | |
| 3007 * | |
| 3008 * A data URI may contain parameters between the the MIMI type and the | |
| 3009 * data. This iterates through those parameters, returning each as a | |
| 3010 * [DataUriParameter] pair of key and value. | |
| 3011 */ | |
| 3012 Iterable<DataUriParameter> get parameters sync* { | |
| 3013 for (int i = 3; i < _separatorIndices.length; i += 2) { | |
| 3014 var start = _separatorIndices[i - 2] + 1; | |
| 3015 var equals = _separatorIndices[i - 1]; | |
| 3016 var end = _separatorIndices[i]; | |
| 3017 String key = Uri._uriDecode(_text, start: start, end: equals); | |
| 3018 String value = Uri._uriDecode(_text, start: equals + 1, end: end); | |
| 3019 yield new DataUriParameter(key, value); | |
| 3020 } | |
| 3021 } | |
| 3022 | |
| 3023 static DataUri _parse(String text, int start) { | |
| 3024 assert(start == 0 || start == 5); | |
| 3025 assert((start == 5) == text.startsWith("data:")); | |
| 3026 | |
| 3027 /// Character codes. | |
| 3028 const int comma = 0x2c; | |
| 3029 const int slash = 0x2f; | |
| 3030 const int semicolon = 0x3b; | |
| 3031 const int equals = 0x3d; | |
| 3032 List indices = [start - 1]; | |
| 3033 int slashIndex = -1; | |
| 3034 var char; | |
| 3035 int i = start; | |
| 3036 for (; i < text.length; i++) { | |
| 3037 char = text.codeUnitAt(i); | |
| 3038 if (char == comma || char == semicolon) break; | |
| 3039 if (char == slash) { | |
| 3040 if (slashIndex < 0) { | |
| 3041 slashIndex = i; | |
| 3042 continue; | |
| 3043 } | |
| 3044 throw new FormatException("Invalid MIME type", text, i); | |
| 3045 } | |
| 3046 } | |
| 3047 if (slashIndex < 0 && i > start) { | |
| 3048 // An empty MIME type is allowed, but if non-empty it must contain | |
| 3049 // exactly one slash. | |
| 3050 throw new FormatException("Invalid MIME type", text, i); | |
| 3051 } | |
| 3052 while (char != comma) { | |
| 3053 // parse parameters and/or "base64". | |
| 3054 indices.add(i); | |
| 3055 i++; | |
| 3056 int equalsIndex = -1; | |
| 3057 for (; i < text.length; i++) { | |
| 3058 char = text.codeUnitAt(i); | |
| 3059 if (char == equals) { | |
| 3060 if (equalsIndex < 0) equalsIndex = i; | |
| 3061 } else if (char == semicolon || char == comma) { | |
| 3062 break; | |
| 3063 } | |
| 3064 } | |
| 3065 if (equalsIndex >= 0) { | |
| 3066 indices.add(equalsIndex); | |
| 3067 } else { | |
| 3068 // Have to be final "base64". | |
| 3069 var lastSeparator = indices.last; | |
| 3070 if (char != comma || | |
| 3071 i != lastSeparator + 7 /* "base64,".length */ || | |
| 3072 !text.startsWith("base64", lastSeparator + 1)) { | |
| 3073 throw new FormatException("Expecting '='", text, i); | |
| 3074 } | |
| 3075 break; | |
| 3076 } | |
| 3077 } | |
| 3078 indices.add(i); | |
| 3079 return new DataUri._(text, indices); | |
| 3080 } | |
| 3081 | |
| 3082 String toString() => text; | |
| 3083 | |
| 3084 // Table of the `token` characters of RFC 2045 in a URI. | |
| 3085 // | |
| 3086 // A token is any US-ASCII character except SPACE, control characters and | |
| 3087 // `tspecial` characters. The `tspecial` category is: | |
| 3088 // '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='. | |
| 3089 // | |
| 3090 // In a data URI, we also need to escape '%' and '#' characters. | |
| 3091 static const _tokenCharTable = const [ | |
| 3092 // LSB MSB | |
| 3093 // | | | |
| 3094 0x0000, // 0x00 - 0x0f 00000000 00000000 | |
| 3095 0x0000, // 0x10 - 0x1f 00000000 00000000 | |
| 3096 // ! $ &' *+ -. | |
| 3097 0x6cd2, // 0x20 - 0x2f 01001011 00110110 | |
| 3098 // 01234567 89 | |
| 3099 0x03ff, // 0x30 - 0x3f 11111111 11000000 | |
| 3100 // ABCDEFG HIJKLMNO | |
| 3101 0xfffe, // 0x40 - 0x4f 01111111 11111111 | |
| 3102 // PQRSTUVW XYZ ^_ | |
| 3103 0xc7ff, // 0x50 - 0x5f 11111111 11100011 | |
| 3104 // `abcdefg hijklmno | |
| 3105 0xffff, // 0x60 - 0x6f 11111111 11111111 | |
| 3106 // pqrstuvw xyz{|}~ | |
| 3107 0x7fff]; // 0x70 - 0x7f 11111111 11111110 | |
| 3108 } | |
| 3109 | |
| 3110 /** | |
| 3111 * A parameter of a data URI. | |
| 3112 * | |
| 3113 * A parameter is a key and a value. | |
| 3114 * | |
| 3115 * The key and value are the actual values to be encoded into the URI. | |
| 3116 * They will be escaped if necessary when creating a data URI, | |
| 3117 * and have been unescaped when extracted from a data URI. | |
| 3118 */ | |
| 3119 class DataUriParameter { | |
| 3120 /** Parameter key. */ | |
| 3121 final String key; | |
| 3122 /** Parameter value. */ | |
| 3123 final String value; | |
| 3124 DataUriParameter(this.key, this.value); | |
| 3125 | |
| 3126 /** | |
| 3127 * Creates an iterable of parameters from a map from key to value. | |
| 3128 * | |
| 3129 * Parameter keys are not required to be unique in a data URI, but | |
| 3130 * when they are, a map can be used to represent the parameters, and | |
| 3131 * this function provides a way to access the map pairs as parameter | |
| 3132 * values. | |
| 3133 */ | |
| 3134 static Iterable<DataUriParameter> fromMap(Map<String, String> headers) sync* { | |
| 3135 for (String key in headers.keys) { | |
| 3136 yield new DataUriParameter(key, headers[key]); | |
| 3137 } | |
| 3138 } | |
| 3139 } | |
| OLD | NEW |