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 |