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 part of dart.core; |
6 | 6 |
7 /** | 7 /** |
8 * A parsed URI, such as a URL. | 8 * A parsed URI, such as a URL. |
9 * | 9 * |
10 * **See also:** | 10 * **See also:** |
11 * | 11 * |
12 * * [URIs][uris] in the [library tour][libtour] | 12 * * [URIs][uris] in the [library tour][libtour] |
13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) | 13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) |
14 * | 14 * |
15 * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#c
h03-uri | 15 * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris |
16 * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.htm
l | 16 * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.ht
ml |
17 */ | 17 */ |
18 class Uri { | 18 class Uri { |
19 // The host name of the URI. | 19 /** |
20 // Set to `null` if there is no authority in a URI. | 20 * The scheme component of the URI. |
21 final String _host; | |
22 // The port. Set to null if there is no port. Normalized to null if | |
23 // the port is the default port for the scheme. | |
24 // Set to the value of the default port if an empty port was supplied. | |
25 num _port; | |
26 // The path. Always non-null. | |
27 String _path; | |
28 | |
29 /** | |
30 * Returns the scheme component. | |
31 * | 21 * |
32 * Returns the empty string if there is no scheme component. | 22 * Returns the empty string if there is no scheme component. |
| 23 * |
| 24 * A URI scheme is case insensitive. |
| 25 * The returned scheme is canonicalized to lowercase letters. |
33 */ | 26 */ |
34 // We represent the missing scheme as an empty string. | 27 // We represent the missing scheme as an empty string. |
35 // A valid scheme cannot be empty. | 28 // A valid scheme cannot be empty. |
36 final String scheme; | 29 final String scheme; |
37 | 30 |
38 /** | 31 /** |
| 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 Map<String, List<String>> _queryParameterLists; |
| 80 |
| 81 /// Internal non-verifying constructor. Only call with validated arguments. |
| 82 Uri._internal(this.scheme, |
| 83 this._userInfo, |
| 84 this._host, |
| 85 this._port, |
| 86 this._path, |
| 87 this._query, |
| 88 this._fragment); |
| 89 |
| 90 /** |
| 91 * Creates a new URI from its components. |
| 92 * |
| 93 * Each component is set through a named argument. Any number of |
| 94 * components can be provided. The [path] and [query] components can be set |
| 95 * using either of two different named arguments. |
| 96 * |
| 97 * The scheme component is set through [scheme]. The scheme is |
| 98 * normalized to all lowercase letters. If the scheme is omitted or empty, |
| 99 * the URI will not have a scheme part. |
| 100 * |
| 101 * The user info part of the authority component is set through |
| 102 * [userInfo]. It defaults to the empty string, which will be omitted |
| 103 * from the string representation of the URI. |
| 104 * |
| 105 * The host part of the authority component is set through |
| 106 * [host]. The host can either be a hostname, an IPv4 address or an |
| 107 * IPv6 address, contained in '[' and ']'. If the host contains a |
| 108 * ':' character, the '[' and ']' are added if not already provided. |
| 109 * The host is normalized to all lowercase letters. |
| 110 * |
| 111 * The port part of the authority component is set through |
| 112 * [port]. |
| 113 * If [port] is omitted or `null`, it implies the default port for |
| 114 * the URI's scheme, and is equivalent to passing that port explicitly. |
| 115 * The recognized schemes, and their default ports, are "http" (80) and |
| 116 * "https" (443). All other schemes are considered as having zero as the |
| 117 * default port. |
| 118 * |
| 119 * If any of `userInfo`, `host` or `port` are provided, |
| 120 * the URI has an autority according to [hasAuthority]. |
| 121 * |
| 122 * The path component is set through either [path] or |
| 123 * [pathSegments]. |
| 124 * When [path] is used, it should be a valid URI path, |
| 125 * but invalid characters, except the general delimiters ':/@[]?#', |
| 126 * will be escaped if necessary. |
| 127 * When [pathSegments] is used, each of the provided segments |
| 128 * is first percent-encoded and then joined using the forward slash |
| 129 * separator. |
| 130 * |
| 131 * The percent-encoding of the path segments encodes all |
| 132 * characters except for the unreserved characters and the following |
| 133 * list of characters: `!$&'()*+,;=:@`. If the other components |
| 134 * necessitate an absolute path, a leading slash `/` is prepended if |
| 135 * not already there. |
| 136 * |
| 137 * The query component is set through either [query] or [queryParameters]. |
| 138 * When [query] is used, the provided string should be a valid URI query, |
| 139 * but invalid characters, other than general delimiters, |
| 140 * will be escaped if necessary. |
| 141 * When [queryParameters] is used the query is built from the |
| 142 * provided map. Each key and value in the map is percent-encoded |
| 143 * and joined using equal and ampersand characters. |
| 144 * A value in the map must be either a string, or an [Iterable] of strings, |
| 145 * where the latter corresponds to multiple values for the same key. |
| 146 * |
| 147 * The percent-encoding of the keys and values encodes all characters |
| 148 * except for the unreserved characters, and replaces spaces with `+`. |
| 149 * If `query` is the empty string, it is equivalent to omitting it. |
| 150 * To have an actual empty query part, |
| 151 * use an empty map for `queryParameters`. |
| 152 * |
| 153 * If both `query` and `queryParameters` are omitted or `null`, |
| 154 * the URI has no query part. |
| 155 * |
| 156 * The fragment component is set through [fragment]. |
| 157 * It should be a valid URI fragment, but invalid characters other than |
| 158 * general delimiters, are escaped if necessary. |
| 159 * If `fragment` is omitted or `null`, the URI has no fragment part. |
| 160 */ |
| 161 factory Uri({String scheme : "", |
| 162 String userInfo : "", |
| 163 String host, |
| 164 int port, |
| 165 String path, |
| 166 Iterable<String> pathSegments, |
| 167 String query, |
| 168 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
| 169 String fragment}) { |
| 170 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); |
| 171 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
| 172 host = _makeHost(host, 0, _stringOrNullLength(host), false); |
| 173 // Special case this constructor for backwards compatibility. |
| 174 if (query == "") query = null; |
| 175 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| 176 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
| 177 port = _makePort(port, scheme); |
| 178 bool isFile = (scheme == "file"); |
| 179 if (host == null && |
| 180 (userInfo.isNotEmpty || port != null || isFile)) { |
| 181 host = ""; |
| 182 } |
| 183 bool hasAuthority = (host != null); |
| 184 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| 185 scheme, hasAuthority); |
| 186 if (scheme.isEmpty && host == null && !path.startsWith('/')) { |
| 187 path = _normalizeRelativePath(path); |
| 188 } else { |
| 189 path = _removeDotSegments(path); |
| 190 } |
| 191 return new Uri._internal(scheme, userInfo, host, port, |
| 192 path, query, fragment); |
| 193 } |
| 194 |
| 195 /** |
| 196 * Creates a new `http` URI from authority, path and query. |
| 197 * |
| 198 * Examples: |
| 199 * |
| 200 * ``` |
| 201 * // http://example.org/path?q=dart. |
| 202 * new Uri.http("google.com", "/search", { "q" : "dart" }); |
| 203 * |
| 204 * // http://user:pass@localhost:8080 |
| 205 * new Uri.http("user:pass@localhost:8080", ""); |
| 206 * |
| 207 * // http://example.org/a%20b |
| 208 * new Uri.http("example.org", "a b"); |
| 209 * |
| 210 * // http://example.org/a%252F |
| 211 * new Uri.http("example.org", "/a%2F"); |
| 212 * ``` |
| 213 * |
| 214 * The `scheme` is always set to `http`. |
| 215 * |
| 216 * The `userInfo`, `host` and `port` components are set from the |
| 217 * [authority] argument. If `authority` is `null` or empty, |
| 218 * the created `Uri` has no authority, and isn't directly usable |
| 219 * as an HTTP URL, which must have a non-empty host. |
| 220 * |
| 221 * The `path` component is set from the [unencodedPath] |
| 222 * argument. The path passed must not be encoded as this constructor |
| 223 * encodes the path. |
| 224 * |
| 225 * The `query` component is set from the optional [queryParameters] |
| 226 * argument. |
| 227 */ |
| 228 factory Uri.http(String authority, |
| 229 String unencodedPath, |
| 230 [Map<String, String> queryParameters]) { |
| 231 return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
| 232 } |
| 233 |
| 234 /** |
| 235 * Creates a new `https` URI from authority, path and query. |
| 236 * |
| 237 * This constructor is the same as [Uri.http] except for the scheme |
| 238 * which is set to `https`. |
| 239 */ |
| 240 factory Uri.https(String authority, |
| 241 String unencodedPath, |
| 242 [Map<String, String> queryParameters]) { |
| 243 return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
| 244 } |
| 245 |
| 246 /** |
39 * Returns the authority component. | 247 * Returns the authority component. |
40 * | 248 * |
41 * The authority is formatted from the [userInfo], [host] and [port] | 249 * The authority is formatted from the [userInfo], [host] and [port] |
42 * parts. | 250 * parts. |
43 * | 251 * |
44 * Returns the empty string if there is no authority component. | 252 * Returns the empty string if there is no authority component. |
45 */ | 253 */ |
46 String get authority { | 254 String get authority { |
47 if (!hasAuthority) return ""; | 255 if (!hasAuthority) return ""; |
48 var sb = new StringBuffer(); | 256 var sb = new StringBuffer(); |
49 _writeAuthority(sb); | 257 _writeAuthority(sb); |
50 return sb.toString(); | 258 return sb.toString(); |
51 } | 259 } |
52 | 260 |
53 /** | 261 /** |
54 * The user-info part of the authority. | |
55 * | |
56 * Does not distinguish between an empty user-info and an absent one. | |
57 * The value is always non-null. | |
58 */ | |
59 final String _userInfo; | |
60 | |
61 /** | |
62 * Returns the user info part of the authority component. | 262 * Returns the user info part of the authority component. |
63 * | 263 * |
64 * Returns the empty string if there is no user info in the | 264 * Returns the empty string if there is no user info in the |
65 * authority component. | 265 * authority component. |
66 */ | 266 */ |
67 String get userInfo => _userInfo; | 267 String get userInfo => _userInfo; |
68 | 268 |
69 /** | 269 /** |
70 * Returns the host part of the authority component. | 270 * Returns the host part of the authority component. |
71 * | 271 * |
72 * Returns the empty string if there is no authority component and | 272 * Returns the empty string if there is no authority component and |
73 * hence no host. | 273 * hence no host. |
74 * | 274 * |
75 * If the host is an IP version 6 address, the surrounding `[` and `]` is | 275 * If the host is an IP version 6 address, the surrounding `[` and `]` is |
76 * removed. | 276 * removed. |
| 277 * |
| 278 * The host string is case-insensitive. |
| 279 * The returned host name is canonicalized to lower-case |
| 280 * with upper-case percent-escapes. |
77 */ | 281 */ |
78 String get host { | 282 String get host { |
79 if (_host == null) return ""; | 283 if (_host == null) return ""; |
80 if (_host.startsWith('[')) { | 284 if (_host.startsWith('[')) { |
81 return _host.substring(1, _host.length - 1); | 285 return _host.substring(1, _host.length - 1); |
82 } | 286 } |
83 return _host; | 287 return _host; |
84 } | 288 } |
85 | 289 |
86 /** | 290 /** |
(...skipping 17 matching lines...) Expand all Loading... |
104 /** | 308 /** |
105 * Returns the path component. | 309 * Returns the path component. |
106 * | 310 * |
107 * The returned path is encoded. To get direct access to the decoded | 311 * The returned path is encoded. To get direct access to the decoded |
108 * path use [pathSegments]. | 312 * path use [pathSegments]. |
109 * | 313 * |
110 * Returns the empty string if there is no path component. | 314 * Returns the empty string if there is no path component. |
111 */ | 315 */ |
112 String get path => _path; | 316 String get path => _path; |
113 | 317 |
114 // The query content, or null if there is no query. | |
115 final String _query; | |
116 | |
117 /** | 318 /** |
118 * Returns the query component. The returned query is encoded. To get | 319 * Returns the query component. The returned query is encoded. To get |
119 * direct access to the decoded query use [queryParameters]. | 320 * direct access to the decoded query use [queryParameters]. |
120 * | 321 * |
121 * Returns the empty string if there is no query component. | 322 * Returns the empty string if there is no query component. |
122 */ | 323 */ |
123 String get query => (_query == null) ? "" : _query; | 324 String get query => (_query == null) ? "" : _query; |
124 | 325 |
125 // The fragment content, or null if there is no fragment. | |
126 final String _fragment; | |
127 | |
128 /** | 326 /** |
129 * Returns the fragment identifier component. | 327 * Returns the fragment identifier component. |
130 * | 328 * |
131 * Returns the empty string if there is no fragment identifier | 329 * Returns the empty string if there is no fragment identifier |
132 * component. | 330 * component. |
133 */ | 331 */ |
134 String get fragment => (_fragment == null) ? "" : _fragment; | 332 String get fragment => (_fragment == null) ? "" : _fragment; |
135 | 333 |
136 /** | 334 /** |
137 * Cache the computed return value of [pathSegements]. | |
138 */ | |
139 List<String> _pathSegments; | |
140 | |
141 /** | |
142 * Cache the computed return value of [queryParameters]. | |
143 */ | |
144 Map<String, String> _queryParameters; | |
145 | |
146 /** | |
147 * Creates a new `Uri` object by parsing a URI string. | 335 * Creates a new `Uri` object by parsing a URI string. |
148 * | 336 * |
| 337 * If [start] and [end] are provided, only the substring from `start` |
| 338 * to `end` is parsed as a URI. |
| 339 * |
149 * If the string is not valid as a URI or URI reference, | 340 * If the string is not valid as a URI or URI reference, |
150 * invalid characters will be percent escaped where possible. | 341 * a [FormatException] is thrown. |
151 * The resulting `Uri` will represent a valid URI or URI reference. | |
152 */ | 342 */ |
153 static Uri parse(String uri) { | 343 static Uri parse(String uri, [int start = 0, int end]) { |
154 // This parsing will not validate percent-encoding, IPv6, etc. When done | 344 // This parsing will not validate percent-encoding, IPv6, etc. |
155 // it will call `new Uri(...)` which will perform these validations. | 345 // When done splitting into parts, it will call, e.g., [_makeFragment] |
156 // This is purely splitting up the URI string into components. | 346 // to do the final parsing. |
157 // | 347 // |
158 // Important parts of the RFC 3986 used here: | 348 // Important parts of the RFC 3986 used here: |
159 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] | 349 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] |
160 // | 350 // |
161 // hier-part = "//" authority path-abempty | 351 // hier-part = "//" authority path-abempty |
162 // / path-absolute | 352 // / path-absolute |
163 // / path-rootless | 353 // / path-rootless |
164 // / path-empty | 354 // / path-empty |
165 // | 355 // |
166 // URI-reference = URI / relative-ref | 356 // URI-reference = URI / relative-ref |
(...skipping 30 matching lines...) Expand all Loading... |
197 // segment = *pchar | 387 // segment = *pchar |
198 // segment-nz = 1*pchar | 388 // segment-nz = 1*pchar |
199 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 389 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
200 // ; non-zero-length segment without any colon ":" | 390 // ; non-zero-length segment without any colon ":" |
201 // | 391 // |
202 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 392 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
203 // | 393 // |
204 // query = *( pchar / "/" / "?" ) | 394 // query = *( pchar / "/" / "?" ) |
205 // | 395 // |
206 // fragment = *( pchar / "/" / "?" ) | 396 // fragment = *( pchar / "/" / "?" ) |
207 bool isRegName(int ch) { | |
208 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | |
209 } | |
210 const int EOI = -1; | 397 const int EOI = -1; |
211 | 398 |
212 String scheme = ""; | 399 String scheme = ""; |
213 String userinfo = ""; | 400 String userinfo = ""; |
214 String host = null; | 401 String host = null; |
215 num port = null; | 402 int port = null; |
216 String path = null; | 403 String path = null; |
217 String query = null; | 404 String query = null; |
218 String fragment = null; | 405 String fragment = null; |
| 406 if (end == null) end = uri.length; |
219 | 407 |
220 int index = 0; | 408 int index = start; |
221 int pathStart = 0; | 409 int pathStart = start; |
222 // End of input-marker. | 410 // End of input-marker. |
223 int char = EOI; | 411 int char = EOI; |
224 | 412 |
225 void parseAuth() { | 413 void parseAuth() { |
226 if (index == uri.length) { | 414 if (index == end) { |
227 char = EOI; | 415 char = EOI; |
228 return; | 416 return; |
229 } | 417 } |
230 int authStart = index; | 418 int authStart = index; |
231 int lastColon = -1; | 419 int lastColon = -1; |
232 int lastAt = -1; | 420 int lastAt = -1; |
233 char = uri.codeUnitAt(index); | 421 char = uri.codeUnitAt(index); |
234 while (index < uri.length) { | 422 while (index < end) { |
235 char = uri.codeUnitAt(index); | 423 char = uri.codeUnitAt(index); |
236 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { | 424 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { |
237 break; | 425 break; |
238 } | 426 } |
239 if (char == _AT_SIGN) { | 427 if (char == _AT_SIGN) { |
240 lastAt = index; | 428 lastAt = index; |
241 lastColon = -1; | 429 lastColon = -1; |
242 } else if (char == _COLON) { | 430 } else if (char == _COLON) { |
243 lastColon = index; | 431 lastColon = index; |
244 } else if (char == _LEFT_BRACKET) { | 432 } else if (char == _LEFT_BRACKET) { |
245 lastColon = -1; | 433 lastColon = -1; |
246 int endBracket = uri.indexOf(']', index + 1); | 434 int endBracket = uri.indexOf(']', index + 1); |
247 if (endBracket == -1) { | 435 if (endBracket == -1) { |
248 index = uri.length; | 436 index = end; |
249 char = EOI; | 437 char = EOI; |
250 break; | 438 break; |
251 } else { | 439 } else { |
252 index = endBracket; | 440 index = endBracket; |
253 } | 441 } |
254 } | 442 } |
255 index++; | 443 index++; |
256 char = EOI; | 444 char = EOI; |
257 } | 445 } |
258 int hostStart = authStart; | 446 int hostStart = authStart; |
(...skipping 11 matching lines...) Expand all Loading... |
270 if (_ZERO > digit || _NINE < digit) { | 458 if (_ZERO > digit || _NINE < digit) { |
271 _fail(uri, i, "Invalid port number"); | 459 _fail(uri, i, "Invalid port number"); |
272 } | 460 } |
273 portNumber = portNumber * 10 + (digit - _ZERO); | 461 portNumber = portNumber * 10 + (digit - _ZERO); |
274 } | 462 } |
275 } | 463 } |
276 port = _makePort(portNumber, scheme); | 464 port = _makePort(portNumber, scheme); |
277 hostEnd = lastColon; | 465 hostEnd = lastColon; |
278 } | 466 } |
279 host = _makeHost(uri, hostStart, hostEnd, true); | 467 host = _makeHost(uri, hostStart, hostEnd, true); |
280 if (index < uri.length) { | 468 if (index < end) { |
281 char = uri.codeUnitAt(index); | 469 char = uri.codeUnitAt(index); |
282 } | 470 } |
283 } | 471 } |
284 | 472 |
285 // When reaching path parsing, the current character is known to not | 473 // When reaching path parsing, the current character is known to not |
286 // be part of the path. | 474 // be part of the path. |
287 const int NOT_IN_PATH = 0; | 475 const int NOT_IN_PATH = 0; |
288 // When reaching path parsing, the current character is part | 476 // When reaching path parsing, the current character is part |
289 // of the a non-empty path. | 477 // of the a non-empty path. |
290 const int IN_PATH = 1; | 478 const int IN_PATH = 1; |
291 // When reaching authority parsing, authority is possible. | 479 // When reaching authority parsing, authority is possible. |
292 // This is only true at start or right after scheme. | 480 // This is only true at start or right after scheme. |
293 const int ALLOW_AUTH = 2; | 481 const int ALLOW_AUTH = 2; |
294 | 482 |
295 // Current state. | 483 // Current state. |
296 // Initialized to the default value that is used when exiting the | 484 // Initialized to the default value that is used when exiting the |
297 // scheme loop by reaching the end of input. | 485 // scheme loop by reaching the end of input. |
298 // All other breaks set their own state. | 486 // All other breaks set their own state. |
299 int state = NOT_IN_PATH; | 487 int state = NOT_IN_PATH; |
300 int i = index; // Temporary alias for index to avoid bug 19550 in dart2js. | 488 int i = index; // Temporary alias for index to avoid bug 19550 in dart2js. |
301 while (i < uri.length) { | 489 while (i < end) { |
302 char = uri.codeUnitAt(i); | 490 char = uri.codeUnitAt(i); |
303 if (char == _QUESTION || char == _NUMBER_SIGN) { | 491 if (char == _QUESTION || char == _NUMBER_SIGN) { |
304 state = NOT_IN_PATH; | 492 state = NOT_IN_PATH; |
305 break; | 493 break; |
306 } | 494 } |
307 if (char == _SLASH) { | 495 if (char == _SLASH) { |
308 state = (i == 0) ? ALLOW_AUTH : IN_PATH; | 496 state = (i == start) ? ALLOW_AUTH : IN_PATH; |
309 break; | 497 break; |
310 } | 498 } |
311 if (char == _COLON) { | 499 if (char == _COLON) { |
312 if (i == 0) _fail(uri, 0, "Invalid empty scheme"); | 500 if (i == start) _fail(uri, start, "Invalid empty scheme"); |
313 scheme = _makeScheme(uri, i); | 501 scheme = _makeScheme(uri, start, i); |
314 i++; | 502 i++; |
315 pathStart = i; | 503 pathStart = i; |
316 if (i == uri.length) { | 504 if (i == end) { |
317 char = EOI; | 505 char = EOI; |
318 state = NOT_IN_PATH; | 506 state = NOT_IN_PATH; |
319 } else { | 507 } else { |
320 char = uri.codeUnitAt(i); | 508 char = uri.codeUnitAt(i); |
321 if (char == _QUESTION || char == _NUMBER_SIGN) { | 509 if (char == _QUESTION || char == _NUMBER_SIGN) { |
322 state = NOT_IN_PATH; | 510 state = NOT_IN_PATH; |
323 } else if (char == _SLASH) { | 511 } else if (char == _SLASH) { |
324 state = ALLOW_AUTH; | 512 state = ALLOW_AUTH; |
325 } else { | 513 } else { |
326 state = IN_PATH; | 514 state = IN_PATH; |
327 } | 515 } |
328 } | 516 } |
329 break; | 517 break; |
330 } | 518 } |
331 i++; | 519 i++; |
332 char = EOI; | 520 char = EOI; |
333 } | 521 } |
334 index = i; // Remove alias when bug is fixed. | 522 index = i; // Remove alias when bug is fixed. |
335 | 523 |
336 if (state == ALLOW_AUTH) { | 524 if (state == ALLOW_AUTH) { |
337 assert(char == _SLASH); | 525 assert(char == _SLASH); |
338 // Have seen one slash either at start or right after scheme. | 526 // Have seen one slash either at start or right after scheme. |
339 // If two slashes, it's an authority, otherwise it's just the path. | 527 // If two slashes, it's an authority, otherwise it's just the path. |
340 index++; | 528 index++; |
341 if (index == uri.length) { | 529 if (index == end) { |
342 char = EOI; | 530 char = EOI; |
343 state = NOT_IN_PATH; | 531 state = NOT_IN_PATH; |
344 } else { | 532 } else { |
345 char = uri.codeUnitAt(index); | 533 char = uri.codeUnitAt(index); |
346 if (char == _SLASH) { | 534 if (char == _SLASH) { |
347 index++; | 535 index++; |
348 parseAuth(); | 536 parseAuth(); |
349 pathStart = index; | 537 pathStart = index; |
350 } | 538 } |
351 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { | 539 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { |
352 state = NOT_IN_PATH; | 540 state = NOT_IN_PATH; |
353 } else { | 541 } else { |
354 state = IN_PATH; | 542 state = IN_PATH; |
355 } | 543 } |
356 } | 544 } |
357 } | 545 } |
358 | 546 |
359 assert(state == IN_PATH || state == NOT_IN_PATH); | 547 assert(state == IN_PATH || state == NOT_IN_PATH); |
360 if (state == IN_PATH) { | 548 if (state == IN_PATH) { |
361 // Characters from pathStart to index (inclusive) are known | 549 // Characters from pathStart to index (inclusive) are known |
362 // to be part of the path. | 550 // to be part of the path. |
363 while (++index < uri.length) { | 551 while (++index < end) { |
364 char = uri.codeUnitAt(index); | 552 char = uri.codeUnitAt(index); |
365 if (char == _QUESTION || char == _NUMBER_SIGN) { | 553 if (char == _QUESTION || char == _NUMBER_SIGN) { |
366 break; | 554 break; |
367 } | 555 } |
368 char = EOI; | 556 char = EOI; |
369 } | 557 } |
370 state = NOT_IN_PATH; | 558 state = NOT_IN_PATH; |
371 } | 559 } |
372 | 560 |
373 assert(state == NOT_IN_PATH); | 561 assert(state == NOT_IN_PATH); |
374 bool isFile = (scheme == "file"); | 562 bool hasAuthority = (host != null); |
375 bool ensureLeadingSlash = host != null; | 563 path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); |
376 path = _makePath(uri, pathStart, index, null, ensureLeadingSlash, isFile); | |
377 | 564 |
378 if (char == _QUESTION) { | 565 if (char == _QUESTION) { |
379 int numberSignIndex = uri.indexOf('#', index + 1); | 566 int numberSignIndex = -1; |
| 567 for (int i = index + 1; i < end; i++) { |
| 568 if (uri.codeUnitAt(i) == _NUMBER_SIGN) { |
| 569 numberSignIndex = i; |
| 570 break; |
| 571 } |
| 572 } |
380 if (numberSignIndex < 0) { | 573 if (numberSignIndex < 0) { |
381 query = _makeQuery(uri, index + 1, uri.length, null); | 574 query = _makeQuery(uri, index + 1, end, null); |
382 } else { | 575 } else { |
383 query = _makeQuery(uri, index + 1, numberSignIndex, null); | 576 query = _makeQuery(uri, index + 1, numberSignIndex, null); |
384 fragment = _makeFragment(uri, numberSignIndex + 1, uri.length); | 577 fragment = _makeFragment(uri, numberSignIndex + 1, end); |
385 } | 578 } |
386 } else if (char == _NUMBER_SIGN) { | 579 } else if (char == _NUMBER_SIGN) { |
387 fragment = _makeFragment(uri, index + 1, uri.length); | 580 fragment = _makeFragment(uri, index + 1, end); |
388 } | 581 } |
389 return new Uri._internal(scheme, | 582 return new Uri._internal(scheme, |
390 userinfo, | 583 userinfo, |
391 host, | 584 host, |
392 port, | 585 port, |
393 path, | 586 path, |
394 query, | 587 query, |
395 fragment); | 588 fragment); |
396 } | 589 } |
397 | 590 |
398 // Report a parse failure. | 591 // Report a parse failure. |
399 static void _fail(String uri, int index, String message) { | 592 static void _fail(String uri, int index, String message) { |
400 throw new FormatException(message, uri, index); | 593 throw new FormatException(message, uri, index); |
401 } | 594 } |
402 | 595 |
403 /// Internal non-verifying constructor. Only call with validated arguments. | |
404 Uri._internal(this.scheme, | |
405 this._userInfo, | |
406 this._host, | |
407 this._port, | |
408 this._path, | |
409 this._query, | |
410 this._fragment); | |
411 | |
412 /** | |
413 * Creates a new URI from its components. | |
414 * | |
415 * Each component is set through a named argument. Any number of | |
416 * components can be provided. The [path] and [query] components can be set | |
417 * using either of two different named arguments. | |
418 * | |
419 * The scheme component is set through [scheme]. The scheme is | |
420 * normalized to all lowercase letters. If the scheme is omitted or empty, | |
421 * the URI will not have a scheme part. | |
422 * | |
423 * The user info part of the authority component is set through | |
424 * [userInfo]. It defaults to the empty string, which will be omitted | |
425 * from the string representation of the URI. | |
426 * | |
427 * The host part of the authority component is set through | |
428 * [host]. The host can either be a hostname, an IPv4 address or an | |
429 * IPv6 address, contained in '[' and ']'. If the host contains a | |
430 * ':' character, the '[' and ']' are added if not already provided. | |
431 * The host is normalized to all lowercase letters. | |
432 * | |
433 * The port part of the authority component is set through | |
434 * [port]. | |
435 * If [port] is omitted or `null`, it implies the default port for | |
436 * the URI's scheme, and is equivalent to passing that port explicitly. | |
437 * The recognized schemes, and their default ports, are "http" (80) and | |
438 * "https" (443). All other schemes are considered as having zero as the | |
439 * default port. | |
440 * | |
441 * If any of `userInfo`, `host` or `port` are provided, | |
442 * the URI will have an autority according to [hasAuthority]. | |
443 * | |
444 * The path component is set through either [path] or | |
445 * [pathSegments]. When [path] is used, it should be a valid URI path, | |
446 * but invalid characters, except the general delimiters ':/@[]?#', | |
447 * will be escaped if necessary. | |
448 * When [pathSegments] is used, each of the provided segments | |
449 * is first percent-encoded and then joined using the forward slash | |
450 * separator. The percent-encoding of the path segments encodes all | |
451 * characters except for the unreserved characters and the following | |
452 * list of characters: `!$&'()*+,;=:@`. If the other components | |
453 * calls for an absolute path a leading slash `/` is prepended if | |
454 * not already there. | |
455 * | |
456 * The query component is set through either [query] or | |
457 * [queryParameters]. When [query] is used the provided string should | |
458 * be a valid URI query, but invalid characters other than general delimiters, | |
459 * will be escaped if necessary. | |
460 * When [queryParameters] is used the query is built from the | |
461 * provided map. Each key and value in the map is percent-encoded | |
462 * and joined using equal and ampersand characters. The | |
463 * percent-encoding of the keys and values encodes all characters | |
464 * except for the unreserved characters. | |
465 * If `query` is the empty string, it is equivalent to omitting it. | |
466 * To have an actual empty query part, | |
467 * use an empty list for `queryParameters`. | |
468 * If both `query` and `queryParameters` are omitted or `null`, the | |
469 * URI will have no query part. | |
470 * | |
471 * The fragment component is set through [fragment]. | |
472 * It should be a valid URI fragment, but invalid characters other than | |
473 * general delimiters, will be escaped if necessary. | |
474 * If `fragment` is omitted or `null`, the URI will have no fragment part. | |
475 */ | |
476 factory Uri({String scheme : "", | |
477 String userInfo : "", | |
478 String host, | |
479 int port, | |
480 String path, | |
481 Iterable<String> pathSegments, | |
482 String query, | |
483 Map<String, String> queryParameters, | |
484 String fragment}) { | |
485 scheme = _makeScheme(scheme, _stringOrNullLength(scheme)); | |
486 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
487 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
488 // Special case this constructor for backwards compatibility. | |
489 if (query == "") query = null; | |
490 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
491 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
492 port = _makePort(port, scheme); | |
493 bool isFile = (scheme == "file"); | |
494 if (host == null && | |
495 (userInfo.isNotEmpty || port != null || isFile)) { | |
496 host = ""; | |
497 } | |
498 bool ensureLeadingSlash = host != null; | |
499 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
500 ensureLeadingSlash, isFile); | |
501 return new Uri._internal(scheme, userInfo, host, port, | |
502 path, query, fragment); | |
503 } | |
504 | |
505 /** | |
506 * Creates a new `http` URI from authority, path and query. | |
507 * | |
508 * Examples: | |
509 * | |
510 * ``` | |
511 * // http://example.org/path?q=dart. | |
512 * new Uri.http("google.com", "/search", { "q" : "dart" }); | |
513 * | |
514 * // http://user:pass@localhost:8080 | |
515 * new Uri.http("user:pass@localhost:8080", ""); | |
516 * | |
517 * // http://example.org/a%20b | |
518 * new Uri.http("example.org", "a b"); | |
519 * | |
520 * // http://example.org/a%252F | |
521 * new Uri.http("example.org", "/a%2F"); | |
522 * ``` | |
523 * | |
524 * The `scheme` is always set to `http`. | |
525 * | |
526 * The `userInfo`, `host` and `port` components are set from the | |
527 * [authority] argument. If `authority` is `null` or empty, | |
528 * the created `Uri` will have no authority, and will not be directly usable | |
529 * as an HTTP URL, which must have a non-empty host. | |
530 * | |
531 * The `path` component is set from the [unencodedPath] | |
532 * argument. The path passed must not be encoded as this constructor | |
533 * encodes the path. | |
534 * | |
535 * The `query` component is set from the optional [queryParameters] | |
536 * argument. | |
537 */ | |
538 factory Uri.http(String authority, | |
539 String unencodedPath, | |
540 [Map<String, String> queryParameters]) { | |
541 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
542 } | |
543 | |
544 /** | |
545 * Creates a new `https` URI from authority, path and query. | |
546 * | |
547 * This constructor is the same as [Uri.http] except for the scheme | |
548 * which is set to `https`. | |
549 */ | |
550 factory Uri.https(String authority, | |
551 String unencodedPath, | |
552 [Map<String, String> queryParameters]) { | |
553 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | |
554 } | |
555 | |
556 static Uri _makeHttpUri(String scheme, | 596 static Uri _makeHttpUri(String scheme, |
557 String authority, | 597 String authority, |
558 String unencodedPath, | 598 String unencodedPath, |
559 Map<String, String> queryParameters) { | 599 Map<String, String> queryParameters) { |
560 var userInfo = ""; | 600 var userInfo = ""; |
561 var host = null; | 601 var host = null; |
562 var port = null; | 602 var port = null; |
563 | 603 |
564 if (authority != null && authority.isNotEmpty) { | 604 if (authority != null && authority.isNotEmpty) { |
565 var hostStart = 0; | 605 var hostStart = 0; |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
687 * // This throws an error. A path with a drive letter is not absolute. | 727 * // This throws an error. A path with a drive letter is not absolute. |
688 * new Uri.file(r"C:xxx\yyy", windows: true); | 728 * new Uri.file(r"C:xxx\yyy", windows: true); |
689 * | 729 * |
690 * // file://server/share/file | 730 * // file://server/share/file |
691 * new Uri.file(r"\\server\share\file", windows: true); | 731 * new Uri.file(r"\\server\share\file", windows: true); |
692 * ``` | 732 * ``` |
693 * | 733 * |
694 * If the path passed is not a legal file path [ArgumentError] is thrown. | 734 * If the path passed is not a legal file path [ArgumentError] is thrown. |
695 */ | 735 */ |
696 factory Uri.file(String path, {bool windows}) { | 736 factory Uri.file(String path, {bool windows}) { |
697 windows = windows == null ? Uri._isWindows : windows; | 737 windows = (windows == null) ? Uri._isWindows : windows; |
698 return windows ? _makeWindowsFileUrl(path) : _makeFileUri(path); | 738 return windows ? _makeWindowsFileUrl(path, false) |
| 739 : _makeFileUri(path, false); |
699 } | 740 } |
700 | 741 |
701 /** | 742 /** |
| 743 * Like [Uri.file] except that a non-empty URI path ends in a slash. |
| 744 * |
| 745 * If [path] is not empty, and it doesn't end in a directory separator, |
| 746 * then a slash is added to the returned URI's path. |
| 747 * In all other cases, the result is the same as returned by `Uri.file`. |
| 748 */ |
| 749 factory Uri.directory(String path, {bool windows}) { |
| 750 windows = (windows == null) ? Uri._isWindows : windows; |
| 751 return windows ? _makeWindowsFileUrl(path, true) |
| 752 : _makeFileUri(path, true); |
| 753 } |
| 754 |
| 755 /** |
| 756 * Creates a `data:` URI containing the [content] string. |
| 757 * |
| 758 * Converts the content to a bytes using [encoding] or the charset specified |
| 759 * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), |
| 760 * then encodes the bytes into the resulting data URI. |
| 761 * |
| 762 * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid |
| 763 * bytes is replaced by a percent encoding). If [base64] is true, the bytes |
| 764 * are instead encoded using [BASE64]. |
| 765 * |
| 766 * If [encoding] is not provided and [parameters] has a `charset` entry, |
| 767 * that name is looked up using [Encoding.getByName], |
| 768 * and if the lookup returns an encoding, that encoding is used to convert |
| 769 * [content] to bytes. |
| 770 * If providing both an [encoding] and a charset [parameter], they should |
| 771 * agree, otherwise decoding won't be able to use the charset parameter |
| 772 * to determine the encoding. |
| 773 * |
| 774 * If [mimeType] and/or [parameters] are supplied, they are added to the |
| 775 * created URI. If any of these contain characters that are not allowed |
| 776 * in the data URI, the character is percent-escaped. If the character is |
| 777 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| 778 * encoded. An omitted [mimeType] in a data URI means `text/plain`, just |
| 779 * as an omitted `charset` parameter defaults to meaning `US-ASCII`. |
| 780 * |
| 781 * To read the content back, use [UriData.contentAsString]. |
| 782 */ |
| 783 factory Uri.dataFromString(String content, |
| 784 {String mimeType, |
| 785 Encoding encoding, |
| 786 Map<String, String> parameters, |
| 787 bool base64: false}) { |
| 788 UriData data = new UriData.fromString(content, |
| 789 mimeType: mimeType, |
| 790 encoding: encoding, |
| 791 parameters: parameters, |
| 792 base64: base64); |
| 793 return data.uri; |
| 794 } |
| 795 |
| 796 /** |
| 797 * Creates a `data:` URI containing an encoding of [bytes]. |
| 798 * |
| 799 * Defaults to Base64 encoding the bytes, but if [percentEncoded] |
| 800 * is `true`, the bytes will instead be percent encoded (any non-ASCII |
| 801 * or non-valid-ASCII-character byte is replaced by a percent encoding). |
| 802 * |
| 803 * To read the bytes back, use [UriData.contentAsBytes]. |
| 804 * |
| 805 * It defaults to having the mime-type `application/octet-stream`. |
| 806 * The [mimeType] and [parameters] are added to the created URI. |
| 807 * If any of these contain characters that are not allowed |
| 808 * in the data URI, the character is percent-escaped. If the character is |
| 809 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| 810 * encoded. |
| 811 */ |
| 812 factory Uri.dataFromBytes(List<int> bytes, |
| 813 {mimeType: "application/octet-stream", |
| 814 Map<String, String> parameters, |
| 815 percentEncoded: false}) { |
| 816 UriData data = new UriData.fromBytes(bytes, |
| 817 mimeType: mimeType, |
| 818 parameters: parameters, |
| 819 percentEncoded: percentEncoded); |
| 820 return data.uri; |
| 821 } |
| 822 |
| 823 /** |
702 * Returns the natural base URI for the current platform. | 824 * Returns the natural base URI for the current platform. |
703 * | 825 * |
704 * When running in a browser this is the current URL (from | 826 * When running in a browser this is the current URL (from |
705 * `window.location.href`). | 827 * `window.location.href`). |
706 * | 828 * |
707 * When not running in a browser this is the file URI referencing | 829 * When not running in a browser this is the file URI referencing |
708 * the current working directory. | 830 * the current working directory. |
709 */ | 831 */ |
710 external static Uri get base; | 832 external static Uri get base; |
711 | 833 |
712 external static bool get _isWindows; | 834 external static bool get _isWindows; |
713 | 835 |
714 static _checkNonWindowsPathReservedCharacters(List<String> segments, | 836 static _checkNonWindowsPathReservedCharacters(List<String> segments, |
715 bool argumentError) { | 837 bool argumentError) { |
716 segments.forEach((segment) { | 838 segments.forEach((segment) { |
717 if (segment.contains("/")) { | 839 if (segment.contains("/")) { |
718 if (argumentError) { | 840 if (argumentError) { |
719 throw new ArgumentError("Illegal path character $segment"); | 841 throw new ArgumentError("Illegal path character $segment"); |
720 } else { | 842 } else { |
721 throw new UnsupportedError("Illegal path character $segment"); | 843 throw new UnsupportedError("Illegal path character $segment"); |
722 } | 844 } |
723 } | 845 } |
724 }); | 846 }); |
725 } | 847 } |
726 | 848 |
727 static _checkWindowsPathReservedCharacters(List<String> segments, | 849 static _checkWindowsPathReservedCharacters(List<String> segments, |
728 bool argumentError, | 850 bool argumentError, |
729 [int firstSegment = 0]) { | 851 [int firstSegment = 0]) { |
730 segments.skip(firstSegment).forEach((segment) { | 852 for (var segment in segments.skip(firstSegment)) { |
731 if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { | 853 if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { |
732 if (argumentError) { | 854 if (argumentError) { |
733 throw new ArgumentError("Illegal character in path"); | 855 throw new ArgumentError("Illegal character in path"); |
734 } else { | 856 } else { |
735 throw new UnsupportedError("Illegal character in path"); | 857 throw new UnsupportedError("Illegal character in path"); |
736 } | 858 } |
737 } | 859 } |
738 }); | 860 } |
739 } | 861 } |
740 | 862 |
741 static _checkWindowsDriveLetter(int charCode, bool argumentError) { | 863 static _checkWindowsDriveLetter(int charCode, bool argumentError) { |
742 if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) || | 864 if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) || |
743 (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) { | 865 (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) { |
744 return; | 866 return; |
745 } | 867 } |
746 if (argumentError) { | 868 if (argumentError) { |
747 throw new ArgumentError("Illegal drive letter " + | 869 throw new ArgumentError("Illegal drive letter " + |
748 new String.fromCharCode(charCode)); | 870 new String.fromCharCode(charCode)); |
749 } else { | 871 } else { |
750 throw new UnsupportedError("Illegal drive letter " + | 872 throw new UnsupportedError("Illegal drive letter " + |
751 new String.fromCharCode(charCode)); | 873 new String.fromCharCode(charCode)); |
752 } | 874 } |
753 } | 875 } |
754 | 876 |
755 static _makeFileUri(String path) { | 877 static _makeFileUri(String path, bool slashTerminated) { |
756 String sep = "/"; | 878 const String sep = "/"; |
757 if (path.startsWith(sep)) { | 879 var segments = path.split(sep); |
| 880 if (slashTerminated && |
| 881 segments.isNotEmpty && |
| 882 segments.last.isNotEmpty) { |
| 883 segments.add(""); // Extra separator at end. |
| 884 } |
| 885 if (path.startsWith(sep)) { |
758 // Absolute file:// URI. | 886 // Absolute file:// URI. |
759 return new Uri(scheme: "file", pathSegments: path.split(sep)); | 887 return new Uri(scheme: "file", pathSegments: segments); |
760 } else { | 888 } else { |
761 // Relative URI. | 889 // Relative URI. |
762 return new Uri(pathSegments: path.split(sep)); | 890 return new Uri(pathSegments: segments); |
763 } | 891 } |
764 } | 892 } |
765 | 893 |
766 static _makeWindowsFileUrl(String path) { | 894 static _makeWindowsFileUrl(String path, bool slashTerminated) { |
767 if (path.startsWith("\\\\?\\")) { | 895 if (path.startsWith(r"\\?\")) { |
768 if (path.startsWith("\\\\?\\UNC\\")) { | 896 if (path.startsWith(r"UNC\", 4)) { |
769 path = "\\${path.substring(7)}"; | 897 path = path.replaceRange(0, 7, r'\'); |
770 } else { | 898 } else { |
771 path = path.substring(4); | 899 path = path.substring(4); |
772 if (path.length < 3 || | 900 if (path.length < 3 || |
773 path.codeUnitAt(1) != _COLON || | 901 path.codeUnitAt(1) != _COLON || |
774 path.codeUnitAt(2) != _BACKSLASH) { | 902 path.codeUnitAt(2) != _BACKSLASH) { |
775 throw new ArgumentError( | 903 throw new ArgumentError( |
776 "Windows paths with \\\\?\\ prefix must be absolute"); | 904 r"Windows paths with \\?\ prefix must be absolute"); |
777 } | 905 } |
778 } | 906 } |
779 } else { | 907 } else { |
780 path = path.replaceAll("/", "\\"); | 908 path = path.replaceAll("/", r'\'); |
781 } | 909 } |
782 String sep = "\\"; | 910 const String sep = r'\'; |
783 if (path.length > 1 && path[1] == ":") { | 911 if (path.length > 1 && path.codeUnitAt(1) == _COLON) { |
784 _checkWindowsDriveLetter(path.codeUnitAt(0), true); | 912 _checkWindowsDriveLetter(path.codeUnitAt(0), true); |
785 if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) { | 913 if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) { |
786 throw new ArgumentError( | 914 throw new ArgumentError( |
787 "Windows paths with drive letter must be absolute"); | 915 "Windows paths with drive letter must be absolute"); |
788 } | 916 } |
789 // Absolute file://C:/ URI. | 917 // Absolute file://C:/ URI. |
790 var pathSegments = path.split(sep); | 918 var pathSegments = path.split(sep); |
| 919 if (slashTerminated && |
| 920 pathSegments.last.isNotEmpty) { |
| 921 pathSegments.add(""); // Extra separator at end. |
| 922 } |
791 _checkWindowsPathReservedCharacters(pathSegments, true, 1); | 923 _checkWindowsPathReservedCharacters(pathSegments, true, 1); |
792 return new Uri(scheme: "file", pathSegments: pathSegments); | 924 return new Uri(scheme: "file", pathSegments: pathSegments); |
793 } | 925 } |
794 | 926 |
795 if (path.length > 0 && path[0] == sep) { | 927 if (path.startsWith(sep)) { |
796 if (path.length > 1 && path[1] == sep) { | 928 if (path.startsWith(sep, 1)) { |
797 // Absolute file:// URI with host. | 929 // Absolute file:// URI with host. |
798 int pathStart = path.indexOf("\\", 2); | 930 int pathStart = path.indexOf(r'\', 2); |
799 String hostPart = | 931 String hostPart = |
800 pathStart == -1 ? path.substring(2) : path.substring(2, pathStart); | 932 (pathStart < 0) ? path.substring(2) : path.substring(2, pathStart); |
801 String pathPart = | 933 String pathPart = |
802 pathStart == -1 ? "" : path.substring(pathStart + 1); | 934 (pathStart < 0) ? "" : path.substring(pathStart + 1); |
803 var pathSegments = pathPart.split(sep); | 935 var pathSegments = pathPart.split(sep); |
804 _checkWindowsPathReservedCharacters(pathSegments, true); | 936 _checkWindowsPathReservedCharacters(pathSegments, true); |
| 937 if (slashTerminated && |
| 938 pathSegments.last.isNotEmpty) { |
| 939 pathSegments.add(""); // Extra separator at end. |
| 940 } |
805 return new Uri( | 941 return new Uri( |
806 scheme: "file", host: hostPart, pathSegments: pathSegments); | 942 scheme: "file", host: hostPart, pathSegments: pathSegments); |
807 } else { | 943 } else { |
808 // Absolute file:// URI. | 944 // Absolute file:// URI. |
809 var pathSegments = path.split(sep); | 945 var pathSegments = path.split(sep); |
| 946 if (slashTerminated && |
| 947 pathSegments.last.isNotEmpty) { |
| 948 pathSegments.add(""); // Extra separator at end. |
| 949 } |
810 _checkWindowsPathReservedCharacters(pathSegments, true); | 950 _checkWindowsPathReservedCharacters(pathSegments, true); |
811 return new Uri(scheme: "file", pathSegments: pathSegments); | 951 return new Uri(scheme: "file", pathSegments: pathSegments); |
812 } | 952 } |
813 } else { | 953 } else { |
814 // Relative URI. | 954 // Relative URI. |
815 var pathSegments = path.split(sep); | 955 var pathSegments = path.split(sep); |
816 _checkWindowsPathReservedCharacters(pathSegments, true); | 956 _checkWindowsPathReservedCharacters(pathSegments, true); |
| 957 if (slashTerminated && |
| 958 pathSegments.isNotEmpty && |
| 959 pathSegments.last.isNotEmpty) { |
| 960 pathSegments.add(""); // Extra separator at end. |
| 961 } |
817 return new Uri(pathSegments: pathSegments); | 962 return new Uri(pathSegments: pathSegments); |
818 } | 963 } |
819 } | 964 } |
820 | 965 |
821 /** | 966 /** |
822 * Returns a new `Uri` based on this one, but with some parts replaced. | 967 * Returns a new `Uri` based on this one, but with some parts replaced. |
823 * | 968 * |
824 * This method takes the same parameters as the [new Uri] constructor, | 969 * This method takes the same parameters as the [new Uri] constructor, |
825 * and they have the same meaning. | 970 * and they have the same meaning. |
826 * | 971 * |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
858 * call above, but may also be slightly faster because the parts taken | 1003 * call above, but may also be slightly faster because the parts taken |
859 * from this `Uri` need not be checked for validity again. | 1004 * from this `Uri` need not be checked for validity again. |
860 */ | 1005 */ |
861 Uri replace({String scheme, | 1006 Uri replace({String scheme, |
862 String userInfo, | 1007 String userInfo, |
863 String host, | 1008 String host, |
864 int port, | 1009 int port, |
865 String path, | 1010 String path, |
866 Iterable<String> pathSegments, | 1011 Iterable<String> pathSegments, |
867 String query, | 1012 String query, |
868 Map<String, String> queryParameters, | 1013 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
869 String fragment}) { | 1014 String fragment}) { |
870 // Set to true if the scheme has (potentially) changed. | 1015 // Set to true if the scheme has (potentially) changed. |
871 // In that case, the default port may also have changed and we need | 1016 // In that case, the default port may also have changed and we need |
872 // to check even the existing port. | 1017 // to check even the existing port. |
873 bool schemeChanged = false; | 1018 bool schemeChanged = false; |
874 if (scheme != null) { | 1019 if (scheme != null) { |
875 scheme = _makeScheme(scheme, scheme.length); | 1020 scheme = _makeScheme(scheme, 0, scheme.length); |
876 schemeChanged = true; | 1021 schemeChanged = true; |
877 } else { | 1022 } else { |
878 scheme = this.scheme; | 1023 scheme = this.scheme; |
879 } | 1024 } |
880 bool isFile = (scheme == "file"); | 1025 bool isFile = (scheme == "file"); |
881 if (userInfo != null) { | 1026 if (userInfo != null) { |
882 userInfo = _makeUserInfo(userInfo, 0, userInfo.length); | 1027 userInfo = _makeUserInfo(userInfo, 0, userInfo.length); |
883 } else { | 1028 } else { |
884 userInfo = this.userInfo; | 1029 userInfo = this._userInfo; |
885 } | 1030 } |
886 if (port != null) { | 1031 if (port != null) { |
887 port = _makePort(port, scheme); | 1032 port = _makePort(port, scheme); |
888 } else { | 1033 } else { |
889 port = this._port; | 1034 port = this._port; |
890 if (schemeChanged) { | 1035 if (schemeChanged) { |
891 // The default port might have changed. | 1036 // The default port might have changed. |
892 port = _makePort(port, scheme); | 1037 port = _makePort(port, scheme); |
893 } | 1038 } |
894 } | 1039 } |
895 if (host != null) { | 1040 if (host != null) { |
896 host = _makeHost(host, 0, host.length, false); | 1041 host = _makeHost(host, 0, host.length, false); |
897 } else if (this.hasAuthority) { | 1042 } else if (this.hasAuthority) { |
898 host = this.host; | 1043 host = this._host; |
899 } else if (userInfo.isNotEmpty || port != null || isFile) { | 1044 } else if (userInfo.isNotEmpty || port != null || isFile) { |
900 host = ""; | 1045 host = ""; |
901 } | 1046 } |
902 | 1047 |
903 bool ensureLeadingSlash = (host != null); | 1048 bool hasAuthority = host != null; |
904 if (path != null || pathSegments != null) { | 1049 if (path != null || pathSegments != null) { |
905 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | 1050 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
906 ensureLeadingSlash, isFile); | 1051 scheme, hasAuthority); |
907 } else { | 1052 } else { |
908 path = this.path; | 1053 path = this._path; |
909 if ((isFile || (ensureLeadingSlash && !path.isEmpty)) && | 1054 if ((isFile || (hasAuthority && !path.isEmpty)) && |
910 !path.startsWith('/')) { | 1055 !path.startsWith('/')) { |
911 path = "/$path"; | 1056 path = "/" + path; |
912 } | 1057 } |
913 } | 1058 } |
914 | 1059 |
915 if (query != null || queryParameters != null) { | 1060 if (query != null || queryParameters != null) { |
916 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | 1061 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
917 } else if (this.hasQuery) { | 1062 } else { |
918 query = this.query; | 1063 query = this._query; |
919 } | 1064 } |
920 | 1065 |
921 if (fragment != null) { | 1066 if (fragment != null) { |
922 fragment = _makeFragment(fragment, 0, fragment.length); | 1067 fragment = _makeFragment(fragment, 0, fragment.length); |
923 } else if (this.hasFragment) { | 1068 } else { |
924 fragment = this.fragment; | 1069 fragment = this._fragment; |
925 } | 1070 } |
926 | 1071 |
927 return new Uri._internal( | 1072 return new Uri._internal( |
928 scheme, userInfo, host, port, path, query, fragment); | 1073 scheme, userInfo, host, port, path, query, fragment); |
929 } | 1074 } |
930 | 1075 |
931 /** | 1076 /** |
932 * Returns the URI path split into its segments. Each of the | 1077 * Returns a `Uri` that differs from this only in not having a fragment. |
933 * segments in the returned list have been decoded. If the path is | 1078 * |
934 * empty the empty list will be returned. A leading slash `/` does | 1079 * If this `Uri` does not have a fragment, it is itself returned. |
935 * not affect the segments returned. | 1080 */ |
| 1081 Uri removeFragment() { |
| 1082 if (!this.hasFragment) return this; |
| 1083 return new Uri._internal(scheme, _userInfo, _host, _port, |
| 1084 _path, _query, null); |
| 1085 } |
| 1086 |
| 1087 /** |
| 1088 * Returns the URI path split into its segments. Each of the segments in the |
| 1089 * returned list have been decoded. If the path is empty the empty list will |
| 1090 * be returned. A leading slash `/` does not affect the segments returned. |
936 * | 1091 * |
937 * The returned list is unmodifiable and will throw [UnsupportedError] on any | 1092 * The returned list is unmodifiable and will throw [UnsupportedError] on any |
938 * calls that would mutate it. | 1093 * calls that would mutate it. |
939 */ | 1094 */ |
940 List<String> get pathSegments { | 1095 List<String> get pathSegments { |
941 if (_pathSegments == null) { | 1096 var result = _pathSegments; |
942 var pathToSplit = !path.isEmpty && path.codeUnitAt(0) == _SLASH | 1097 if (result != null) return result; |
943 ? path.substring(1) | 1098 |
944 : path; | 1099 var pathToSplit = path; |
945 _pathSegments = new UnmodifiableListView( | 1100 if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { |
946 pathToSplit == "" ? const<String>[] | 1101 pathToSplit = pathToSplit.substring(1); |
947 : new List<String>.from( | |
948 pathToSplit.split("/") | |
949 .map(Uri.decodeComponent), | |
950 growable: false)); | |
951 } | 1102 } |
952 return _pathSegments; | 1103 result = (pathToSplit == "") |
| 1104 ? const<String>[] |
| 1105 : new List<String>.unmodifiable( |
| 1106 pathToSplit.split("/").map(Uri.decodeComponent)); |
| 1107 _pathSegments = result; |
| 1108 return result; |
953 } | 1109 } |
954 | 1110 |
955 /** | 1111 /** |
956 * Returns the URI query split into a map according to the rules | 1112 * Returns the URI query split into a map according to the rules |
957 * specified for FORM post in the [HTML 4.01 specification section 17.13.4] | 1113 * specified for FORM post in the [HTML 4.01 specification section |
958 * (http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 | 1114 * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTM
L 4.01 section 17.13.4"). |
959 * "HTML 4.01 section 17.13.4"). Each key and value in the returned map | 1115 * Each key and value in the returned map has been decoded. |
960 * has been decoded. If there is no query the empty map is returned. | 1116 * If there is no query the empty map is returned. |
961 * | 1117 * |
962 * Keys in the query string that have no value are mapped to the | 1118 * Keys in the query string that have no value are mapped to the |
963 * empty string. | 1119 * empty string. |
| 1120 * If a key occurs more than once in the query string, it is mapped to |
| 1121 * an arbitrary choice of possible value. |
| 1122 * The [queryParametersAll] getter can provide a map |
| 1123 * that maps keys to all of their values. |
964 * | 1124 * |
965 * The returned map is unmodifiable and will throw [UnsupportedError] on any | 1125 * The returned map is unmodifiable. |
966 * calls that would mutate it. | |
967 */ | 1126 */ |
968 Map<String, String> get queryParameters { | 1127 Map<String, String> get queryParameters { |
969 if (_queryParameters == null) { | 1128 if (_queryParameters == null) { |
970 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); | 1129 _queryParameters = |
| 1130 new UnmodifiableMapView<String, String>(splitQueryString(query)); |
971 } | 1131 } |
972 return _queryParameters; | 1132 return _queryParameters; |
973 } | 1133 } |
974 | 1134 |
| 1135 /** |
| 1136 * Returns the URI query split into a map according to the rules |
| 1137 * specified for FORM post in the [HTML 4.01 specification section |
| 1138 * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTM
L 4.01 section 17.13.4"). |
| 1139 * Each key and value in the returned map has been decoded. If there is no |
| 1140 * query the empty map is returned. |
| 1141 * |
| 1142 * Keys are mapped to lists of their values. If a key occurs only once, |
| 1143 * its value is a singleton list. If a key occurs with no value, the |
| 1144 * empty string is used as the value for that occurrence. |
| 1145 * |
| 1146 * The returned map and the lists it contains are unmodifiable. |
| 1147 */ |
| 1148 Map<String, List<String>> get queryParametersAll { |
| 1149 if (_queryParameterLists == null) { |
| 1150 Map queryParameterLists = _splitQueryStringAll(query); |
| 1151 for (var key in queryParameterLists.keys) { |
| 1152 queryParameterLists[key] = |
| 1153 new List<String>.unmodifiable(queryParameterLists[key]); |
| 1154 } |
| 1155 _queryParameterLists = |
| 1156 new Map<String, List<String>>.unmodifiable(queryParameterLists); |
| 1157 } |
| 1158 return _queryParameterLists; |
| 1159 } |
| 1160 |
| 1161 /** |
| 1162 * Returns a URI where the path has been normalized. |
| 1163 * |
| 1164 * A normalized path does not contain `.` segments or non-leading `..` |
| 1165 * segments. |
| 1166 * Only a relative path with no scheme or authority may contain |
| 1167 * leading `..` segments, |
| 1168 * a path that starts with `/` will also drop any leading `..` segments. |
| 1169 * |
| 1170 * This uses the same normalization strategy as `new Uri().resolve(this)`. |
| 1171 * |
| 1172 * Does not change any part of the URI except the path. |
| 1173 * |
| 1174 * The default implementation of `Uri` always normalizes paths, so calling |
| 1175 * this function has no effect. |
| 1176 */ |
| 1177 Uri normalizePath() { |
| 1178 String path = _normalizePath(_path, scheme, hasAuthority); |
| 1179 if (identical(path, _path)) return this; |
| 1180 return this.replace(path: path); |
| 1181 } |
| 1182 |
975 static int _makePort(int port, String scheme) { | 1183 static int _makePort(int port, String scheme) { |
976 // Perform scheme specific normalization. | 1184 // Perform scheme specific normalization. |
977 if (port != null && port == _defaultPort(scheme)) return null; | 1185 if (port != null && port == _defaultPort(scheme)) return null; |
978 return port; | 1186 return port; |
979 } | 1187 } |
980 | 1188 |
981 /** | 1189 /** |
982 * Check and normalize a most name. | 1190 * Check and normalize a host name. |
983 * | 1191 * |
984 * If the host name starts and ends with '[' and ']', it is considered an | 1192 * If the host name starts and ends with '[' and ']', it is considered an |
985 * IPv6 address. If [strictIPv6] is false, the address is also considered | 1193 * IPv6 address. If [strictIPv6] is false, the address is also considered |
986 * an IPv6 address if it contains any ':' character. | 1194 * an IPv6 address if it contains any ':' character. |
987 * | 1195 * |
988 * If it is not an IPv6 address, it is case- and escape-normalized. | 1196 * If it is not an IPv6 address, it is case- and escape-normalized. |
989 * This escapes all characters not valid in a reg-name, | 1197 * This escapes all characters not valid in a reg-name, |
990 * and converts all non-escape upper-case letters to lower-case. | 1198 * and converts all non-escape upper-case letters to lower-case. |
991 */ | 1199 */ |
992 static String _makeHost(String host, int start, int end, bool strictIPv6) { | 1200 static String _makeHost(String host, int start, int end, bool strictIPv6) { |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1094 buffer.write(slice); | 1302 buffer.write(slice); |
1095 } | 1303 } |
1096 return buffer.toString(); | 1304 return buffer.toString(); |
1097 } | 1305 } |
1098 | 1306 |
1099 /** | 1307 /** |
1100 * Validates scheme characters and does case-normalization. | 1308 * Validates scheme characters and does case-normalization. |
1101 * | 1309 * |
1102 * Schemes are converted to lower case. They cannot contain escapes. | 1310 * Schemes are converted to lower case. They cannot contain escapes. |
1103 */ | 1311 */ |
1104 static String _makeScheme(String scheme, int end) { | 1312 static String _makeScheme(String scheme, int start, int end) { |
1105 if (end == 0) return ""; | 1313 if (start == end) return ""; |
1106 final int firstCodeUnit = scheme.codeUnitAt(0); | 1314 final int firstCodeUnit = scheme.codeUnitAt(start); |
1107 if (!_isAlphabeticCharacter(firstCodeUnit)) { | 1315 if (!_isAlphabeticCharacter(firstCodeUnit)) { |
1108 _fail(scheme, 0, "Scheme not starting with alphabetic character"); | 1316 _fail(scheme, start, "Scheme not starting with alphabetic character"); |
1109 } | 1317 } |
1110 bool allLowercase = firstCodeUnit >= _LOWER_CASE_A; | 1318 bool containsUpperCase = false; |
1111 for (int i = 0; i < end; i++) { | 1319 for (int i = start; i < end; i++) { |
1112 final int codeUnit = scheme.codeUnitAt(i); | 1320 final int codeUnit = scheme.codeUnitAt(i); |
1113 if (!_isSchemeCharacter(codeUnit)) { | 1321 if (!_isSchemeCharacter(codeUnit)) { |
1114 _fail(scheme, i, "Illegal scheme character"); | 1322 _fail(scheme, i, "Illegal scheme character"); |
1115 } | 1323 } |
1116 if (codeUnit < _LOWER_CASE_A || codeUnit > _LOWER_CASE_Z) { | 1324 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { |
1117 allLowercase = false; | 1325 containsUpperCase = true; |
1118 } | 1326 } |
1119 } | 1327 } |
1120 scheme = scheme.substring(0, end); | 1328 scheme = scheme.substring(start, end); |
1121 if (!allLowercase) scheme = scheme.toLowerCase(); | 1329 if (containsUpperCase) scheme = scheme.toLowerCase(); |
1122 return scheme; | 1330 return scheme; |
1123 } | 1331 } |
1124 | 1332 |
1125 static String _makeUserInfo(String userInfo, int start, int end) { | 1333 static String _makeUserInfo(String userInfo, int start, int end) { |
1126 if (userInfo == null) return ""; | 1334 if (userInfo == null) return ""; |
1127 return _normalize(userInfo, start, end, _userinfoTable); | 1335 return _normalize(userInfo, start, end, _userinfoTable); |
1128 } | 1336 } |
1129 | 1337 |
1130 static String _makePath(String path, int start, int end, | 1338 static String _makePath(String path, int start, int end, |
1131 Iterable<String> pathSegments, | 1339 Iterable<String> pathSegments, |
1132 bool ensureLeadingSlash, | 1340 String scheme, |
1133 bool isFile) { | 1341 bool hasAuthority) { |
| 1342 bool isFile = (scheme == "file"); |
| 1343 bool ensureLeadingSlash = isFile || hasAuthority; |
1134 if (path == null && pathSegments == null) return isFile ? "/" : ""; | 1344 if (path == null && pathSegments == null) return isFile ? "/" : ""; |
1135 if (path != null && pathSegments != null) { | 1345 if (path != null && pathSegments != null) { |
1136 throw new ArgumentError('Both path and pathSegments specified'); | 1346 throw new ArgumentError('Both path and pathSegments specified'); |
1137 } | 1347 } |
1138 var result; | 1348 var result; |
1139 if (path != null) { | 1349 if (path != null) { |
1140 result = _normalize(path, start, end, _pathCharOrSlashTable); | 1350 result = _normalize(path, start, end, _pathCharOrSlashTable); |
1141 } else { | 1351 } else { |
1142 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); | 1352 result = pathSegments.map((s) => |
| 1353 _uriEncode(_pathCharTable, s, UTF8, false)).join("/"); |
1143 } | 1354 } |
1144 if (result.isEmpty) { | 1355 if (result.isEmpty) { |
1145 if (isFile) return "/"; | 1356 if (isFile) return "/"; |
1146 } else if ((isFile || ensureLeadingSlash) && | 1357 } else if (ensureLeadingSlash && !result.startsWith('/')) { |
1147 result.codeUnitAt(0) != _SLASH) { | 1358 result = "/" + result; |
1148 return "/$result"; | |
1149 } | 1359 } |
| 1360 result = _normalizePath(result, scheme, hasAuthority); |
1150 return result; | 1361 return result; |
1151 } | 1362 } |
1152 | 1363 |
1153 static String _makeQuery(String query, int start, int end, | 1364 /// Performs path normalization (remove dot segments) on a path. |
1154 Map<String, String> queryParameters) { | 1365 /// |
| 1366 /// If the URI has neither scheme nor authority, it's considered a |
| 1367 /// "pure path" and normalization won't remove leading ".." segments. |
| 1368 /// Otherwise it follows the RFC 3986 "remove dot segments" algorithm. |
| 1369 static String _normalizePath(String path, String scheme, bool hasAuthority) { |
| 1370 if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) { |
| 1371 return _normalizeRelativePath(path); |
| 1372 } |
| 1373 return _removeDotSegments(path); |
| 1374 } |
| 1375 |
| 1376 static String _makeQuery( |
| 1377 String query, int start, int end, |
| 1378 Map<String, dynamic/*String|Iterable<String>*/> queryParameters) { |
1155 if (query == null && queryParameters == null) return null; | 1379 if (query == null && queryParameters == null) return null; |
1156 if (query != null && queryParameters != null) { | 1380 if (query != null && queryParameters != null) { |
1157 throw new ArgumentError('Both query and queryParameters specified'); | 1381 throw new ArgumentError('Both query and queryParameters specified'); |
1158 } | 1382 } |
1159 if (query != null) return _normalize(query, start, end, _queryCharTable); | 1383 if (query != null) return _normalize(query, start, end, _queryCharTable); |
1160 | 1384 |
1161 var result = new StringBuffer(); | 1385 var result = new StringBuffer(); |
1162 var first = true; | 1386 var separator = ""; |
1163 queryParameters.forEach((key, value) { | 1387 |
1164 if (!first) { | 1388 void writeParameter(String key, String value) { |
1165 result.write("&"); | 1389 result.write(separator); |
1166 } | 1390 separator = "&"; |
1167 first = false; | |
1168 result.write(Uri.encodeQueryComponent(key)); | 1391 result.write(Uri.encodeQueryComponent(key)); |
1169 if (value != null && !value.isEmpty) { | 1392 if (value != null && value.isNotEmpty) { |
1170 result.write("="); | 1393 result.write("="); |
1171 result.write(Uri.encodeQueryComponent(value)); | 1394 result.write(Uri.encodeQueryComponent(value)); |
1172 } | 1395 } |
| 1396 } |
| 1397 |
| 1398 queryParameters.forEach((key, value) { |
| 1399 if (value == null || value is String) { |
| 1400 writeParameter(key, value); |
| 1401 } else { |
| 1402 Iterable values = value; |
| 1403 for (String value in values) { |
| 1404 writeParameter(key, value); |
| 1405 } |
| 1406 } |
1173 }); | 1407 }); |
1174 return result.toString(); | 1408 return result.toString(); |
1175 } | 1409 } |
1176 | 1410 |
1177 static String _makeFragment(String fragment, int start, int end) { | 1411 static String _makeFragment(String fragment, int start, int end) { |
1178 if (fragment == null) return null; | 1412 if (fragment == null) return null; |
1179 return _normalize(fragment, start, end, _queryCharTable); | 1413 return _normalize(fragment, start, end, _queryCharTable); |
1180 } | 1414 } |
1181 | 1415 |
1182 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | 1416 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
1183 | 1417 |
1184 static bool _isHexDigit(int char) { | |
1185 if (_NINE >= char) return _ZERO <= char; | |
1186 char |= 0x20; | |
1187 return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char; | |
1188 } | |
1189 | |
1190 static int _hexValue(int char) { | |
1191 assert(_isHexDigit(char)); | |
1192 if (_NINE >= char) return char - _ZERO; | |
1193 char |= 0x20; | |
1194 return char - (_LOWER_CASE_A - 10); | |
1195 } | |
1196 | |
1197 /** | 1418 /** |
1198 * Performs RFC 3986 Percent-Encoding Normalization. | 1419 * Performs RFC 3986 Percent-Encoding Normalization. |
1199 * | 1420 * |
1200 * Returns a replacement string that should be replace the original escape. | 1421 * Returns a replacement string that should be replace the original escape. |
1201 * Returns null if no replacement is necessary because the escape is | 1422 * Returns null if no replacement is necessary because the escape is |
1202 * not for an unreserved character and is already non-lower-case. | 1423 * not for an unreserved character and is already non-lower-case. |
1203 * | 1424 * |
1204 * Returns "%" if the escape is invalid (not two valid hex digits following | 1425 * Returns "%" if the escape is invalid (not two valid hex digits following |
1205 * the percent sign). The calling code should replace the percent | 1426 * the percent sign). The calling code should replace the percent |
1206 * sign with "%25", but leave the following two characters unmodified. | 1427 * sign with "%25", but leave the following two characters unmodified. |
1207 * | 1428 * |
1208 * If [lowerCase] is true, a single character returned is always lower case, | 1429 * If [lowerCase] is true, a single character returned is always lower case, |
1209 */ | 1430 */ |
1210 static String _normalizeEscape(String source, int index, bool lowerCase) { | 1431 static String _normalizeEscape(String source, int index, bool lowerCase) { |
1211 assert(source.codeUnitAt(index) == _PERCENT); | 1432 assert(source.codeUnitAt(index) == _PERCENT); |
1212 if (index + 2 >= source.length) { | 1433 if (index + 2 >= source.length) { |
1213 return "%"; // Marks the escape as invalid. | 1434 return "%"; // Marks the escape as invalid. |
1214 } | 1435 } |
1215 int firstDigit = source.codeUnitAt(index + 1); | 1436 int firstDigit = source.codeUnitAt(index + 1); |
1216 int secondDigit = source.codeUnitAt(index + 2); | 1437 int secondDigit = source.codeUnitAt(index + 2); |
1217 if (!_isHexDigit(firstDigit) || !_isHexDigit(secondDigit)) { | 1438 int firstDigitValue = _parseHexDigit(firstDigit); |
| 1439 int secondDigitValue = _parseHexDigit(secondDigit); |
| 1440 if (firstDigitValue < 0 || secondDigitValue < 0) { |
1218 return "%"; // Marks the escape as invalid. | 1441 return "%"; // Marks the escape as invalid. |
1219 } | 1442 } |
1220 int value = _hexValue(firstDigit) * 16 + _hexValue(secondDigit); | 1443 int value = firstDigitValue * 16 + secondDigitValue; |
1221 if (_isUnreservedChar(value)) { | 1444 if (_isUnreservedChar(value)) { |
1222 if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { | 1445 if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { |
1223 value |= 0x20; | 1446 value |= 0x20; |
1224 } | 1447 } |
1225 return new String.fromCharCode(value); | 1448 return new String.fromCharCode(value); |
1226 } | 1449 } |
1227 if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { | 1450 if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { |
1228 // Either digit is lower case. | 1451 // Either digit is lower case. |
1229 return source.substring(index, index + 3).toUpperCase(); | 1452 return source.substring(index, index + 3).toUpperCase(); |
1230 } | 1453 } |
1231 // Escape is retained, and is already non-lower case, so return null to | 1454 // Escape is retained, and is already non-lower case, so return null to |
1232 // represent "no replacement necessary". | 1455 // represent "no replacement necessary". |
1233 return null; | 1456 return null; |
1234 } | 1457 } |
1235 | 1458 |
1236 static bool _isUnreservedChar(int ch) { | 1459 // Converts a UTF-16 code-unit to its value as a hex digit. |
1237 return ch < 127 && | 1460 // Returns -1 for non-hex digits. |
1238 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 1461 static int _parseHexDigit(int char) { |
| 1462 int digit = char ^ Uri._ZERO; |
| 1463 if (digit <= 9) return digit; |
| 1464 int lowerCase = char | 0x20; |
| 1465 if (Uri._LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
| 1466 return lowerCase - (_LOWER_CASE_A - 10); |
| 1467 } |
| 1468 return -1; |
1239 } | 1469 } |
1240 | 1470 |
1241 static String _escapeChar(char) { | 1471 static String _escapeChar(int char) { |
1242 assert(char <= 0x10ffff); // It's a valid unicode code point. | 1472 assert(char <= 0x10ffff); // It's a valid unicode code point. |
1243 const hexDigits = "0123456789ABCDEF"; | 1473 List<int> codeUnits; |
1244 List codeUnits; | |
1245 if (char < 0x80) { | 1474 if (char < 0x80) { |
1246 // ASCII, a single percent encoded sequence. | 1475 // ASCII, a single percent encoded sequence. |
1247 codeUnits = new List(3); | 1476 codeUnits = new List(3); |
1248 codeUnits[0] = _PERCENT; | 1477 codeUnits[0] = _PERCENT; |
1249 codeUnits[1] = hexDigits.codeUnitAt(char >> 4); | 1478 codeUnits[1] = _hexDigits.codeUnitAt(char >> 4); |
1250 codeUnits[2] = hexDigits.codeUnitAt(char & 0xf); | 1479 codeUnits[2] = _hexDigits.codeUnitAt(char & 0xf); |
1251 } else { | 1480 } else { |
1252 // Do UTF-8 encoding of character, then percent encode bytes. | 1481 // Do UTF-8 encoding of character, then percent encode bytes. |
1253 int flag = 0xc0; // The high-bit markers on the first byte of UTF-8. | 1482 int flag = 0xc0; // The high-bit markers on the first byte of UTF-8. |
1254 int encodedBytes = 2; | 1483 int encodedBytes = 2; |
1255 if (char > 0x7ff) { | 1484 if (char > 0x7ff) { |
1256 flag = 0xe0; | 1485 flag = 0xe0; |
1257 encodedBytes = 3; | 1486 encodedBytes = 3; |
1258 if (char > 0xffff) { | 1487 if (char > 0xffff) { |
1259 encodedBytes = 4; | 1488 encodedBytes = 4; |
1260 flag = 0xf0; | 1489 flag = 0xf0; |
1261 } | 1490 } |
1262 } | 1491 } |
1263 codeUnits = new List(3 * encodedBytes); | 1492 codeUnits = new List(3 * encodedBytes); |
1264 int index = 0; | 1493 int index = 0; |
1265 while (--encodedBytes >= 0) { | 1494 while (--encodedBytes >= 0) { |
1266 int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; | 1495 int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; |
1267 codeUnits[index] = _PERCENT; | 1496 codeUnits[index] = _PERCENT; |
1268 codeUnits[index + 1] = hexDigits.codeUnitAt(byte >> 4); | 1497 codeUnits[index + 1] = _hexDigits.codeUnitAt(byte >> 4); |
1269 codeUnits[index + 2] = hexDigits.codeUnitAt(byte & 0xf); | 1498 codeUnits[index + 2] = _hexDigits.codeUnitAt(byte & 0xf); |
1270 index += 3; | 1499 index += 3; |
1271 flag = 0x80; // Following bytes have only high bit set. | 1500 flag = 0x80; // Following bytes have only high bit set. |
1272 } | 1501 } |
1273 } | 1502 } |
1274 return new String.fromCharCodes(codeUnits); | 1503 return new String.fromCharCodes(codeUnits); |
1275 } | 1504 } |
1276 | 1505 |
1277 /** | 1506 /** |
1278 * Runs through component checking that each character is valid and | 1507 * Runs through component checking that each character is valid and |
1279 * normalize percent escapes. | 1508 * normalize percent escapes. |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1351 static bool _isGeneralDelimiter(int ch) { | 1580 static bool _isGeneralDelimiter(int ch) { |
1352 return ch <= _RIGHT_BRACKET && | 1581 return ch <= _RIGHT_BRACKET && |
1353 ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 1582 ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
1354 } | 1583 } |
1355 | 1584 |
1356 /** | 1585 /** |
1357 * Returns whether the URI is absolute. | 1586 * Returns whether the URI is absolute. |
1358 */ | 1587 */ |
1359 bool get isAbsolute => scheme != "" && fragment == ""; | 1588 bool get isAbsolute => scheme != "" && fragment == ""; |
1360 | 1589 |
1361 String _merge(String base, String reference) { | 1590 String _mergePaths(String base, String reference) { |
1362 if (base.isEmpty) return "/$reference"; | |
1363 // Optimize for the case: absolute base, reference beginning with "../". | 1591 // Optimize for the case: absolute base, reference beginning with "../". |
1364 int backCount = 0; | 1592 int backCount = 0; |
1365 int refStart = 0; | 1593 int refStart = 0; |
1366 // Count number of "../" at beginning of reference. | 1594 // Count number of "../" at beginning of reference. |
1367 while (reference.startsWith("../", refStart)) { | 1595 while (reference.startsWith("../", refStart)) { |
1368 refStart += 3; | 1596 refStart += 3; |
1369 backCount++; | 1597 backCount++; |
1370 } | 1598 } |
1371 | 1599 |
1372 // Drop last segment - everything after last '/' of base. | 1600 // Drop last segment - everything after last '/' of base. |
1373 int baseEnd = base.lastIndexOf('/'); | 1601 int baseEnd = base.lastIndexOf('/'); |
1374 // Drop extra segments for each leading "../" of reference. | 1602 // Drop extra segments for each leading "../" of reference. |
1375 while (baseEnd > 0 && backCount > 0) { | 1603 while (baseEnd > 0 && backCount > 0) { |
1376 int newEnd = base.lastIndexOf('/', baseEnd - 1); | 1604 int newEnd = base.lastIndexOf('/', baseEnd - 1); |
1377 if (newEnd < 0) { | 1605 if (newEnd < 0) { |
1378 break; | 1606 break; |
1379 } | 1607 } |
1380 int delta = baseEnd - newEnd; | 1608 int delta = baseEnd - newEnd; |
1381 // If we see a "." or ".." segment in base, stop here and let | 1609 // If we see a "." or ".." segment in base, stop here and let |
1382 // _removeDotSegments handle it. | 1610 // _removeDotSegments handle it. |
1383 if ((delta == 2 || delta == 3) && | 1611 if ((delta == 2 || delta == 3) && |
1384 base.codeUnitAt(newEnd + 1) == _DOT && | 1612 base.codeUnitAt(newEnd + 1) == _DOT && |
1385 (delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) { | 1613 (delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) { |
1386 break; | 1614 break; |
1387 } | 1615 } |
1388 baseEnd = newEnd; | 1616 baseEnd = newEnd; |
1389 backCount--; | 1617 backCount--; |
1390 } | 1618 } |
1391 return base.substring(0, baseEnd + 1) + | 1619 return base.replaceRange(baseEnd + 1, null, |
1392 reference.substring(refStart - 3 * backCount); | 1620 reference.substring(refStart - 3 * backCount)); |
1393 } | 1621 } |
1394 | 1622 |
1395 bool _hasDotSegments(String path) { | 1623 /// Make a guess at whether a path contains a `..` or `.` segment. |
1396 if (path.length > 0 && path.codeUnitAt(0) == _DOT) return true; | 1624 /// |
| 1625 /// This is a primitive test that can cause false positives. |
| 1626 /// It's only used to avoid a more expensive operation in the case where |
| 1627 /// it's not necessary. |
| 1628 static bool _mayContainDotSegments(String path) { |
| 1629 if (path.startsWith('.')) return true; |
1397 int index = path.indexOf("/."); | 1630 int index = path.indexOf("/."); |
1398 return index != -1; | 1631 return index != -1; |
1399 } | 1632 } |
1400 | 1633 |
1401 String _removeDotSegments(String path) { | 1634 /// Removes '.' and '..' segments from a path. |
1402 if (!_hasDotSegments(path)) return path; | 1635 /// |
| 1636 /// Follows the RFC 2986 "remove dot segments" algorithm. |
| 1637 /// This algorithm is only used on paths of URIs with a scheme, |
| 1638 /// and it treats the path as if it is absolute (leading '..' are removed). |
| 1639 static String _removeDotSegments(String path) { |
| 1640 if (!_mayContainDotSegments(path)) return path; |
| 1641 assert(path.isNotEmpty); // An empty path would not have dot segments. |
1403 List<String> output = []; | 1642 List<String> output = []; |
1404 bool appendSlash = false; | 1643 bool appendSlash = false; |
1405 for (String segment in path.split("/")) { | 1644 for (String segment in path.split("/")) { |
1406 appendSlash = false; | 1645 appendSlash = false; |
1407 if (segment == "..") { | 1646 if (segment == "..") { |
1408 if (!output.isEmpty && | 1647 if (output.isNotEmpty) { |
1409 ((output.length != 1) || (output[0] != ""))) output.removeLast(); | 1648 output.removeLast(); |
| 1649 if (output.isEmpty) { |
| 1650 output.add(""); |
| 1651 } |
| 1652 } |
1410 appendSlash = true; | 1653 appendSlash = true; |
1411 } else if ("." == segment) { | 1654 } else if ("." == segment) { |
1412 appendSlash = true; | 1655 appendSlash = true; |
1413 } else { | 1656 } else { |
1414 output.add(segment); | 1657 output.add(segment); |
1415 } | 1658 } |
1416 } | 1659 } |
1417 if (appendSlash) output.add(""); | 1660 if (appendSlash) output.add(""); |
1418 return output.join("/"); | 1661 return output.join("/"); |
1419 } | 1662 } |
1420 | 1663 |
| 1664 /// Removes all `.` segments and any non-leading `..` segments. |
| 1665 /// |
| 1666 /// Removing the ".." from a "bar/foo/.." sequence results in "bar/" |
| 1667 /// (trailing "/"). If the entire path is removed (because it contains as |
| 1668 /// many ".." segments as real segments), the result is "./". |
| 1669 /// This is different from an empty string, which represents "no path", |
| 1670 /// when you resolve it against a base URI with a path with a non-empty |
| 1671 /// final segment. |
| 1672 static String _normalizeRelativePath(String path) { |
| 1673 assert(!path.startsWith('/')); // Only get called for relative paths. |
| 1674 if (!_mayContainDotSegments(path)) return path; |
| 1675 assert(path.isNotEmpty); // An empty path would not have dot segments. |
| 1676 List<String> output = []; |
| 1677 bool appendSlash = false; |
| 1678 for (String segment in path.split("/")) { |
| 1679 appendSlash = false; |
| 1680 if (".." == segment) { |
| 1681 if (!output.isEmpty && output.last != "..") { |
| 1682 output.removeLast(); |
| 1683 appendSlash = true; |
| 1684 } else { |
| 1685 output.add(".."); |
| 1686 } |
| 1687 } else if ("." == segment) { |
| 1688 appendSlash = true; |
| 1689 } else { |
| 1690 output.add(segment); |
| 1691 } |
| 1692 } |
| 1693 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { |
| 1694 return "./"; |
| 1695 } |
| 1696 if (appendSlash || output.last == '..') output.add(""); |
| 1697 return output.join("/"); |
| 1698 } |
| 1699 |
1421 /** | 1700 /** |
1422 * Resolve [reference] as an URI relative to `this`. | 1701 * Resolve [reference] as an URI relative to `this`. |
1423 * | 1702 * |
1424 * First turn [reference] into a URI using [Uri.parse]. Then resolve the | 1703 * First turn [reference] into a URI using [Uri.parse]. Then resolve the |
1425 * resulting URI relative to `this`. | 1704 * resulting URI relative to `this`. |
1426 * | 1705 * |
1427 * Returns the resolved URI. | 1706 * Returns the resolved URI. |
1428 * | 1707 * |
1429 * See [resolveUri] for details. | 1708 * See [resolveUri] for details. |
1430 */ | 1709 */ |
1431 Uri resolve(String reference) { | 1710 Uri resolve(String reference) { |
1432 return resolveUri(Uri.parse(reference)); | 1711 return resolveUri(Uri.parse(reference)); |
1433 } | 1712 } |
1434 | 1713 |
1435 /** | 1714 /** |
1436 * Resolve [reference] as an URI relative to `this`. | 1715 * Resolve [reference] as an URI relative to `this`. |
1437 * | 1716 * |
1438 * Returns the resolved URI. | 1717 * Returns the resolved URI. |
1439 * | 1718 * |
1440 * The algorithm for resolving a reference is described in | 1719 * The algorithm "Transform Reference" for resolving a reference is described |
1441 * [RFC-3986 Section 5] | 1720 * in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1
123"). |
1442 * (http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). | 1721 * |
| 1722 * Updated to handle the case where the base URI is just a relative path - |
| 1723 * that is: when it has no scheme or authority and the path does not start |
| 1724 * with a slash. |
| 1725 * In that case, the paths are combined without removing leading "..", and |
| 1726 * an empty path is not converted to "/". |
1443 */ | 1727 */ |
1444 Uri resolveUri(Uri reference) { | 1728 Uri resolveUri(Uri reference) { |
1445 // From RFC 3986. | 1729 // From RFC 3986. |
1446 String targetScheme; | 1730 String targetScheme; |
1447 String targetUserInfo = ""; | 1731 String targetUserInfo = ""; |
1448 String targetHost; | 1732 String targetHost; |
1449 int targetPort; | 1733 int targetPort; |
1450 String targetPath; | 1734 String targetPath; |
1451 String targetQuery; | 1735 String targetQuery; |
1452 if (reference.scheme.isNotEmpty) { | 1736 if (reference.scheme.isNotEmpty) { |
(...skipping 10 matching lines...) Expand all Loading... |
1463 } else { | 1747 } else { |
1464 targetScheme = this.scheme; | 1748 targetScheme = this.scheme; |
1465 if (reference.hasAuthority) { | 1749 if (reference.hasAuthority) { |
1466 targetUserInfo = reference.userInfo; | 1750 targetUserInfo = reference.userInfo; |
1467 targetHost = reference.host; | 1751 targetHost = reference.host; |
1468 targetPort = _makePort(reference.hasPort ? reference.port : null, | 1752 targetPort = _makePort(reference.hasPort ? reference.port : null, |
1469 targetScheme); | 1753 targetScheme); |
1470 targetPath = _removeDotSegments(reference.path); | 1754 targetPath = _removeDotSegments(reference.path); |
1471 if (reference.hasQuery) targetQuery = reference.query; | 1755 if (reference.hasQuery) targetQuery = reference.query; |
1472 } else { | 1756 } else { |
| 1757 targetUserInfo = this._userInfo; |
| 1758 targetHost = this._host; |
| 1759 targetPort = this._port; |
1473 if (reference.path == "") { | 1760 if (reference.path == "") { |
1474 targetPath = this._path; | 1761 targetPath = this._path; |
1475 if (reference.hasQuery) { | 1762 if (reference.hasQuery) { |
1476 targetQuery = reference.query; | 1763 targetQuery = reference.query; |
1477 } else { | 1764 } else { |
1478 targetQuery = this._query; | 1765 targetQuery = this._query; |
1479 } | 1766 } |
1480 } else { | 1767 } else { |
1481 if (reference.path.startsWith("/")) { | 1768 if (reference.hasAbsolutePath) { |
1482 targetPath = _removeDotSegments(reference.path); | 1769 targetPath = _removeDotSegments(reference.path); |
1483 } else { | 1770 } else { |
1484 targetPath = _removeDotSegments(_merge(this._path, reference.path)); | 1771 // This is the RFC 3986 behavior for merging. |
| 1772 if (this.hasEmptyPath) { |
| 1773 if (!this.hasScheme && !this.hasAuthority) { |
| 1774 // Keep the path relative if no scheme or authority. |
| 1775 targetPath = reference.path; |
| 1776 } else { |
| 1777 // Add path normalization on top of RFC algorithm. |
| 1778 targetPath = _removeDotSegments("/" + reference.path); |
| 1779 } |
| 1780 } else { |
| 1781 var mergedPath = _mergePaths(this._path, reference.path); |
| 1782 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { |
| 1783 targetPath = _removeDotSegments(mergedPath); |
| 1784 } else { |
| 1785 // Non-RFC 3986 beavior. If both base and reference are relative |
| 1786 // path, allow the merged path to start with "..". |
| 1787 // The RFC only specifies the case where the base has a scheme. |
| 1788 targetPath = _normalizeRelativePath(mergedPath); |
| 1789 } |
| 1790 } |
1485 } | 1791 } |
1486 if (reference.hasQuery) targetQuery = reference.query; | 1792 if (reference.hasQuery) targetQuery = reference.query; |
1487 } | 1793 } |
1488 targetUserInfo = this._userInfo; | |
1489 targetHost = this._host; | |
1490 targetPort = this._port; | |
1491 } | 1794 } |
1492 } | 1795 } |
1493 String fragment = reference.hasFragment ? reference.fragment : null; | 1796 String fragment = reference.hasFragment ? reference.fragment : null; |
1494 return new Uri._internal(targetScheme, | 1797 return new Uri._internal(targetScheme, |
1495 targetUserInfo, | 1798 targetUserInfo, |
1496 targetHost, | 1799 targetHost, |
1497 targetPort, | 1800 targetPort, |
1498 targetPath, | 1801 targetPath, |
1499 targetQuery, | 1802 targetQuery, |
1500 fragment); | 1803 fragment); |
1501 } | 1804 } |
1502 | 1805 |
1503 /** | 1806 /** |
| 1807 * Returns whether the URI has a [scheme] component. |
| 1808 */ |
| 1809 bool get hasScheme => scheme.isNotEmpty; |
| 1810 |
| 1811 /** |
1504 * Returns whether the URI has an [authority] component. | 1812 * Returns whether the URI has an [authority] component. |
1505 */ | 1813 */ |
1506 bool get hasAuthority => _host != null; | 1814 bool get hasAuthority => _host != null; |
1507 | 1815 |
1508 /** | 1816 /** |
1509 * Returns whether the URI has an explicit port. | 1817 * Returns whether the URI has an explicit port. |
1510 * | 1818 * |
1511 * If the port number is the default port number | 1819 * If the port number is the default port number |
1512 * (zero for unrecognized schemes, with http (80) and https (443) being | 1820 * (zero for unrecognized schemes, with http (80) and https (443) being |
1513 * recognized), | 1821 * recognized), |
1514 * then the port is made implicit and omitted from the URI. | 1822 * then the port is made implicit and omitted from the URI. |
1515 */ | 1823 */ |
1516 bool get hasPort => _port != null; | 1824 bool get hasPort => _port != null; |
1517 | 1825 |
1518 /** | 1826 /** |
1519 * Returns whether the URI has a query part. | 1827 * Returns whether the URI has a query part. |
1520 */ | 1828 */ |
1521 bool get hasQuery => _query != null; | 1829 bool get hasQuery => _query != null; |
1522 | 1830 |
1523 /** | 1831 /** |
1524 * Returns whether the URI has a fragment part. | 1832 * Returns whether the URI has a fragment part. |
1525 */ | 1833 */ |
1526 bool get hasFragment => _fragment != null; | 1834 bool get hasFragment => _fragment != null; |
1527 | 1835 |
1528 /** | 1836 /** |
| 1837 * Returns whether the URI has an empty path. |
| 1838 */ |
| 1839 bool get hasEmptyPath => _path.isEmpty; |
| 1840 |
| 1841 /** |
| 1842 * Returns whether the URI has an absolute path (starting with '/'). |
| 1843 */ |
| 1844 bool get hasAbsolutePath => _path.startsWith('/'); |
| 1845 |
| 1846 /** |
1529 * Returns the origin of the URI in the form scheme://host:port for the | 1847 * Returns the origin of the URI in the form scheme://host:port for the |
1530 * schemes http and https. | 1848 * schemes http and https. |
1531 * | 1849 * |
1532 * It is an error if the scheme is not "http" or "https". | 1850 * It is an error if the scheme is not "http" or "https". |
1533 * | 1851 * |
1534 * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | 1852 * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin |
1535 */ | 1853 */ |
1536 String get origin { | 1854 String get origin { |
1537 if (scheme == "" || _host == null || _host == "") { | 1855 if (scheme == "" || _host == null || _host == "") { |
1538 throw new StateError("Cannot use origin without a scheme: $this"); | 1856 throw new StateError("Cannot use origin without a scheme: $this"); |
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1672 ss.write(_userInfo); | 1990 ss.write(_userInfo); |
1673 ss.write("@"); | 1991 ss.write("@"); |
1674 } | 1992 } |
1675 if (_host != null) ss.write(_host); | 1993 if (_host != null) ss.write(_host); |
1676 if (_port != null) { | 1994 if (_port != null) { |
1677 ss.write(":"); | 1995 ss.write(":"); |
1678 ss.write(_port); | 1996 ss.write(_port); |
1679 } | 1997 } |
1680 } | 1998 } |
1681 | 1999 |
| 2000 /** |
| 2001 * Access the structure of a `data:` URI. |
| 2002 * |
| 2003 * Returns a [UriData] object for `data:` URIs and `null` for all other |
| 2004 * URIs. |
| 2005 * The [UriData] object can be used to access the media type and data |
| 2006 * of a `data:` URI. |
| 2007 */ |
| 2008 UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; |
| 2009 |
1682 String toString() { | 2010 String toString() { |
1683 StringBuffer sb = new StringBuffer(); | 2011 StringBuffer sb = new StringBuffer(); |
1684 _addIfNonEmpty(sb, scheme, scheme, ':'); | 2012 _addIfNonEmpty(sb, scheme, scheme, ':'); |
1685 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | 2013 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { |
1686 // File URIS always have the authority, even if it is empty. | 2014 // File URIS always have the authority, even if it is empty. |
1687 // The empty URI means "localhost". | 2015 // The empty URI means "localhost". |
1688 sb.write("//"); | 2016 sb.write("//"); |
1689 _writeAuthority(sb); | 2017 _writeAuthority(sb); |
1690 } | 2018 } |
1691 sb.write(path); | 2019 sb.write(path); |
1692 if (_query != null) { sb..write("?")..write(_query); } | 2020 if (_query != null) { sb..write("?")..write(_query); } |
1693 if (_fragment != null) { sb..write("#")..write(_fragment); } | 2021 if (_fragment != null) { sb..write("#")..write(_fragment); } |
1694 return sb.toString(); | 2022 return sb.toString(); |
1695 } | 2023 } |
1696 | 2024 |
1697 bool operator==(other) { | 2025 bool operator==(other) { |
1698 if (other is! Uri) return false; | 2026 if (other is! Uri) return false; |
1699 Uri uri = other; | 2027 Uri uri = other; |
1700 return scheme == uri.scheme && | 2028 return scheme == uri.scheme && |
1701 hasAuthority == uri.hasAuthority && | 2029 hasAuthority == uri.hasAuthority && |
1702 userInfo == uri.userInfo && | 2030 userInfo == uri.userInfo && |
1703 host == uri.host && | 2031 host == uri.host && |
1704 port == uri.port && | 2032 port == uri.port && |
1705 path == uri.path && | 2033 path == uri.path && |
1706 hasQuery == uri.hasQuery && | 2034 hasQuery == uri.hasQuery && |
1707 query == uri.query && | 2035 query == uri.query && |
1708 hasFragment == uri.hasFragment && | 2036 hasFragment == uri.hasFragment && |
1709 fragment == uri.fragment; | 2037 fragment == uri.fragment; |
1710 } | 2038 } |
1711 | 2039 |
1712 int get hashCode { | 2040 int get hashCode { |
1713 int combine(part, current) { | 2041 int combine(part, current) { |
1714 // The sum is truncated to 30 bits to make sure it fits into a Smi. | 2042 // The sum is truncated to 30 bits to make sure it fits into a Smi. |
1715 return (current * 31 + part.hashCode) & 0x3FFFFFFF; | 2043 return (current * 31 + part.hashCode) & 0x3FFFFFFF; |
1716 } | 2044 } |
1717 return combine(scheme, combine(userInfo, combine(host, combine(port, | 2045 return combine(scheme, combine(userInfo, combine(host, combine(port, |
1718 combine(path, combine(query, combine(fragment, 1))))))); | 2046 combine(path, combine(query, combine(fragment, 1))))))); |
1719 } | 2047 } |
(...skipping 20 matching lines...) Expand all Loading... |
1740 * string. | 2068 * string. |
1741 * | 2069 * |
1742 * For encoding the query part consider using | 2070 * For encoding the query part consider using |
1743 * [encodeQueryComponent]. | 2071 * [encodeQueryComponent]. |
1744 * | 2072 * |
1745 * To avoid the need for explicitly encoding use the [pathSegments] | 2073 * To avoid the need for explicitly encoding use the [pathSegments] |
1746 * and [queryParameters] optional named arguments when constructing | 2074 * and [queryParameters] optional named arguments when constructing |
1747 * a [Uri]. | 2075 * a [Uri]. |
1748 */ | 2076 */ |
1749 static String encodeComponent(String component) { | 2077 static String encodeComponent(String component) { |
1750 return _uriEncode(_unreserved2396Table, component); | 2078 return _uriEncode(_unreserved2396Table, component, UTF8, false); |
1751 } | 2079 } |
1752 | 2080 |
1753 /** | 2081 /** |
1754 * Encode the string [component] according to the HTML 4.01 rules | 2082 * Encode the string [component] according to the HTML 4.01 rules |
1755 * for encoding the posting of a HTML form as a query string | 2083 * for encoding the posting of a HTML form as a query string |
1756 * component. | 2084 * component. |
1757 * | 2085 * |
1758 * Encode the string [component] according to the HTML 4.01 rules | 2086 * Encode the string [component] according to the HTML 4.01 rules |
1759 * for encoding the posting of a HTML form as a query string | 2087 * for encoding the posting of a HTML form as a query string |
1760 * component. | 2088 * component. |
(...skipping 17 matching lines...) Expand all Loading... |
1778 * | 2106 * |
1779 * To avoid the need for explicitly encoding the query use the | 2107 * To avoid the need for explicitly encoding the query use the |
1780 * [queryParameters] optional named arguments when constructing a | 2108 * [queryParameters] optional named arguments when constructing a |
1781 * [Uri]. | 2109 * [Uri]. |
1782 * | 2110 * |
1783 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | 2111 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more |
1784 * details. | 2112 * details. |
1785 */ | 2113 */ |
1786 static String encodeQueryComponent(String component, | 2114 static String encodeQueryComponent(String component, |
1787 {Encoding encoding: UTF8}) { | 2115 {Encoding encoding: UTF8}) { |
1788 return _uriEncode( | 2116 return _uriEncode(_unreservedTable, component, encoding, true); |
1789 _unreservedTable, component, encoding: encoding, spaceToPlus: true); | |
1790 } | 2117 } |
1791 | 2118 |
1792 /** | 2119 /** |
1793 * Decodes the percent-encoding in [encodedComponent]. | 2120 * Decodes the percent-encoding in [encodedComponent]. |
1794 * | 2121 * |
1795 * Note that decoding a URI component might change its meaning as | 2122 * Note that decoding a URI component might change its meaning as |
1796 * some of the decoded characters could be characters with are | 2123 * some of the decoded characters could be characters with are |
1797 * delimiters for a given URI componene type. Always split a URI | 2124 * delimiters for a given URI componene type. Always split a URI |
1798 * component using the delimiters for the component before decoding | 2125 * component using the delimiters for the component before decoding |
1799 * the individual parts. | 2126 * the individual parts. |
1800 * | 2127 * |
1801 * For handling the [path] and [query] components consider using | 2128 * For handling the [path] and [query] components consider using |
1802 * [pathSegments] and [queryParameters] to get the separated and | 2129 * [pathSegments] and [queryParameters] to get the separated and |
1803 * decoded component. | 2130 * decoded component. |
1804 */ | 2131 */ |
1805 static String decodeComponent(String encodedComponent) { | 2132 static String decodeComponent(String encodedComponent) { |
1806 return _uriDecode(encodedComponent); | 2133 return _uriDecode(encodedComponent, 0, encodedComponent.length, |
| 2134 UTF8, false); |
1807 } | 2135 } |
1808 | 2136 |
1809 /** | 2137 /** |
1810 * Decodes the percent-encoding in [encodedComponent], converting | 2138 * Decodes the percent-encoding in [encodedComponent], converting |
1811 * pluses to spaces. | 2139 * pluses to spaces. |
1812 * | 2140 * |
1813 * It will create a byte-list of the decoded characters, and then use | 2141 * It will create a byte-list of the decoded characters, and then use |
1814 * [encoding] to decode the byte-list to a String. The default encoding is | 2142 * [encoding] to decode the byte-list to a String. The default encoding is |
1815 * UTF-8. | 2143 * UTF-8. |
1816 */ | 2144 */ |
1817 static String decodeQueryComponent( | 2145 static String decodeQueryComponent( |
1818 String encodedComponent, | 2146 String encodedComponent, |
1819 {Encoding encoding: UTF8}) { | 2147 {Encoding encoding: UTF8}) { |
1820 return _uriDecode(encodedComponent, plusToSpace: true, encoding: encoding); | 2148 return _uriDecode(encodedComponent, 0, encodedComponent.length, |
| 2149 encoding, true); |
1821 } | 2150 } |
1822 | 2151 |
1823 /** | 2152 /** |
1824 * Encode the string [uri] using percent-encoding to make it | 2153 * Encode the string [uri] using percent-encoding to make it |
1825 * safe for literal use as a full URI. | 2154 * safe for literal use as a full URI. |
1826 * | 2155 * |
1827 * All characters except uppercase and lowercase letters, digits and | 2156 * All characters except uppercase and lowercase letters, digits and |
1828 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | 2157 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This |
1829 * is the set of characters specified in in ECMA-262 version 5.1 for | 2158 * is the set of characters specified in in ECMA-262 version 5.1 for |
1830 * the encodeURI function . | 2159 * the encodeURI function . |
1831 */ | 2160 */ |
1832 static String encodeFull(String uri) { | 2161 static String encodeFull(String uri) { |
1833 return _uriEncode(_encodeFullTable, uri); | 2162 return _uriEncode(_encodeFullTable, uri, UTF8, false); |
1834 } | 2163 } |
1835 | 2164 |
1836 /** | 2165 /** |
1837 * Decodes the percent-encoding in [uri]. | 2166 * Decodes the percent-encoding in [uri]. |
1838 * | 2167 * |
1839 * Note that decoding a full URI might change its meaning as some of | 2168 * Note that decoding a full URI might change its meaning as some of |
1840 * the decoded characters could be reserved characters. In most | 2169 * the decoded characters could be reserved characters. In most |
1841 * cases an encoded URI should be parsed into components using | 2170 * cases an encoded URI should be parsed into components using |
1842 * [Uri.parse] before decoding the separate components. | 2171 * [Uri.parse] before decoding the separate components. |
1843 */ | 2172 */ |
1844 static String decodeFull(String uri) { | 2173 static String decodeFull(String uri) { |
1845 return _uriDecode(uri); | 2174 return _uriDecode(uri, 0, uri.length, UTF8, false); |
1846 } | 2175 } |
1847 | 2176 |
1848 /** | 2177 /** |
1849 * Returns the [query] split into a map according to the rules | 2178 * Returns the [query] split into a map according to the rules |
1850 * specified for FORM post in the | 2179 * specified for FORM post in the [HTML 4.01 specification section |
1851 * [HTML 4.01 specification section 17.13.4] | 2180 * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTM
L 4.01 section 17.13.4"). |
1852 * (http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 | 2181 * Each key and value in the returned map has been decoded. If the [query] |
1853 * "HTML 4.01 section 17.13.4"). Each key and value in the returned | |
1854 * map has been decoded. If the [query] | |
1855 * is the empty string an empty map is returned. | 2182 * is the empty string an empty map is returned. |
1856 * | 2183 * |
1857 * Keys in the query string that have no value are mapped to the | 2184 * Keys in the query string that have no value are mapped to the |
1858 * empty string. | 2185 * empty string. |
1859 * | 2186 * |
1860 * Each query component will be decoded using [encoding]. The default encoding | 2187 * Each query component will be decoded using [encoding]. The default encoding |
1861 * is UTF-8. | 2188 * is UTF-8. |
1862 */ | 2189 */ |
1863 static Map<String, String> splitQueryString(String query, | 2190 static Map<String, String> splitQueryString(String query, |
1864 {Encoding encoding: UTF8}) { | 2191 {Encoding encoding: UTF8}) { |
1865 return query.split("&").fold({}, (map, element) { | 2192 return query.split("&").fold({}, (map, element) { |
1866 int index = element.indexOf("="); | 2193 int index = element.indexOf("="); |
1867 if (index == -1) { | 2194 if (index == -1) { |
1868 if (element != "") { | 2195 if (element != "") { |
1869 map[decodeQueryComponent(element, encoding: encoding)] = ""; | 2196 map[decodeQueryComponent(element, encoding: encoding)] = ""; |
1870 } | 2197 } |
1871 } else if (index != 0) { | 2198 } else if (index != 0) { |
1872 var key = element.substring(0, index); | 2199 var key = element.substring(0, index); |
1873 var value = element.substring(index + 1); | 2200 var value = element.substring(index + 1); |
1874 map[Uri.decodeQueryComponent(key, encoding: encoding)] = | 2201 map[Uri.decodeQueryComponent(key, encoding: encoding)] = |
1875 decodeQueryComponent(value, encoding: encoding); | 2202 decodeQueryComponent(value, encoding: encoding); |
1876 } | 2203 } |
1877 return map; | 2204 return map; |
1878 }); | 2205 }); |
1879 } | 2206 } |
1880 | 2207 |
| 2208 static List _createList() => []; |
| 2209 |
| 2210 static Map _splitQueryStringAll( |
| 2211 String query, {Encoding encoding: UTF8}) { |
| 2212 Map result = {}; |
| 2213 int i = 0; |
| 2214 int start = 0; |
| 2215 int equalsIndex = -1; |
| 2216 |
| 2217 void parsePair(int start, int equalsIndex, int end) { |
| 2218 String key; |
| 2219 String value; |
| 2220 if (start == end) return; |
| 2221 if (equalsIndex < 0) { |
| 2222 key = _uriDecode(query, start, end, encoding, true); |
| 2223 value = ""; |
| 2224 } else { |
| 2225 key = _uriDecode(query, start, equalsIndex, encoding, true); |
| 2226 value = _uriDecode(query, equalsIndex + 1, end, encoding, true); |
| 2227 } |
| 2228 result.putIfAbsent(key, _createList).add(value); |
| 2229 } |
| 2230 |
| 2231 const int _equals = 0x3d; |
| 2232 const int _ampersand = 0x26; |
| 2233 while (i < query.length) { |
| 2234 int char = query.codeUnitAt(i); |
| 2235 if (char == _equals) { |
| 2236 if (equalsIndex < 0) equalsIndex = i; |
| 2237 } else if (char == _ampersand) { |
| 2238 parsePair(start, equalsIndex, i); |
| 2239 start = i + 1; |
| 2240 equalsIndex = -1; |
| 2241 } |
| 2242 i++; |
| 2243 } |
| 2244 parsePair(start, equalsIndex, i); |
| 2245 return result; |
| 2246 } |
| 2247 |
1881 /** | 2248 /** |
1882 * Parse the [host] as an IP version 4 (IPv4) address, returning the address | 2249 * Parse the [host] as an IP version 4 (IPv4) address, returning the address |
1883 * as a list of 4 bytes in network byte order (big endian). | 2250 * as a list of 4 bytes in network byte order (big endian). |
1884 * | 2251 * |
1885 * Throws a [FormatException] if [host] is not a valid IPv4 address | 2252 * Throws a [FormatException] if [host] is not a valid IPv4 address |
1886 * representation. | 2253 * representation. |
1887 */ | 2254 */ |
1888 static List<int> parseIPv4Address(String host) { | 2255 static List<int> parseIPv4Address(String host) { |
1889 void error(String msg) { | 2256 void error(String msg) { |
1890 throw new FormatException('Illegal IPv4 address, $msg'); | 2257 throw new FormatException('Illegal IPv4 address, $msg'); |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1992 } | 2359 } |
1993 } | 2360 } |
1994 } | 2361 } |
1995 if (wildcardSeen) { | 2362 if (wildcardSeen) { |
1996 if (parts.length > 7) { | 2363 if (parts.length > 7) { |
1997 error('an address with a wildcard must have less than 7 parts'); | 2364 error('an address with a wildcard must have less than 7 parts'); |
1998 } | 2365 } |
1999 } else if (parts.length != 8) { | 2366 } else if (parts.length != 8) { |
2000 error('an address without a wildcard must contain exactly 8 parts'); | 2367 error('an address without a wildcard must contain exactly 8 parts'); |
2001 } | 2368 } |
2002 // TODO(ajohnsen): Consider using Uint8List. | 2369 List<int> bytes = new Uint8List(16); |
2003 List bytes = new List<int>(16); | |
2004 for (int i = 0, index = 0; i < parts.length; i++) { | 2370 for (int i = 0, index = 0; i < parts.length; i++) { |
2005 int value = parts[i]; | 2371 int value = parts[i]; |
2006 if (value == -1) { | 2372 if (value == -1) { |
2007 int wildCardLength = 9 - parts.length; | 2373 int wildCardLength = 9 - parts.length; |
2008 for (int j = 0; j < wildCardLength; j++) { | 2374 for (int j = 0; j < wildCardLength; j++) { |
2009 bytes[index] = 0; | 2375 bytes[index] = 0; |
2010 bytes[index + 1] = 0; | 2376 bytes[index + 1] = 0; |
2011 index += 2; | 2377 index += 2; |
2012 } | 2378 } |
2013 } else { | 2379 } else { |
(...skipping 25 matching lines...) Expand all Loading... |
2039 static const int _UPPER_CASE_F = 0x46; | 2405 static const int _UPPER_CASE_F = 0x46; |
2040 static const int _UPPER_CASE_Z = 0x5A; | 2406 static const int _UPPER_CASE_Z = 0x5A; |
2041 static const int _LEFT_BRACKET = 0x5B; | 2407 static const int _LEFT_BRACKET = 0x5B; |
2042 static const int _BACKSLASH = 0x5C; | 2408 static const int _BACKSLASH = 0x5C; |
2043 static const int _RIGHT_BRACKET = 0x5D; | 2409 static const int _RIGHT_BRACKET = 0x5D; |
2044 static const int _LOWER_CASE_A = 0x61; | 2410 static const int _LOWER_CASE_A = 0x61; |
2045 static const int _LOWER_CASE_F = 0x66; | 2411 static const int _LOWER_CASE_F = 0x66; |
2046 static const int _LOWER_CASE_Z = 0x7A; | 2412 static const int _LOWER_CASE_Z = 0x7A; |
2047 static const int _BAR = 0x7C; | 2413 static const int _BAR = 0x7C; |
2048 | 2414 |
2049 /** | 2415 static const String _hexDigits = "0123456789ABCDEF"; |
2050 * This is the internal implementation of JavaScript's encodeURI function. | |
2051 * It encodes all characters in the string [text] except for those | |
2052 * that appear in [canonicalTable], and returns the escaped string. | |
2053 */ | |
2054 static String _uriEncode(List<int> canonicalTable, | |
2055 String text, | |
2056 {Encoding encoding: UTF8, | |
2057 bool spaceToPlus: false}) { | |
2058 byteToHex(byte, buffer) { | |
2059 const String hex = '0123456789ABCDEF'; | |
2060 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); | |
2061 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); | |
2062 } | |
2063 | 2416 |
2064 // Encode the string into bytes then generate an ASCII only string | 2417 external static String _uriEncode(List<int> canonicalTable, |
2065 // by percent encoding selected bytes. | 2418 String text, |
2066 StringBuffer result = new StringBuffer(); | 2419 Encoding encoding, |
2067 var bytes = encoding.encode(text); | 2420 bool spaceToPlus); |
2068 for (int i = 0; i < bytes.length; i++) { | |
2069 int byte = bytes[i]; | |
2070 if (byte < 128 && | |
2071 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | |
2072 result.writeCharCode(byte); | |
2073 } else if (spaceToPlus && byte == _SPACE) { | |
2074 result.writeCharCode(_PLUS); | |
2075 } else { | |
2076 result.writeCharCode(_PERCENT); | |
2077 byteToHex(byte, result); | |
2078 } | |
2079 } | |
2080 return result.toString(); | |
2081 } | |
2082 | 2421 |
2083 /** | 2422 /** |
2084 * Convert a byte (2 character hex sequence) in string [s] starting | 2423 * Convert a byte (2 character hex sequence) in string [s] starting |
2085 * at position [pos] to its ordinal value | 2424 * at position [pos] to its ordinal value |
2086 */ | 2425 */ |
2087 static int _hexCharPairToByte(String s, int pos) { | 2426 static int _hexCharPairToByte(String s, int pos) { |
2088 int byte = 0; | 2427 int byte = 0; |
2089 for (int i = 0; i < 2; i++) { | 2428 for (int i = 0; i < 2; i++) { |
2090 var charCode = s.codeUnitAt(pos + i); | 2429 var charCode = s.codeUnitAt(pos + i); |
2091 if (0x30 <= charCode && charCode <= 0x39) { | 2430 if (0x30 <= charCode && charCode <= 0x39) { |
(...skipping 17 matching lines...) Expand all Loading... |
2109 * It unescapes the string [text] and returns the unescaped string. | 2448 * It unescapes the string [text] and returns the unescaped string. |
2110 * | 2449 * |
2111 * This function is similar to the JavaScript-function `decodeURI`. | 2450 * This function is similar to the JavaScript-function `decodeURI`. |
2112 * | 2451 * |
2113 * If [plusToSpace] is `true`, plus characters will be converted to spaces. | 2452 * If [plusToSpace] is `true`, plus characters will be converted to spaces. |
2114 * | 2453 * |
2115 * The decoder will create a byte-list of the percent-encoded parts, and then | 2454 * The decoder will create a byte-list of the percent-encoded parts, and then |
2116 * decode the byte-list using [encoding]. The default encodingis UTF-8. | 2455 * decode the byte-list using [encoding]. The default encodingis UTF-8. |
2117 */ | 2456 */ |
2118 static String _uriDecode(String text, | 2457 static String _uriDecode(String text, |
2119 {bool plusToSpace: false, | 2458 int start, |
2120 Encoding encoding: UTF8}) { | 2459 int end, |
| 2460 Encoding encoding, |
| 2461 bool plusToSpace) { |
| 2462 assert(0 <= start); |
| 2463 assert(start <= end); |
| 2464 assert(end <= text.length); |
| 2465 assert(encoding != null); |
2121 // First check whether there is any characters which need special handling. | 2466 // First check whether there is any characters which need special handling. |
2122 bool simple = true; | 2467 bool simple = true; |
2123 for (int i = 0; i < text.length && simple; i++) { | 2468 for (int i = start; i < end; i++) { |
2124 var codeUnit = text.codeUnitAt(i); | 2469 var codeUnit = text.codeUnitAt(i); |
2125 simple = codeUnit != _PERCENT && codeUnit != _PLUS; | 2470 if (codeUnit > 127 || |
| 2471 codeUnit == _PERCENT || |
| 2472 (plusToSpace && codeUnit == _PLUS)) { |
| 2473 simple = false; |
| 2474 break; |
| 2475 } |
2126 } | 2476 } |
2127 List<int> bytes; | 2477 List<int> bytes; |
2128 if (simple) { | 2478 if (simple) { |
2129 if (encoding == UTF8 || encoding == LATIN1) { | 2479 if (UTF8 == encoding || LATIN1 == encoding || ASCII == encoding) { |
2130 return text; | 2480 return text.substring(start, end); |
2131 } else { | 2481 } else { |
2132 bytes = text.codeUnits; | 2482 bytes = text.substring(start, end).codeUnits; |
2133 } | 2483 } |
2134 } else { | 2484 } else { |
2135 bytes = new List(); | 2485 bytes = new List(); |
2136 for (int i = 0; i < text.length; i++) { | 2486 for (int i = start; i < end; i++) { |
2137 var codeUnit = text.codeUnitAt(i); | 2487 var codeUnit = text.codeUnitAt(i); |
2138 if (codeUnit > 127) { | 2488 if (codeUnit > 127) { |
2139 throw new ArgumentError("Illegal percent encoding in URI"); | 2489 throw new ArgumentError("Illegal percent encoding in URI"); |
2140 } | 2490 } |
2141 if (codeUnit == _PERCENT) { | 2491 if (codeUnit == _PERCENT) { |
2142 if (i + 3 > text.length) { | 2492 if (i + 3 > text.length) { |
2143 throw new ArgumentError('Truncated URI'); | 2493 throw new ArgumentError('Truncated URI'); |
2144 } | 2494 } |
2145 bytes.add(_hexCharPairToByte(text, i + 1)); | 2495 bytes.add(_hexCharPairToByte(text, i + 1)); |
2146 i += 2; | 2496 i += 2; |
2147 } else if (plusToSpace && codeUnit == _PLUS) { | 2497 } else if (plusToSpace && codeUnit == _PLUS) { |
2148 bytes.add(_SPACE); | 2498 bytes.add(_SPACE); |
2149 } else { | 2499 } else { |
2150 bytes.add(codeUnit); | 2500 bytes.add(codeUnit); |
2151 } | 2501 } |
2152 } | 2502 } |
2153 } | 2503 } |
2154 return encoding.decode(bytes); | 2504 return encoding.decode(bytes); |
2155 } | 2505 } |
2156 | 2506 |
2157 static bool _isAlphabeticCharacter(int codeUnit) | 2507 static bool _isAlphabeticCharacter(int codeUnit) { |
2158 => (codeUnit >= _LOWER_CASE_A && codeUnit <= _LOWER_CASE_Z) || | 2508 var lowerCase = codeUnit | 0x20; |
2159 (codeUnit >= _UPPER_CASE_A && codeUnit <= _UPPER_CASE_Z); | 2509 return (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_Z); |
| 2510 } |
| 2511 |
| 2512 static bool _isUnreservedChar(int char) { |
| 2513 return char < 127 && |
| 2514 ((_unreservedTable[char >> 4] & (1 << (char & 0x0f))) != 0); |
| 2515 } |
2160 | 2516 |
2161 // Tables of char-codes organized as a bit vector of 128 bits where | 2517 // Tables of char-codes organized as a bit vector of 128 bits where |
2162 // each bit indicate whether a character code on the 0-127 needs to | 2518 // each bit indicate whether a character code on the 0-127 needs to |
2163 // be escaped or not. | 2519 // be escaped or not. |
2164 | 2520 |
2165 // The unreserved characters of RFC 3986. | 2521 // The unreserved characters of RFC 3986. |
2166 static const _unreservedTable = const [ | 2522 static const _unreservedTable = const [ |
2167 // LSB MSB | 2523 // LSB MSB |
2168 // | | | 2524 // | | |
2169 0x0000, // 0x00 - 0x0f 0000000000000000 | 2525 0x0000, // 0x00 - 0x0f 0000000000000000 |
(...skipping 228 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2398 // 0123456789:; = ? | 2754 // 0123456789:; = ? |
2399 0xafff, // 0x30 - 0x3f 1111111111110101 | 2755 0xafff, // 0x30 - 0x3f 1111111111110101 |
2400 // @ABCDEFGHIJKLMNO | 2756 // @ABCDEFGHIJKLMNO |
2401 0xffff, // 0x40 - 0x4f 1111111111111111 | 2757 0xffff, // 0x40 - 0x4f 1111111111111111 |
2402 // PQRSTUVWXYZ _ | 2758 // PQRSTUVWXYZ _ |
2403 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2759 0x87ff, // 0x50 - 0x5f 1111111111100001 |
2404 // abcdefghijklmno | 2760 // abcdefghijklmno |
2405 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2761 0xfffe, // 0x60 - 0x6f 0111111111111111 |
2406 // pqrstuvwxyz ~ | 2762 // pqrstuvwxyz ~ |
2407 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2763 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
| 2764 |
2408 } | 2765 } |
| 2766 |
| 2767 // -------------------------------------------------------------------- |
| 2768 // Data URI |
| 2769 // -------------------------------------------------------------------- |
| 2770 |
| 2771 /** |
| 2772 * A way to access the structure of a `data:` URI. |
| 2773 * |
| 2774 * Data URIs are non-hierarchical URIs that can contain any binary data. |
| 2775 * They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397). |
| 2776 * |
| 2777 * This class allows parsing the URI text and extracting individual parts of the |
| 2778 * URI, as well as building the URI text from structured parts. |
| 2779 */ |
| 2780 class UriData { |
| 2781 static const int _noScheme = -1; |
| 2782 /** |
| 2783 * Contains the text content of a `data:` URI, with or without a |
| 2784 * leading `data:`. |
| 2785 * |
| 2786 * If [_separatorIndices] starts with `4` (the index of the `:`), then |
| 2787 * there is a leading `data:`, otherwise [_separatorIndices] starts with |
| 2788 * `-1`. |
| 2789 */ |
| 2790 final String _text; |
| 2791 |
| 2792 /** |
| 2793 * List of the separators (';', '=' and ',') in the text. |
| 2794 * |
| 2795 * Starts with the index of the `:` in `data:` of the mimeType. |
| 2796 * That is always either -1 or 4, depending on whether `_text` includes the |
| 2797 * `data:` scheme or not. |
| 2798 * |
| 2799 * The first speparator ends the mime type. We don't bother with finding |
| 2800 * the '/' inside the mime type. |
| 2801 * |
| 2802 * Each two separators after that marks a parameter key and value. |
| 2803 * |
| 2804 * If there is a single separator left, it ends the "base64" marker. |
| 2805 * |
| 2806 * So the following separators are found for a text: |
| 2807 * |
| 2808 * data:text/plain;foo=bar;base64,ARGLEBARGLE= |
| 2809 * ^ ^ ^ ^ ^ |
| 2810 * |
| 2811 */ |
| 2812 final List<int> _separatorIndices; |
| 2813 |
| 2814 /** |
| 2815 * Cache of the result returned by [uri]. |
| 2816 */ |
| 2817 Uri _uriCache; |
| 2818 |
| 2819 UriData._(this._text, this._separatorIndices, this._uriCache); |
| 2820 |
| 2821 /** |
| 2822 * Creates a `data:` URI containing the [content] string. |
| 2823 * |
| 2824 * Equivalent to `new Uri.dataFromString(...).data`, but may |
| 2825 * be more efficient if the [uri] itself isn't used. |
| 2826 */ |
| 2827 factory UriData.fromString(String content, |
| 2828 {String mimeType, |
| 2829 Encoding encoding, |
| 2830 Map<String, String> parameters, |
| 2831 bool base64: false}) { |
| 2832 StringBuffer buffer = new StringBuffer(); |
| 2833 List<int> indices = [_noScheme]; |
| 2834 String charsetName; |
| 2835 String encodingName; |
| 2836 if (parameters != null) charsetName = parameters["charset"]; |
| 2837 if (encoding == null) { |
| 2838 if (charsetName != null) { |
| 2839 encoding = Encoding.getByName(charsetName); |
| 2840 } |
| 2841 } else if (charsetName == null) { |
| 2842 // Non-null only if parameters does not contain "charset". |
| 2843 encodingName = encoding.name; |
| 2844 } |
| 2845 encoding ??= ASCII; |
| 2846 _writeUri(mimeType, encodingName, parameters, buffer, indices); |
| 2847 indices.add(buffer.length); |
| 2848 if (base64) { |
| 2849 buffer.write(';base64,'); |
| 2850 indices.add(buffer.length - 1); |
| 2851 buffer.write(encoding.fuse(BASE64).encode(content)); |
| 2852 } else { |
| 2853 buffer.write(','); |
| 2854 _uriEncodeBytes(_uricTable, encoding.encode(content), buffer); |
| 2855 } |
| 2856 return new UriData._(buffer.toString(), indices, null); |
| 2857 } |
| 2858 |
| 2859 /** |
| 2860 * Creates a `data:` URI containing an encoding of [bytes]. |
| 2861 * |
| 2862 * Equivalent to `new Uri.dataFromBytes(...).data`, but may |
| 2863 * be more efficient if the [uri] itself isn't used. |
| 2864 */ |
| 2865 factory UriData.fromBytes(List<int> bytes, |
| 2866 {mimeType: "application/octet-stream", |
| 2867 Map<String, String> parameters, |
| 2868 percentEncoded: false}) { |
| 2869 StringBuffer buffer = new StringBuffer(); |
| 2870 List<int> indices = [_noScheme]; |
| 2871 _writeUri(mimeType, null, parameters, buffer, indices); |
| 2872 indices.add(buffer.length); |
| 2873 if (percentEncoded) { |
| 2874 buffer.write(','); |
| 2875 _uriEncodeBytes(_uricTable, bytes, buffer); |
| 2876 } else { |
| 2877 buffer.write(';base64,'); |
| 2878 indices.add(buffer.length - 1); |
| 2879 BASE64.encoder |
| 2880 .startChunkedConversion( |
| 2881 new StringConversionSink.fromStringSink(buffer)) |
| 2882 .addSlice(bytes, 0, bytes.length, true); |
| 2883 } |
| 2884 |
| 2885 return new UriData._(buffer.toString(), indices, null); |
| 2886 } |
| 2887 |
| 2888 /** |
| 2889 * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. |
| 2890 * |
| 2891 * The [uri] must have scheme `data` and no authority or fragment, |
| 2892 * and the path (concatenated with the query, if there is one) must be valid |
| 2893 * as data URI content with the same rules as [parse]. |
| 2894 */ |
| 2895 factory UriData.fromUri(Uri uri) { |
| 2896 if (uri.scheme != "data") { |
| 2897 throw new ArgumentError.value(uri, "uri", |
| 2898 "Scheme must be 'data'"); |
| 2899 } |
| 2900 if (uri.hasAuthority) { |
| 2901 throw new ArgumentError.value(uri, "uri", |
| 2902 "Data uri must not have authority"); |
| 2903 } |
| 2904 if (uri.hasFragment) { |
| 2905 throw new ArgumentError.value(uri, "uri", |
| 2906 "Data uri must not have a fragment part"); |
| 2907 } |
| 2908 if (!uri.hasQuery) { |
| 2909 return _parse(uri.path, 0, uri); |
| 2910 } |
| 2911 // Includes path and query (and leading "data:"). |
| 2912 return _parse("$uri", 5, uri); |
| 2913 } |
| 2914 |
| 2915 /** |
| 2916 * Writes the initial part of a `data:` uri, from after the "data:" |
| 2917 * until just before the ',' before the data, or before a `;base64,` |
| 2918 * marker. |
| 2919 * |
| 2920 * Of an [indices] list is passed, separator indices are stored in that |
| 2921 * list. |
| 2922 */ |
| 2923 static void _writeUri(String mimeType, |
| 2924 String charsetName, |
| 2925 Map<String, String> parameters, |
| 2926 StringBuffer buffer, List indices) { |
| 2927 if (mimeType == null || mimeType == "text/plain") { |
| 2928 mimeType = ""; |
| 2929 } |
| 2930 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { |
| 2931 buffer.write(mimeType); // Common cases need no escaping. |
| 2932 } else { |
| 2933 int slashIndex = _validateMimeType(mimeType); |
| 2934 if (slashIndex < 0) { |
| 2935 throw new ArgumentError.value(mimeType, "mimeType", |
| 2936 "Invalid MIME type"); |
| 2937 } |
| 2938 buffer.write(Uri._uriEncode(_tokenCharTable, |
| 2939 mimeType.substring(0, slashIndex), |
| 2940 UTF8, false)); |
| 2941 buffer.write("/"); |
| 2942 buffer.write(Uri._uriEncode(_tokenCharTable, |
| 2943 mimeType.substring(slashIndex + 1), |
| 2944 UTF8, false)); |
| 2945 } |
| 2946 if (charsetName != null) { |
| 2947 if (indices != null) { |
| 2948 indices..add(buffer.length) |
| 2949 ..add(buffer.length + 8); |
| 2950 } |
| 2951 buffer.write(";charset="); |
| 2952 buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
| 2953 } |
| 2954 parameters?.forEach((var key, var value) { |
| 2955 if (key.isEmpty) { |
| 2956 throw new ArgumentError.value("", "Parameter names must not be empty"); |
| 2957 } |
| 2958 if (value.isEmpty) { |
| 2959 throw new ArgumentError.value("", "Parameter values must not be empty", |
| 2960 'parameters["$key"]'); |
| 2961 } |
| 2962 if (indices != null) indices.add(buffer.length); |
| 2963 buffer.write(';'); |
| 2964 // Encode any non-RFC2045-token character and both '%' and '#'. |
| 2965 buffer.write(Uri._uriEncode(_tokenCharTable, key, UTF8, false)); |
| 2966 if (indices != null) indices.add(buffer.length); |
| 2967 buffer.write('='); |
| 2968 buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
| 2969 }); |
| 2970 } |
| 2971 |
| 2972 /** |
| 2973 * Checks mimeType is valid-ish (`token '/' token`). |
| 2974 * |
| 2975 * Returns the index of the slash, or -1 if the mime type is not |
| 2976 * considered valid. |
| 2977 * |
| 2978 * Currently only looks for slashes, all other characters will be |
| 2979 * percent-encoded as UTF-8 if necessary. |
| 2980 */ |
| 2981 static int _validateMimeType(String mimeType) { |
| 2982 int slashIndex = -1; |
| 2983 for (int i = 0; i < mimeType.length; i++) { |
| 2984 var char = mimeType.codeUnitAt(i); |
| 2985 if (char != Uri._SLASH) continue; |
| 2986 if (slashIndex < 0) { |
| 2987 slashIndex = i; |
| 2988 continue; |
| 2989 } |
| 2990 return -1; |
| 2991 } |
| 2992 return slashIndex; |
| 2993 } |
| 2994 |
| 2995 /** |
| 2996 * Parses a string as a `data` URI. |
| 2997 * |
| 2998 * The string must have the format: |
| 2999 * |
| 3000 * ``` |
| 3001 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat
a |
| 3002 * ```` |
| 3003 * |
| 3004 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, |
| 3005 * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). |
| 3006 * |
| 3007 * This means that all the characters must be ASCII, but the URI may contain |
| 3008 * percent-escapes for non-ASCII byte values that need an interpretation |
| 3009 * to be converted to the corresponding string. |
| 3010 * |
| 3011 * Parsing doesn't check the validity of any part, it just checks that the |
| 3012 * input has the correct structure with the correct sequence of `/`, `;`, `=` |
| 3013 * and `,` delimiters. |
| 3014 * |
| 3015 * Accessing the individual parts may fail later if they turn out to have |
| 3016 * content that can't be decoded sucessfully as a string. |
| 3017 */ |
| 3018 static UriData parse(String uri) { |
| 3019 if (!uri.startsWith("data:")) { |
| 3020 throw new FormatException("Does not start with 'data:'", uri, 0); |
| 3021 } |
| 3022 return _parse(uri, 5, null); |
| 3023 } |
| 3024 |
| 3025 /** |
| 3026 * The [Uri] that this `UriData` is giving access to. |
| 3027 * |
| 3028 * Returns a `Uri` with scheme `data` and the remainder of the data URI |
| 3029 * as path. |
| 3030 */ |
| 3031 Uri get uri { |
| 3032 if (_uriCache != null) return _uriCache; |
| 3033 String path = _text; |
| 3034 String query = null; |
| 3035 int colonIndex = _separatorIndices[0]; |
| 3036 int queryIndex = _text.indexOf('?', colonIndex + 1); |
| 3037 int end = null; |
| 3038 if (queryIndex >= 0) { |
| 3039 query = _text.substring(queryIndex + 1); |
| 3040 end = queryIndex; |
| 3041 } |
| 3042 path = _text.substring(colonIndex + 1, end); |
| 3043 // TODO(lrn): This is probably too simple. We should ensure URI |
| 3044 // normalization before passing in the raw strings, maybe using |
| 3045 // Uri._makePath, Uri._makeQuery. |
| 3046 _uriCache = new Uri._internal("data", "", null, null, path, query, null); |
| 3047 return _uriCache; |
| 3048 } |
| 3049 |
| 3050 /** |
| 3051 * The MIME type of the data URI. |
| 3052 * |
| 3053 * A data URI consists of a "media type" followed by data. |
| 3054 * The media type starts with a MIME type and can be followed by |
| 3055 * extra parameters. |
| 3056 * |
| 3057 * Example: |
| 3058 * |
| 3059 * data:text/plain;charset=utf-8,Hello%20World! |
| 3060 * |
| 3061 * This data URI has the media type `text/plain;charset=utf-8`, which is the |
| 3062 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. |
| 3063 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. |
| 3064 * |
| 3065 * If the first part of the data URI is empty, it defaults to `text/plain`. |
| 3066 */ |
| 3067 String get mimeType { |
| 3068 int start = _separatorIndices[0] + 1; |
| 3069 int end = _separatorIndices[1]; |
| 3070 if (start == end) return "text/plain"; |
| 3071 return Uri._uriDecode(_text, start, end, UTF8, false); |
| 3072 } |
| 3073 |
| 3074 /** |
| 3075 * The charset parameter of the media type. |
| 3076 * |
| 3077 * If the parameters of the media type contains a `charset` parameter |
| 3078 * then this returns its value, otherwise it returns `US-ASCII`, |
| 3079 * which is the default charset for data URIs. |
| 3080 */ |
| 3081 String get charset { |
| 3082 int parameterStart = 1; |
| 3083 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. |
| 3084 if (isBase64) { |
| 3085 // There is a ";base64" separator, so subtract one for that as well. |
| 3086 parameterEnd -= 1; |
| 3087 } |
| 3088 for (int i = parameterStart; i < parameterEnd; i += 2) { |
| 3089 var keyStart = _separatorIndices[i] + 1; |
| 3090 var keyEnd = _separatorIndices[i + 1]; |
| 3091 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { |
| 3092 return Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], |
| 3093 UTF8, false); |
| 3094 } |
| 3095 } |
| 3096 return "US-ASCII"; |
| 3097 } |
| 3098 |
| 3099 /** |
| 3100 * Whether the data is Base64 encoded or not. |
| 3101 */ |
| 3102 bool get isBase64 => _separatorIndices.length.isOdd; |
| 3103 |
| 3104 /** |
| 3105 * The content part of the data URI, as its actual representation. |
| 3106 * |
| 3107 * This string may contain percent escapes. |
| 3108 */ |
| 3109 String get contentText => _text.substring(_separatorIndices.last + 1); |
| 3110 |
| 3111 /** |
| 3112 * The content part of the data URI as bytes. |
| 3113 * |
| 3114 * If the data is Base64 encoded, it will be decoded to bytes. |
| 3115 * |
| 3116 * If the data is not Base64 encoded, it will be decoded by unescaping |
| 3117 * percent-escaped characters and returning byte values of each unescaped |
| 3118 * character. The bytes will not be, e.g., UTF-8 decoded. |
| 3119 */ |
| 3120 List<int> contentAsBytes() { |
| 3121 String text = _text; |
| 3122 int start = _separatorIndices.last + 1; |
| 3123 if (isBase64) { |
| 3124 return BASE64.decoder.convert(text, start); |
| 3125 } |
| 3126 |
| 3127 // Not base64, do percent-decoding and return the remaining bytes. |
| 3128 // Compute result size. |
| 3129 const int percent = 0x25; |
| 3130 int length = text.length - start; |
| 3131 for (int i = start; i < text.length; i++) { |
| 3132 var codeUnit = text.codeUnitAt(i); |
| 3133 if (codeUnit == percent) { |
| 3134 i += 2; |
| 3135 length -= 2; |
| 3136 } |
| 3137 } |
| 3138 // Fill result array. |
| 3139 Uint8List result = new Uint8List(length); |
| 3140 if (length == text.length) { |
| 3141 result.setRange(0, length, text.codeUnits, start); |
| 3142 return result; |
| 3143 } |
| 3144 int index = 0; |
| 3145 for (int i = start; i < text.length; i++) { |
| 3146 var codeUnit = text.codeUnitAt(i); |
| 3147 if (codeUnit != percent) { |
| 3148 result[index++] = codeUnit; |
| 3149 } else { |
| 3150 if (i + 2 < text.length) { |
| 3151 var digit1 = Uri._parseHexDigit(text.codeUnitAt(i + 1)); |
| 3152 var digit2 = Uri._parseHexDigit(text.codeUnitAt(i + 2)); |
| 3153 if (digit1 >= 0 && digit2 >= 0) { |
| 3154 int byte = digit1 * 16 + digit2; |
| 3155 result[index++] = byte; |
| 3156 i += 2; |
| 3157 continue; |
| 3158 } |
| 3159 } |
| 3160 throw new FormatException("Invalid percent escape", text, i); |
| 3161 } |
| 3162 } |
| 3163 assert(index == result.length); |
| 3164 return result; |
| 3165 } |
| 3166 |
| 3167 /** |
| 3168 * Returns a string created from the content of the data URI. |
| 3169 * |
| 3170 * If the content is Base64 encoded, it will be decoded to bytes and then |
| 3171 * decoded to a string using [encoding]. |
| 3172 * If encoding is omitted, the value of a `charset` parameter is used |
| 3173 * if it is recongized by [Encoding.getByName], otherwise it defaults to |
| 3174 * the [ASCII] encoding, which is the default encoding for data URIs |
| 3175 * that do not specify an encoding. |
| 3176 * |
| 3177 * If the content is not Base64 encoded, it will first have percent-escapes |
| 3178 * converted to bytes and then the character codes and byte values are |
| 3179 * decoded using [encoding]. |
| 3180 */ |
| 3181 String contentAsString({Encoding encoding}) { |
| 3182 if (encoding == null) { |
| 3183 var charset = this.charset; // Returns "US-ASCII" if not present. |
| 3184 encoding = Encoding.getByName(charset); |
| 3185 if (encoding == null) { |
| 3186 throw new UnsupportedError("Unknown charset: $charset"); |
| 3187 } |
| 3188 } |
| 3189 String text = _text; |
| 3190 int start = _separatorIndices.last + 1; |
| 3191 if (isBase64) { |
| 3192 var converter = BASE64.decoder.fuse(encoding.decoder); |
| 3193 return converter.convert(text.substring(start)); |
| 3194 } |
| 3195 return Uri._uriDecode(text, start, text.length, encoding, false); |
| 3196 } |
| 3197 |
| 3198 /** |
| 3199 * A map representing the parameters of the media type. |
| 3200 * |
| 3201 * A data URI may contain parameters between the the MIME type and the |
| 3202 * data. This converts these parameters to a map from parameter name |
| 3203 * to parameter value. |
| 3204 * The map only contains parameters that actually occur in the URI. |
| 3205 * The `charset` parameter has a default value even if it doesn't occur |
| 3206 * in the URI, which is reflected by the [charset] getter. This means that |
| 3207 * [charset] may return a value even if `parameters["charset"]` is `null`. |
| 3208 * |
| 3209 * If the values contain non-ASCII values or percent escapes, they default |
| 3210 * to being decoded as UTF-8. |
| 3211 */ |
| 3212 Map<String, String> get parameters { |
| 3213 var result = <String, String>{}; |
| 3214 for (int i = 3; i < _separatorIndices.length; i += 2) { |
| 3215 var start = _separatorIndices[i - 2] + 1; |
| 3216 var equals = _separatorIndices[i - 1]; |
| 3217 var end = _separatorIndices[i]; |
| 3218 String key = Uri._uriDecode(_text, start, equals, UTF8, false); |
| 3219 String value = Uri._uriDecode(_text,equals + 1, end, UTF8, false); |
| 3220 result[key] = value; |
| 3221 } |
| 3222 return result; |
| 3223 } |
| 3224 |
| 3225 static UriData _parse(String text, int start, Uri sourceUri) { |
| 3226 assert(start == 0 || start == 5); |
| 3227 assert((start == 5) == text.startsWith("data:")); |
| 3228 |
| 3229 /// Character codes. |
| 3230 const int comma = 0x2c; |
| 3231 const int slash = 0x2f; |
| 3232 const int semicolon = 0x3b; |
| 3233 const int equals = 0x3d; |
| 3234 List<int> indices = [start - 1]; |
| 3235 int slashIndex = -1; |
| 3236 var char; |
| 3237 int i = start; |
| 3238 for (; i < text.length; i++) { |
| 3239 char = text.codeUnitAt(i); |
| 3240 if (char == comma || char == semicolon) break; |
| 3241 if (char == slash) { |
| 3242 if (slashIndex < 0) { |
| 3243 slashIndex = i; |
| 3244 continue; |
| 3245 } |
| 3246 throw new FormatException("Invalid MIME type", text, i); |
| 3247 } |
| 3248 } |
| 3249 if (slashIndex < 0 && i > start) { |
| 3250 // An empty MIME type is allowed, but if non-empty it must contain |
| 3251 // exactly one slash. |
| 3252 throw new FormatException("Invalid MIME type", text, i); |
| 3253 } |
| 3254 while (char != comma) { |
| 3255 // Parse parameters and/or "base64". |
| 3256 indices.add(i); |
| 3257 i++; |
| 3258 int equalsIndex = -1; |
| 3259 for (; i < text.length; i++) { |
| 3260 char = text.codeUnitAt(i); |
| 3261 if (char == equals) { |
| 3262 if (equalsIndex < 0) equalsIndex = i; |
| 3263 } else if (char == semicolon || char == comma) { |
| 3264 break; |
| 3265 } |
| 3266 } |
| 3267 if (equalsIndex >= 0) { |
| 3268 indices.add(equalsIndex); |
| 3269 } else { |
| 3270 // Have to be final "base64". |
| 3271 var lastSeparator = indices.last; |
| 3272 if (char != comma || |
| 3273 i != lastSeparator + 7 /* "base64,".length */ || |
| 3274 !text.startsWith("base64", lastSeparator + 1)) { |
| 3275 throw new FormatException("Expecting '='", text, i); |
| 3276 } |
| 3277 break; |
| 3278 } |
| 3279 } |
| 3280 indices.add(i); |
| 3281 return new UriData._(text, indices, sourceUri); |
| 3282 } |
| 3283 |
| 3284 /** |
| 3285 * Like [Uri._uriEncode] but takes the input as bytes, not a string. |
| 3286 * |
| 3287 * Encodes into [buffer] instead of creating its own buffer. |
| 3288 */ |
| 3289 static void _uriEncodeBytes(List<int> canonicalTable, |
| 3290 List<int> bytes, |
| 3291 StringSink buffer) { |
| 3292 // Encode the string into bytes then generate an ASCII only string |
| 3293 // by percent encoding selected bytes. |
| 3294 int byteOr = 0; |
| 3295 for (int i = 0; i < bytes.length; i++) { |
| 3296 int byte = bytes[i]; |
| 3297 byteOr |= byte; |
| 3298 if (byte < 128 && |
| 3299 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
| 3300 buffer.writeCharCode(byte); |
| 3301 } else { |
| 3302 buffer.writeCharCode(Uri._PERCENT); |
| 3303 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte >> 4)); |
| 3304 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte & 0x0f)); |
| 3305 } |
| 3306 } |
| 3307 if ((byteOr & ~0xFF) != 0) { |
| 3308 for (int i = 0; i < bytes.length; i++) { |
| 3309 var byte = bytes[i]; |
| 3310 if (byte < 0 || byte > 255) { |
| 3311 throw new ArgumentError.value(byte, "non-byte value"); |
| 3312 } |
| 3313 } |
| 3314 } |
| 3315 } |
| 3316 |
| 3317 String toString() => |
| 3318 (_separatorIndices[0] == _noScheme) ? "data:$_text" : _text; |
| 3319 |
| 3320 // Table of the `token` characters of RFC 2045 in a URI. |
| 3321 // |
| 3322 // A token is any US-ASCII character except SPACE, control characters and |
| 3323 // `tspecial` characters. The `tspecial` category is: |
| 3324 // '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='. |
| 3325 // |
| 3326 // In a data URI, we also need to escape '%' and '#' characters. |
| 3327 static const _tokenCharTable = const [ |
| 3328 // LSB MSB |
| 3329 // | | |
| 3330 0x0000, // 0x00 - 0x0f 00000000 00000000 |
| 3331 0x0000, // 0x10 - 0x1f 00000000 00000000 |
| 3332 // ! $ &' *+ -. |
| 3333 0x6cd2, // 0x20 - 0x2f 01001011 00110110 |
| 3334 // 01234567 89 |
| 3335 0x03ff, // 0x30 - 0x3f 11111111 11000000 |
| 3336 // ABCDEFG HIJKLMNO |
| 3337 0xfffe, // 0x40 - 0x4f 01111111 11111111 |
| 3338 // PQRSTUVW XYZ ^_ |
| 3339 0xc7ff, // 0x50 - 0x5f 11111111 11100011 |
| 3340 // `abcdefg hijklmno |
| 3341 0xffff, // 0x60 - 0x6f 11111111 11111111 |
| 3342 // pqrstuvw xyz{|}~ |
| 3343 0x7fff]; // 0x70 - 0x7f 11111111 11111110 |
| 3344 |
| 3345 // All non-escape RFC-2396 uric characters. |
| 3346 // |
| 3347 // uric = reserved | unreserved | escaped |
| 3348 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," |
| 3349 // unreserved = alphanum | mark |
| 3350 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" |
| 3351 // |
| 3352 // This is the same characters as in a URI query (which is URI pchar plus '?') |
| 3353 static const _uricTable = Uri._queryCharTable; |
| 3354 } |
OLD | NEW |