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, ASCII, 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) { | |
Ivan Posva
2015/11/03 17:04:20
Why is this not a static private function? Where i
Lasse Reichstein Nielsen
2015/11/03 18:02:52
I don't think it is being used at all.
That's actu
| |
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 1234 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2253 | 2248 |
2254 /** | 2249 /** |
2255 * This is the internal implementation of JavaScript's encodeURI function. | 2250 * This is the internal implementation of JavaScript's encodeURI function. |
2256 * It encodes all characters in the string [text] except for those | 2251 * It encodes all characters in the string [text] except for those |
2257 * that appear in [canonicalTable], and returns the escaped string. | 2252 * that appear in [canonicalTable], and returns the escaped string. |
2258 */ | 2253 */ |
2259 static String _uriEncode(List<int> canonicalTable, | 2254 static String _uriEncode(List<int> canonicalTable, |
2260 String text, | 2255 String text, |
2261 {Encoding encoding: UTF8, | 2256 {Encoding encoding: UTF8, |
2262 bool spaceToPlus: false}) { | 2257 bool spaceToPlus: false}) { |
2263 byteToHex(byte, buffer) { | |
2264 const String hex = '0123456789ABCDEF'; | |
2265 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); | |
2266 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); | |
2267 } | |
2268 | |
2269 // Encode the string into bytes then generate an ASCII only string | 2258 // Encode the string into bytes then generate an ASCII only string |
2270 // by percent encoding selected bytes. | 2259 // by percent encoding selected bytes. |
2260 List<int> bytes = encoding.encode(text); | |
2271 StringBuffer result = new StringBuffer(); | 2261 StringBuffer result = new StringBuffer(); |
2272 var bytes = encoding.encode(text); | 2262 _uriEncodeBytes(canonicalTable, bytes, result, spaceToPlus); |
2273 for (int i = 0; i < bytes.length; i++) { | |
2274 int byte = bytes[i]; | |
2275 if (byte < 128 && | |
2276 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | |
2277 result.writeCharCode(byte); | |
2278 } else if (spaceToPlus && byte == _SPACE) { | |
2279 result.writeCharCode(_PLUS); | |
2280 } else { | |
2281 result.writeCharCode(_PERCENT); | |
2282 byteToHex(byte, result); | |
2283 } | |
2284 } | |
2285 return result.toString(); | 2263 return result.toString(); |
2286 } | 2264 } |
2287 | 2265 |
2288 /** | 2266 /** |
2267 * Like [_uriEncode] but takes the input as bytes, not a string. | |
2268 * | |
2269 * Encodes into [buffer] instead of creating its own buffer. | |
2270 */ | |
2271 static void _uriEncodeBytes(List<int> canonicalTable, | |
2272 List<int> bytes, | |
2273 StringSink buffer, | |
2274 bool spaceToPlus) { | |
2275 // Encode the string into bytes then generate an ASCII only string | |
2276 // by percent encoding selected bytes. | |
2277 int byteOr = 0; | |
2278 for (int i = 0; i < bytes.length; i++) { | |
2279 int byte = bytes[i]; | |
2280 byteOr |= byte; | |
2281 if (byte < 128 && | |
2282 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | |
2283 buffer.writeCharCode(byte); | |
2284 } else if (spaceToPlus && byte == _SPACE) { | |
2285 result.writeCharCode(_PLUS); | |
2286 } else { | |
2287 buffer.writeCharCode(_PERCENT); | |
2288 const String hex = '0123456789ABCDEF'; | |
2289 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); | |
2290 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); | |
2291 } | |
2292 } | |
2293 if ((byteOr & ~0xFF) != 0) { | |
2294 for (int i = 0; i < bytes.length; i++) { | |
2295 var byte = bytes[i]; | |
2296 if (byte < 0 || byte > 255) { | |
2297 throw new ArgumentError.value(byte, "non-byte value"); | |
2298 } | |
2299 } | |
2300 } | |
2301 } | |
2302 | |
2303 /** | |
2289 * Convert a byte (2 character hex sequence) in string [s] starting | 2304 * Convert a byte (2 character hex sequence) in string [s] starting |
2290 * at position [pos] to its ordinal value | 2305 * at position [pos] to its ordinal value |
2291 */ | 2306 */ |
2292 static int _hexCharPairToByte(String s, int pos) { | 2307 static int _hexCharPairToByte(String s, int pos) { |
2293 int byte = 0; | 2308 int byte = 0; |
2294 for (int i = 0; i < 2; i++) { | 2309 for (int i = 0; i < 2; i++) { |
2295 var charCode = s.codeUnitAt(pos + i); | 2310 var charCode = s.codeUnitAt(pos + i); |
2296 if (0x30 <= charCode && charCode <= 0x39) { | 2311 int digit = charCode ^ 0x30; |
2297 byte = byte * 16 + charCode - 0x30; | 2312 if (digit <= 9) { |
2313 byte = byte * 16 + digit; | |
2298 } else { | 2314 } else { |
2299 // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). | 2315 // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). |
2300 charCode |= 0x20; | 2316 charCode |= 0x20; |
2301 if (0x61 <= charCode && charCode <= 0x66) { | 2317 if (0x61 <= charCode && charCode <= 0x66) { |
2302 byte = byte * 16 + charCode - 0x57; | 2318 byte = byte * 16 + charCode - 0x57; |
2303 } else { | 2319 } else { |
2304 throw new ArgumentError("Invalid URL encoding"); | 2320 throw new ArgumentError("Invalid URL encoding"); |
2305 } | 2321 } |
2306 } | 2322 } |
2307 } | 2323 } |
2308 return byte; | 2324 return byte; |
2309 } | 2325 } |
2310 | 2326 |
2311 /** | 2327 /** |
2312 * Uri-decode a percent-encoded string. | 2328 * Uri-decode a percent-encoded string. |
2313 * | 2329 * |
2314 * It unescapes the string [text] and returns the unescaped string. | 2330 * It unescapes the string [text] and returns the unescaped string. |
2315 * | 2331 * |
2316 * This function is similar to the JavaScript-function `decodeURI`. | 2332 * This function is similar to the JavaScript-function `decodeURI`. |
2317 * | 2333 * |
2318 * If [plusToSpace] is `true`, plus characters will be converted to spaces. | 2334 * If [plusToSpace] is `true`, plus characters will be converted to spaces. |
2319 * | 2335 * |
2320 * The decoder will create a byte-list of the percent-encoded parts, and then | 2336 * 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. | 2337 * decode the byte-list using [encoding]. The default encodingis UTF-8. |
2322 */ | 2338 */ |
2323 static String _uriDecode(String text, | 2339 static String _uriDecode(String text, |
2324 {bool plusToSpace: false, | 2340 {bool plusToSpace: false, |
2325 Encoding encoding: UTF8}) { | 2341 Encoding encoding: UTF8, |
2342 int start: 0, | |
2343 int end}) { | |
2344 if (end == null) end = text.length; | |
2326 // First check whether there is any characters which need special handling. | 2345 // First check whether there is any characters which need special handling. |
2327 bool simple = true; | 2346 bool simple = true; |
2328 for (int i = 0; i < text.length && simple; i++) { | 2347 for (int i = start; i < end && simple; i++) { |
2329 var codeUnit = text.codeUnitAt(i); | 2348 var codeUnit = text.codeUnitAt(i); |
2330 simple = codeUnit != _PERCENT && codeUnit != _PLUS; | 2349 simple = codeUnit != _PERCENT && codeUnit != _PLUS; |
2331 } | 2350 } |
2332 List<int> bytes; | 2351 List<int> bytes; |
2333 if (simple) { | 2352 if (simple) { |
2334 if (encoding == UTF8 || encoding == LATIN1) { | 2353 if (encoding == UTF8 || encoding == LATIN1) { |
2335 return text; | 2354 return text.substring(start, end); |
2355 } else if (start == 0 && end == text.length) { | |
2356 bytes = text.codeUnits; | |
2336 } else { | 2357 } else { |
2337 bytes = text.codeUnits; | 2358 var decoder = encoding.decoder; |
2359 var result; | |
2360 var conversionSink = decoder.startChunkedConversion( | |
2361 new ChunkedConversionSink.withCallback((list) { | |
2362 result = list.join(); | |
2363 })); | |
2364 if (conversionSink is ByteConversionSink) { | |
2365 conversionSink.addSlice(text.codeUnits, start, end, true); | |
2366 } else { | |
2367 conversionSink.add(text.codeUnits.sublist(start, end)); | |
2368 conversionSink.close(); | |
2369 } | |
2370 return result; | |
2338 } | 2371 } |
2339 } else { | 2372 } else { |
2340 bytes = new List(); | 2373 bytes = new List(); |
2341 for (int i = 0; i < text.length; i++) { | 2374 for (int i = start; i < end; i++) { |
2342 var codeUnit = text.codeUnitAt(i); | 2375 var codeUnit = text.codeUnitAt(i); |
2343 if (codeUnit > 127) { | 2376 if (codeUnit > 127) { |
2344 throw new ArgumentError("Illegal percent encoding in URI"); | 2377 throw new ArgumentError("Illegal percent encoding in URI"); |
2345 } | 2378 } |
2346 if (codeUnit == _PERCENT) { | 2379 if (codeUnit == _PERCENT) { |
2347 if (i + 3 > text.length) { | 2380 if (i + 3 > text.length) { |
2348 throw new ArgumentError('Truncated URI'); | 2381 throw new ArgumentError('Truncated URI'); |
2349 } | 2382 } |
2350 bytes.add(_hexCharPairToByte(text, i + 1)); | 2383 bytes.add(_hexCharPairToByte(text, i + 1)); |
2351 i += 2; | 2384 i += 2; |
(...skipping 251 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2603 // 0123456789:; = ? | 2636 // 0123456789:; = ? |
2604 0xafff, // 0x30 - 0x3f 1111111111110101 | 2637 0xafff, // 0x30 - 0x3f 1111111111110101 |
2605 // @ABCDEFGHIJKLMNO | 2638 // @ABCDEFGHIJKLMNO |
2606 0xffff, // 0x40 - 0x4f 1111111111111111 | 2639 0xffff, // 0x40 - 0x4f 1111111111111111 |
2607 // PQRSTUVWXYZ _ | 2640 // PQRSTUVWXYZ _ |
2608 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2641 0x87ff, // 0x50 - 0x5f 1111111111100001 |
2609 // abcdefghijklmno | 2642 // abcdefghijklmno |
2610 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2643 0xfffe, // 0x60 - 0x6f 0111111111111111 |
2611 // pqrstuvwxyz ~ | 2644 // pqrstuvwxyz ~ |
2612 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2645 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
2646 | |
2647 } | |
2648 | |
2649 // -------------------------------------------------------------------- | |
2650 // Data URI | |
2651 // -------------------------------------------------------------------- | |
2652 | |
2653 /** | |
2654 * A way to create and access the structure of a `data:` URI. | |
2655 * | |
2656 * Data URIs are non-hierarchial URIs that contain can contain any data. | |
2657 * They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397). | |
2658 * | |
2659 * This class allows parsing the URI text and extracting individual parts of the | |
2660 * URI, as well as building the URI text from structured parts. | |
2661 */ | |
2662 class DataUriHelper { | |
2663 static const int _noScheme = -1; | |
2664 /** | |
2665 * Contains the text content of a `data:` URI, with or without a | |
2666 * leading `data:`. | |
2667 * | |
2668 * If [_separatorIndices] starts with `4` (the index of the `:`), then | |
2669 * there is a leading `data:`, otherwise [_separatorIndices] starts with | |
2670 * `-1`. | |
2671 */ | |
2672 final String _text; | |
2673 | |
2674 /** | |
2675 * List of the separators (';', '=' and ',') in the text. | |
2676 * | |
2677 * Starts with the index of the index of the `:` in `data:` of the mimeType. | |
2678 * That is always either -1 or 4, depending on whether `_text` includes the | |
2679 * `data:` scheme or not. | |
2680 * | |
2681 * The first speparator ends the mime type. We don't bother with finding | |
2682 * the '/' inside the mime type. | |
2683 * | |
2684 * Each two separators after that marks a parameter key and value. | |
2685 * | |
2686 * If there is a single separator left, it ends the "base64" marker. | |
2687 * | |
2688 * So the following separators are found for a text: | |
2689 * | |
2690 * data:text/plain;foo=bar;base64,ARGLEBARGLE= | |
2691 * ^ ^ ^ ^ ^ | |
2692 * | |
2693 */ | |
2694 List<int> _separatorIndices; | |
2695 | |
2696 DataUriHelper._(this._text, this._separatorIndices); | |
2697 | |
2698 /** The entire content of the data URI, including the leading `data:`. */ | |
2699 String get text => _separatorIndices[0] == _noScheme ? "data:$_text" : _text; | |
2700 | |
2701 /** | |
2702 * Creates a `data:` URI containing the [content] string. | |
2703 * | |
2704 * Converts the content to a bytes using [encoiding] or the charset specified | |
2705 * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), | |
2706 * then encodes the bytes into the resulting data URI. | |
2707 * | |
2708 * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid | |
2709 * bytes is replaced by a percent encoding). If [base64] is true, the bytes | |
2710 * are instead encoded using [BASE64]. | |
2711 * | |
2712 * If [encoding] is not provided and [parameters] has a `charset` entry, | |
2713 * that name is looked up using [Encoding.getByName], | |
2714 * and if the lookup returns an encoding, that encoding is used to convert | |
2715 * [content] to bytes. | |
2716 * If providing both an [encoding] and a charset [parameter], they should | |
2717 * agree, otherwise decoding won't be able to use the charset parameter | |
2718 * to determind the encoding. | |
2719 * | |
2720 * If [mimeType] and/or [parameters] are supplied, they are added to the | |
2721 * created URI. If any of these contain characters that are not allowed | |
2722 * in the data URI, the character is percent-escaped. If the character is | |
2723 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
2724 * encoded. An omitted [mimeType] in a data URI means `text/plain`, just | |
2725 * as an omitted `charset` parameter defaults to meaning `US-ASCII`. | |
2726 * | |
2727 * To read the content back, use [contentAsString]. | |
2728 */ | |
2729 factory DataUriHelper.fromString(String content, | |
2730 {String mimeType, | |
2731 Encoding encoding, | |
2732 Map<String, String> parameters, | |
2733 bool base64: false}) { | |
2734 StringBuffer buffer = new StringBuffer(); | |
2735 List indices = [_noScheme]; | |
2736 String charsetName; | |
2737 String encodingName; | |
2738 if (parameters != null) charsetName = parameters["charset"]; | |
2739 if (encoding == null) { | |
2740 if (charsetName != null) { | |
2741 encoding = Encoding.getByName(charsetName); | |
2742 } | |
2743 } else if (charsetName == null) { | |
2744 // Non-null only if parameters does not contain "charset". | |
2745 encodingName = encoding.name; | |
2746 } | |
2747 encoding ??= ASCII; | |
2748 _writeUri(mimeType, encodingName, parameters, buffer, indices); | |
2749 indices.add(buffer.length); | |
2750 if (base64) { | |
2751 buffer.write(';base64,'); | |
2752 indices.add(buffer.length - 1); | |
2753 buffer.write(encoding.fuse(BASE64).encode(content)); | |
2754 } else { | |
2755 buffer.write(','); | |
2756 Uri._uriEncodeBytes(_uricTable, encoding.encode(content), buffer, false); | |
2757 } | |
2758 return new DataUriHelper._(buffer.toString(), indices); | |
2759 } | |
2760 | |
2761 /** | |
2762 * Creates a `data:` URI string containing an encoding of [bytes]. | |
2763 * | |
2764 * Defaults to Base64 encoding the bytes, but if [percentEncoded] | |
2765 * is `true`, the bytes will instead be percent encoded (any non-ASCII | |
2766 * or non-valid byte is replaced by a percent encoding). | |
2767 * | |
2768 * To read the bytes back, use [contentAsBytes]. | |
2769 * | |
2770 * It defaults to having the mime-type `application/octet-stream`. | |
2771 * The [mimeType] and [parameters] are added to the created URI. | |
2772 * If any of these contain characters that are not allowed | |
2773 * in the data URI, the character is percent-escaped. If the character is | |
2774 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
2775 * encoded. | |
2776 */ | |
2777 factory DataUriHelper.fromBytes(List<int> bytes, | |
2778 {mimeType: "application/octet-stream", | |
2779 Map<String, String> parameters, | |
2780 percentEncoded: false}) { | |
2781 StringBuffer buffer = new StringBuffer(); | |
2782 List indices = [_noScheme]; | |
2783 _writeUri(mimeType, null, parameters, buffer, indices); | |
2784 indices.add(buffer.length); | |
2785 if (percentEncoded) { | |
2786 buffer.write(','); | |
2787 Uri._uriEncodeBytes(_uricTable, bytes, buffer, false); | |
2788 } else { | |
2789 buffer.write(';base64,'); | |
2790 indices.add(buffer.length - 1); | |
2791 BASE64.encoder.startChunkedConversion( | |
2792 new StringConversionSink.fromStringSink(buffer)) | |
2793 .addSlice(bytes, 0, bytes.length, true); | |
2794 } | |
2795 | |
2796 return new DataUriHelper._(buffer.toString(), indices); | |
2797 } | |
2798 | |
2799 /** | |
2800 * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. | |
2801 * | |
2802 * The [uri] must have scheme `data` and no authority or fragment, | |
2803 * and the path (concatenated with the query, if there is one) must be valid | |
2804 * as data URI content with the same rules as [parse]. | |
2805 */ | |
2806 factory DataUriHelper.fromUri(Uri uri) { | |
2807 if (uri.scheme != "data") { | |
2808 throw new ArgumentError.value(uri, "uri", | |
2809 "Scheme must be 'data'"); | |
2810 } | |
2811 if (uri.hasAuthority) { | |
2812 throw new ArgumentError.value(uri, "uri", | |
2813 "Data uri must not have authority"); | |
2814 } | |
2815 if (uri.hasFragment) { | |
2816 throw new ArgumentError.value(uri, "uri", | |
2817 "Data uri must not have a fragment part"); | |
2818 } | |
2819 if (!uri.hasQuery) { | |
2820 return _parse(uri.path, 0); | |
2821 } | |
2822 // Includes path and query (and leading "data:"). | |
2823 return _parse("$uri", 5); | |
2824 } | |
2825 | |
2826 /** | |
2827 * Writes the initial part of a `data:` uri, from after the "data:" | |
2828 * until just before the ',' before the data, or before a `;base64,` | |
2829 * marker. | |
2830 * | |
2831 * Of an [indices] list is passed, separator indices are stored in that | |
2832 * list. | |
2833 */ | |
2834 static void _writeUri(String mimeType, | |
2835 String charsetName, | |
2836 Map<String, String> parameters, | |
2837 StringBuffer buffer, List indices) { | |
2838 if (mimeType == null || mimeType == "text/plain") { | |
2839 mimeType = ""; | |
2840 } | |
2841 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { | |
2842 buffer.write(mimeType); // Common cases need no escaping. | |
2843 } else { | |
2844 int slashIndex = _validateMimeType(mimeType); | |
2845 if (slashIndex < 0) { | |
2846 throw new ArgumentError.value(mimeType, "mimeType", | |
2847 "Invalid MIME type"); | |
2848 } | |
2849 buffer.write(Uri._uriEncode(_tokenCharTable, | |
2850 mimeType.substring(0, slashIndex))); | |
2851 buffer.write("/"); | |
2852 buffer.write(Uri._uriEncode(_tokenCharTable, | |
2853 mimeType.substring(slashIndex + 1))); | |
2854 } | |
2855 if (charsetName != null) { | |
2856 if (indices != null) { | |
2857 indices..add(buffer.length) | |
2858 ..add(buffer.length + 8); | |
2859 } | |
2860 buffer.write(";charset="); | |
2861 buffer.write(Uri._uriEncode(_tokenCharTable, charsetName)); | |
2862 } | |
2863 parameters?.forEach((var key, var value) { | |
2864 if (key.isEmpty) { | |
2865 throw new ArgumentError.value("", "Parameter names must not be empty"); | |
2866 } | |
2867 if (value.isEmpty) { | |
2868 throw new ArgumentError.value("", "Parameter values must not be empty", | |
2869 'parameters["$key"]'); | |
2870 } | |
2871 if (indices != null) indices.add(buffer.length); | |
2872 buffer.write(';'); | |
2873 // Encode any non-RFC2045-token character and both '%' and '#'. | |
2874 buffer.write(Uri._uriEncode(_tokenCharTable, key)); | |
2875 if (indices != null) indices.add(buffer.length); | |
2876 buffer.write('='); | |
2877 buffer.write(Uri._uriEncode(_tokenCharTable, value)); | |
2878 }); | |
2879 } | |
2880 | |
2881 /** | |
2882 * Checks mimeType is valid-ish (`token '/' token`). | |
2883 * | |
2884 * Returns the index of the slash, or -1 if the mime type is not | |
2885 * considered valid. | |
2886 * | |
2887 * Currently only looks for slashes, all other characters will be | |
2888 * percent-encoded as UTF-8 if necessary. | |
2889 */ | |
2890 static int _validateMimeType(String mimeType) { | |
2891 int slashIndex = -1; | |
2892 for (int i = 0; i < mimeType.length; i++) { | |
2893 var char = mimeType.codeUnitAt(i); | |
2894 if (char != Uri._SLASH) continue; | |
2895 if (slashIndex < 0) { | |
2896 slashIndex = i; | |
2897 continue; | |
2898 } | |
2899 return -1; | |
2900 } | |
2901 return slashIndex; | |
2902 } | |
2903 | |
2904 /** | |
2905 * Parses a string as a `data` URI. | |
2906 * | |
2907 * The string must have the format: | |
2908 * | |
2909 * ``` | |
2910 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat a | |
2911 * ```` | |
2912 * | |
2913 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, | |
2914 * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). | |
2915 * | |
2916 * This means that all the characters must be ASCII, but the URI may contain | |
2917 * percent-escapes for non-ASCII byte values that need an interpretation | |
2918 * to be converted to the corresponding string. | |
2919 * | |
2920 * Parsing doesn't check the validity of any part, it just checks that the | |
2921 * input has the correct structure with the correct sequence of `/`, `;`, `=` | |
2922 * and `,` delimiters. | |
2923 * | |
2924 * Accessing the individual parts may fail later if they turn out to have | |
2925 * content that can't be decoded sucessfully as a string. | |
2926 */ | |
2927 static DataUriHelper parse(String uri) { | |
2928 if (!uri.startsWith("data:")) { | |
2929 throw new FormatException("Does not start with 'data:'", uri, 0); | |
2930 } | |
2931 return _parse(uri, 5); | |
2932 } | |
2933 | |
2934 /** | |
2935 * Converts a `DataUri` to a [Uri]. | |
2936 * | |
2937 * Returns a `Uri` with scheme `data` and the remainder of the data URI | |
2938 * as path. | |
2939 */ | |
2940 Uri toUri() { | |
2941 String path = _text; | |
2942 String query = null; | |
2943 int colonIndex = _separatorIndices[0]; | |
2944 int queryIndex = _text.indexOf('?', colonIndex + 1); | |
2945 int end = null; | |
2946 if (queryIndex >= 0) { | |
2947 query = _text.substring(queryIndex + 1); | |
2948 end = queryIndex; | |
2949 } | |
2950 path = _text.substring(colonIndex + 1, end); | |
2951 // TODO(lrn): This is probably too simple. We should ensure URI | |
2952 // normalization before passing in the raw strings. | |
2953 return new Uri._internal("data", "", null, null, path, query, null); | |
2954 } | |
2955 | |
2956 /** | |
2957 * The MIME type of the data URI. | |
2958 * | |
2959 * A data URI consists of a "media type" followed by data. | |
2960 * The media type starts with a MIME type and can be followed by | |
2961 * extra parameters. | |
2962 * | |
2963 * Example: | |
2964 * | |
2965 * data:text/plain;charset=utf-8,Hello%20World! | |
2966 * | |
2967 * This data URI has the media type `text/plain;charset=utf-8`, which is the | |
2968 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. | |
2969 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. | |
2970 * | |
2971 * If the first part of the data URI is empty, it defaults to `text/plain`. | |
2972 */ | |
2973 String get mimeType { | |
2974 int start = _separatorIndices[0] + 1; | |
2975 int end = _separatorIndices[1]; | |
2976 if (start == end) return "text/plain"; | |
2977 return Uri._uriDecode(_text, start: start, end: end); | |
2978 } | |
2979 | |
2980 /** | |
2981 * The charset parameter of the media type. | |
2982 * | |
2983 * If the parameters of the media type contains a `charset` parameter | |
2984 * then this returns its value, otherwise it returns `US-ASCII`, | |
2985 * which is the default charset for data URIs. | |
2986 */ | |
2987 String get charset { | |
2988 int parameterStart = 1; | |
2989 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. | |
2990 if (isBase64) { | |
2991 // There is a ";base64" separator, so subtract one for that as well. | |
2992 parameterEnd -= 1; | |
2993 } | |
2994 for (int i = parameterStart; i < parameterEnd; i += 2) { | |
2995 var keyStart = _separatorIndices[i] + 1; | |
2996 var keyEnd = _separatorIndices[i + 1]; | |
2997 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { | |
2998 return Uri._uriDecode(_text, start: keyEnd + 1, | |
2999 end: _separatorIndices[i + 2]); | |
3000 } | |
3001 } | |
3002 return "US-ASCII"; | |
3003 } | |
3004 | |
3005 /** | |
3006 * Whether the data is Base64 encoded or not. | |
3007 */ | |
3008 bool get isBase64 => _separatorIndices.length.isOdd; | |
3009 | |
3010 /** | |
3011 * The content part of the data URI, as its actual representation. | |
3012 * | |
3013 * This string may contain percent escapes. | |
3014 */ | |
3015 String get contentText => _text.substring(_separatorIndices.last + 1); | |
3016 | |
3017 /** | |
3018 * The content part of the data URI as bytes. | |
3019 * | |
3020 * If the data is Base64 encoded, it will be decoded to bytes. | |
3021 * | |
3022 * If the data is not Base64 encoded, it will be decoded by unescaping | |
3023 * percent-escaped characters and returning byte values of each unescaped | |
3024 * character. The bytes will not be, e.g., UTF-8 decoded. | |
3025 */ | |
3026 List<int> contentAsBytes() { | |
3027 String text = _text; | |
3028 int start = _separatorIndices.last + 1; | |
3029 if (isBase64) { | |
3030 return BASE64.decode(text.substring(start)); | |
3031 } | |
3032 | |
3033 // Not base64, do percent-decoding and return the remaining bytes. | |
3034 // Compute result size. | |
3035 const int percent = 0x25; | |
3036 int length = text.length - start; | |
3037 for (int i = start; i < text.length; i++) { | |
3038 var codeUnit = text.codeUnitAt(i); | |
3039 if (codeUnit == percent) { | |
3040 i += 2; | |
3041 length -= 2; | |
3042 } | |
3043 } | |
3044 // Fill result array. | |
3045 Uint8List result = new Uint8List(length); | |
3046 if (length == text.length) { | |
3047 result.setRange(0, length, text.codeUnits, start); | |
3048 return result; | |
3049 } | |
3050 int index = 0; | |
3051 for (int i = start; i < text.length; i++) { | |
3052 var codeUnit = text.codeUnitAt(i); | |
3053 if (codeUnit != percent) { | |
3054 result[index++] = codeUnit; | |
3055 } else { | |
3056 if (i + 2 < text.length) { | |
3057 var digit1 = _hexDigit(text.codeUnitAt(i + 1)); | |
3058 var digit2 = _hexDigit(text.codeUnitAt(i + 2)); | |
3059 if (digit1 >= 0 && digit2 >= 0) { | |
3060 int byte = digit1 * 16 + digit2; | |
3061 result[index++] = byte; | |
3062 i += 2; | |
3063 continue; | |
3064 } | |
3065 } | |
3066 throw new FormatException("Invalid percent escape", text, i); | |
3067 } | |
3068 } | |
3069 assert(index == result.length); | |
3070 return result; | |
3071 } | |
3072 | |
3073 // Converts a UTF-16 code-unit to its value as a hex digit. | |
3074 // Returns -1 for non-hex digits. | |
3075 int _hexDigit(int char) { | |
3076 const int char_0 = 0x30; | |
3077 const int char_a = 0x61; | |
3078 const int char_f = 0x66; | |
3079 | |
3080 int digit = char ^ char_0; | |
3081 if (digit <= 9) return digit; | |
3082 int lowerCase = char | 0x20; | |
3083 if (char_a <= lowerCase && lowerCase <= char_f) { | |
3084 return lowerCase - (char_a - 10); | |
3085 } | |
3086 return -1; | |
3087 } | |
3088 | |
3089 /** | |
3090 * Returns a string created from the content of the data URI. | |
3091 * | |
3092 * If the content is Base64 encoded, it will be decoded to bytes and then | |
3093 * decoded to a string using [encoding]. | |
3094 * If encoding is omitted, the value of a `charset` parameter is used | |
3095 * if it is recongized by [Encoding.getByName], otherwise it defaults to | |
3096 * the [ASCII] encoding, which is the default encoding for data URIs | |
3097 * that do not specify an encoding. | |
3098 * | |
3099 * If the content is not Base64 encoded, it will first have percent-escapes | |
3100 * converted to bytes and then the character codes and byte values are | |
3101 * decoded using [encoding]. | |
3102 */ | |
3103 String contentAsString({Encoding encoding}) { | |
3104 if (encoding == null) { | |
3105 var charset = this.charset; // Returns "US-ASCII" if not present. | |
3106 encoding = Encoding.getByName(charset); | |
3107 if (encoding == null) { | |
3108 throw new UnsupportedError("Unknown charset: $charset"); | |
3109 } | |
3110 } | |
3111 String text = _text; | |
3112 int start = _separatorIndices.last + 1; | |
3113 if (isBase64) { | |
3114 var converter = BASE64.decoder.fuse(encoding.decoder); | |
3115 return converter.convert(text.substring(start)); | |
3116 } | |
3117 return Uri._uriDecode(text, start: start, encoding: encoding); | |
3118 } | |
3119 | |
3120 /** | |
3121 * A map representing the parameters of the media type. | |
3122 * | |
3123 * A data URI may contain parameters between the the MIME type and the | |
3124 * data. This converts these parameters to a map from parameter name | |
3125 * to parameter value. | |
3126 * The map only contains parameters that actually occur in the URI. | |
3127 * The `charset` parameter has a default value even if it doesn't occur | |
3128 * in the URI, which is reflected by the [charset] getter. This means that | |
3129 * [charset] may return a value even if `parameters["charset"]` is `null`. | |
3130 * | |
3131 * If the values contain non-ASCII values or percent escapes, they default | |
3132 * to being decoded as UTF-8. | |
3133 */ | |
3134 Map<String, String> get parameters { | |
3135 var result = <String, String>{}; | |
3136 for (int i = 3; i < _separatorIndices.length; i += 2) { | |
3137 var start = _separatorIndices[i - 2] + 1; | |
3138 var equals = _separatorIndices[i - 1]; | |
3139 var end = _separatorIndices[i]; | |
3140 String key = Uri._uriDecode(_text, start: start, end: equals); | |
3141 String value = Uri._uriDecode(_text, start: equals + 1, end: end); | |
3142 result[key] = value; | |
3143 } | |
3144 return result; | |
3145 } | |
3146 | |
3147 static DataUriHelper _parse(String text, int start) { | |
3148 assert(start == 0 || start == 5); | |
3149 assert((start == 5) == text.startsWith("data:")); | |
3150 | |
3151 /// Character codes. | |
3152 const int comma = 0x2c; | |
3153 const int slash = 0x2f; | |
3154 const int semicolon = 0x3b; | |
3155 const int equals = 0x3d; | |
3156 List indices = [start - 1]; | |
3157 int slashIndex = -1; | |
3158 var char; | |
3159 int i = start; | |
3160 for (; i < text.length; i++) { | |
3161 char = text.codeUnitAt(i); | |
3162 if (char == comma || char == semicolon) break; | |
3163 if (char == slash) { | |
3164 if (slashIndex < 0) { | |
3165 slashIndex = i; | |
3166 continue; | |
3167 } | |
3168 throw new FormatException("Invalid MIME type", text, i); | |
3169 } | |
3170 } | |
3171 if (slashIndex < 0 && i > start) { | |
3172 // An empty MIME type is allowed, but if non-empty it must contain | |
3173 // exactly one slash. | |
3174 throw new FormatException("Invalid MIME type", text, i); | |
3175 } | |
3176 while (char != comma) { | |
3177 // parse parameters and/or "base64". | |
3178 indices.add(i); | |
3179 i++; | |
3180 int equalsIndex = -1; | |
3181 for (; i < text.length; i++) { | |
3182 char = text.codeUnitAt(i); | |
3183 if (char == equals) { | |
3184 if (equalsIndex < 0) equalsIndex = i; | |
3185 } else if (char == semicolon || char == comma) { | |
3186 break; | |
3187 } | |
3188 } | |
3189 if (equalsIndex >= 0) { | |
3190 indices.add(equalsIndex); | |
3191 } else { | |
3192 // Have to be final "base64". | |
3193 var lastSeparator = indices.last; | |
3194 if (char != comma || | |
3195 i != lastSeparator + 7 /* "base64,".length */ || | |
3196 !text.startsWith("base64", lastSeparator + 1)) { | |
3197 throw new FormatException("Expecting '='", text, i); | |
3198 } | |
3199 break; | |
3200 } | |
3201 } | |
3202 indices.add(i); | |
3203 return new DataUriHelper._(text, indices); | |
3204 } | |
3205 | |
3206 String toString() => text; | |
3207 | |
3208 // Table of the `token` characters of RFC 2045 in a URI. | |
3209 // | |
3210 // A token is any US-ASCII character except SPACE, control characters and | |
3211 // `tspecial` characters. The `tspecial` category is: | |
3212 // '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='. | |
3213 // | |
3214 // In a data URI, we also need to escape '%' and '#' characters. | |
3215 static const _tokenCharTable = const [ | |
3216 // LSB MSB | |
3217 // | | | |
3218 0x0000, // 0x00 - 0x0f 00000000 00000000 | |
3219 0x0000, // 0x10 - 0x1f 00000000 00000000 | |
3220 // ! $ &' *+ -. | |
3221 0x6cd2, // 0x20 - 0x2f 01001011 00110110 | |
3222 // 01234567 89 | |
3223 0x03ff, // 0x30 - 0x3f 11111111 11000000 | |
3224 // ABCDEFG HIJKLMNO | |
3225 0xfffe, // 0x40 - 0x4f 01111111 11111111 | |
3226 // PQRSTUVW XYZ ^_ | |
3227 0xc7ff, // 0x50 - 0x5f 11111111 11100011 | |
3228 // `abcdefg hijklmno | |
3229 0xffff, // 0x60 - 0x6f 11111111 11111111 | |
3230 // pqrstuvw xyz{|}~ | |
3231 0x7fff]; // 0x70 - 0x7f 11111111 11111110 | |
3232 | |
3233 // All non-escape RFC-2396 uric characters. | |
3234 // | |
3235 // uric = reserved | unreserved | escaped | |
3236 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," | |
3237 // unreserved = alphanum | mark | |
3238 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" | |
3239 static const _uricTable = const [ | |
3240 // LSB MSB | |
3241 // | | | |
3242 0x0000, // 0x00 - 0x0f 0000000000000000 | |
3243 0x0000, // 0x10 - 0x1f 0000000000000000 | |
3244 // ! $ &'()*+,-./ | |
3245 0xffd2, // 0x20 - 0x2f 0100101111111111 | |
3246 // 0123456789:; = ? | |
3247 0xafff, // 0x30 - 0x3f 1111111111110101 | |
3248 // @ABCDEFGHIJKLMNO | |
3249 0xffff, // 0x40 - 0x4f 1111111111111111 | |
3250 // PQRSTUVWXYZ _ | |
3251 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
3252 // abcdefghijklmno | |
3253 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
3254 // pqrstuvwxyz ~ | |
3255 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2613 } | 3256 } |
OLD | NEW |