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 // Frequently used character codes. | |
8 const int _SPACE = 0x20; | |
9 const int _PERCENT = 0x25; | |
10 const int _PLUS = 0x2B; | |
11 const int _DOT = 0x2E; | |
12 const int _SLASH = 0x2F; | |
13 const int _COLON = 0x3A; | |
14 const int _UPPER_CASE_A = 0x41; | |
15 const int _UPPER_CASE_Z = 0x5A; | |
16 const int _LEFT_BRACKET = 0x5B; | |
17 const int _BACKSLASH = 0x5C; | |
18 const int _RIGHT_BRACKET = 0x5D; | |
19 const int _LOWER_CASE_A = 0x61; | |
20 const int _LOWER_CASE_F = 0x66; | |
21 const int _LOWER_CASE_Z = 0x7A; | |
22 | |
23 const String _hexDigits = "0123456789ABCDEF"; | |
24 | |
7 /** | 25 /** |
8 * A parsed URI, such as a URL. | 26 * A parsed URI, such as a URL. |
9 * | 27 * |
10 * **See also:** | 28 * **See also:** |
11 * | 29 * |
12 * * [URIs][uris] in the [library tour][libtour] | 30 * * [URIs][uris] in the [library tour][libtour] |
13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) | 31 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) |
14 * | 32 * |
15 * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris | 33 * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris |
16 * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.ht ml | 34 * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.ht ml |
17 */ | 35 */ |
18 class Uri { | 36 abstract class Uri { |
19 /** | 37 /** |
20 * The scheme component of the URI. | 38 * Returns the natural base URI for the current platform. |
21 * | 39 * |
22 * Returns the empty string if there is no scheme component. | 40 * When running in a browser this is the current URL of the current page |
41 * (from `window.location.href`). | |
23 * | 42 * |
24 * A URI scheme is case insensitive. | 43 * When not running in a browser this is the file URI referencing |
25 * The returned scheme is canonicalized to lowercase letters. | 44 * the current working directory. |
26 */ | 45 */ |
27 // We represent the missing scheme as an empty string. | 46 external static Uri get base; |
28 // A valid scheme cannot be empty. | |
29 final String scheme; | |
30 | |
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 | 47 |
90 /** | 48 /** |
91 * Creates a new URI from its components. | 49 * Creates a new URI from its components. |
92 * | 50 * |
93 * Each component is set through a named argument. Any number of | 51 * Each component is set through a named argument. Any number of |
94 * components can be provided. The [path] and [query] components can be set | 52 * components can be provided. The [path] and [query] components can be set |
95 * using either of two different named arguments. | 53 * using either of two different named arguments. |
96 * | 54 * |
97 * The scheme component is set through [scheme]. The scheme is | 55 * The scheme component is set through [scheme]. The scheme is |
98 * normalized to all lowercase letters. If the scheme is omitted or empty, | 56 * normalized to all lowercase letters. If the scheme is omitted or empty, |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
151 * use an empty map for `queryParameters`. | 109 * use an empty map for `queryParameters`. |
152 * | 110 * |
153 * If both `query` and `queryParameters` are omitted or `null`, | 111 * If both `query` and `queryParameters` are omitted or `null`, |
154 * the URI has no query part. | 112 * the URI has no query part. |
155 * | 113 * |
156 * The fragment component is set through [fragment]. | 114 * The fragment component is set through [fragment]. |
157 * It should be a valid URI fragment, but invalid characters other than | 115 * It should be a valid URI fragment, but invalid characters other than |
158 * general delimiters, are escaped if necessary. | 116 * general delimiters, are escaped if necessary. |
159 * If `fragment` is omitted or `null`, the URI has no fragment part. | 117 * If `fragment` is omitted or `null`, the URI has no fragment part. |
160 */ | 118 */ |
161 factory Uri({String scheme : "", | 119 factory Uri({String scheme, |
162 String userInfo : "", | 120 String userInfo, |
163 String host, | 121 String host, |
164 int port, | 122 int port, |
165 String path, | 123 String path, |
166 Iterable<String> pathSegments, | 124 Iterable<String> pathSegments, |
167 String query, | 125 String query, |
168 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 126 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
169 String fragment}) { | 127 String fragment}) = _Uri; |
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 | 128 |
195 /** | 129 /** |
196 * Creates a new `http` URI from authority, path and query. | 130 * Creates a new `http` URI from authority, path and query. |
197 * | 131 * |
198 * Examples: | 132 * Examples: |
199 * | 133 * |
200 * ``` | 134 * ``` |
201 * // http://example.org/path?q=dart. | 135 * // http://example.org/path?q=dart. |
202 * new Uri.http("google.com", "/search", { "q" : "dart" }); | 136 * new Uri.http("google.com", "/search", { "q" : "dart" }); |
203 * | 137 * |
(...skipping 16 matching lines...) Expand all Loading... | |
220 * | 154 * |
221 * The `path` component is set from the [unencodedPath] | 155 * The `path` component is set from the [unencodedPath] |
222 * argument. The path passed must not be encoded as this constructor | 156 * argument. The path passed must not be encoded as this constructor |
223 * encodes the path. | 157 * encodes the path. |
224 * | 158 * |
225 * The `query` component is set from the optional [queryParameters] | 159 * The `query` component is set from the optional [queryParameters] |
226 * argument. | 160 * argument. |
227 */ | 161 */ |
228 factory Uri.http(String authority, | 162 factory Uri.http(String authority, |
229 String unencodedPath, | 163 String unencodedPath, |
230 [Map<String, String> queryParameters]) { | 164 [Map<String, String> queryParameters]) = _Uri.http; |
231 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
232 } | |
233 | 165 |
234 /** | 166 /** |
235 * Creates a new `https` URI from authority, path and query. | 167 * Creates a new `https` URI from authority, path and query. |
236 * | 168 * |
237 * This constructor is the same as [Uri.http] except for the scheme | 169 * This constructor is the same as [Uri.http] except for the scheme |
238 * which is set to `https`. | 170 * which is set to `https`. |
239 */ | 171 */ |
240 factory Uri.https(String authority, | 172 factory Uri.https(String authority, |
241 String unencodedPath, | 173 String unencodedPath, |
242 [Map<String, String> queryParameters]) { | 174 [Map<String, String> queryParameters]) = _Uri.https; |
243 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | 175 |
176 /** | |
177 * Creates a new file URI from an absolute or relative file path. | |
178 * | |
179 * The file path is passed in [path]. | |
180 * | |
181 * This path is interpreted using either Windows or non-Windows | |
182 * semantics. | |
183 * | |
184 * With non-Windows semantics the slash ("/") is used to separate | |
185 * path segments. | |
186 * | |
187 * With Windows semantics, backslash ("\") and forward-slash ("/") | |
188 * are used to separate path segments, except if the path starts | |
189 * with "\\?\" in which case, only backslash ("\") separates path | |
190 * segments. | |
191 * | |
192 * If the path starts with a path separator an absolute URI is | |
193 * created. Otherwise a relative URI is created. One exception from | |
194 * this rule is that when Windows semantics is used and the path | |
195 * starts with a drive letter followed by a colon (":") and a | |
196 * path separator then an absolute URI is created. | |
197 * | |
198 * The default for whether to use Windows or non-Windows semantics | |
199 * determined from the platform Dart is running on. When running in | |
200 * the standalone VM this is detected by the VM based on the | |
201 * operating system. When running in a browser non-Windows semantics | |
202 * is always used. | |
203 * | |
204 * To override the automatic detection of which semantics to use pass | |
205 * a value for [windows]. Passing `true` will use Windows | |
206 * semantics and passing `false` will use non-Windows semantics. | |
207 * | |
208 * Examples using non-Windows semantics: | |
209 * | |
210 * ``` | |
211 * // xxx/yyy | |
212 * new Uri.file("xxx/yyy", windows: false); | |
213 * | |
214 * // xxx/yyy/ | |
215 * new Uri.file("xxx/yyy/", windows: false); | |
216 * | |
217 * // file:///xxx/yyy | |
218 * new Uri.file("/xxx/yyy", windows: false); | |
219 * | |
220 * // file:///xxx/yyy/ | |
221 * new Uri.file("/xxx/yyy/", windows: false); | |
222 * | |
223 * // C: | |
224 * new Uri.file("C:", windows: false); | |
225 * ``` | |
226 * | |
227 * Examples using Windows semantics: | |
228 * | |
229 * ``` | |
230 * // xxx/yyy | |
231 * new Uri.file(r"xxx\yyy", windows: true); | |
232 * | |
233 * // xxx/yyy/ | |
234 * new Uri.file(r"xxx\yyy\", windows: true); | |
235 * | |
236 * file:///xxx/yyy | |
237 * new Uri.file(r"\xxx\yyy", windows: true); | |
238 * | |
239 * file:///xxx/yyy/ | |
240 * new Uri.file(r"\xxx\yyy/", windows: true); | |
241 * | |
242 * // file:///C:/xxx/yyy | |
243 * new Uri.file(r"C:\xxx\yyy", windows: true); | |
244 * | |
245 * // This throws an error. A path with a drive letter is not absolute. | |
246 * new Uri.file(r"C:", windows: true); | |
247 * | |
248 * // This throws an error. A path with a drive letter is not absolute. | |
249 * new Uri.file(r"C:xxx\yyy", windows: true); | |
250 * | |
251 * // file://server/share/file | |
252 * new Uri.file(r"\\server\share\file", windows: true); | |
253 * ``` | |
254 * | |
255 * If the path passed is not a legal file path [ArgumentError] is thrown. | |
256 */ | |
257 factory Uri.file(String path, {bool windows}) = _Uri.file; | |
258 | |
259 /** | |
260 * Like [Uri.file] except that a non-empty URI path ends in a slash. | |
261 * | |
262 * If [path] is not empty, and it doesn't end in a directory separator, | |
263 * then a slash is added to the returned URI's path. | |
264 * In all other cases, the result is the same as returned by `Uri.file`. | |
265 */ | |
266 factory Uri.directory(String path, {bool windows}) = _Uri.directory; | |
267 | |
268 /** | |
269 * Creates a `data:` URI containing the [content] string. | |
270 * | |
271 * Converts the content to a bytes using [encoding] or the charset specified | |
272 * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), | |
273 * then encodes the bytes into the resulting data URI. | |
274 * | |
275 * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid | |
276 * bytes is replaced by a percent encoding). If [base64] is true, the bytes | |
277 * are instead encoded using [BASE64]. | |
278 * | |
279 * If [encoding] is not provided and [parameters] has a `charset` entry, | |
280 * that name is looked up using [Encoding.getByName], | |
281 * and if the lookup returns an encoding, that encoding is used to convert | |
282 * [content] to bytes. | |
283 * If providing both an [encoding] and a charset [parameter], they should | |
284 * agree, otherwise decoding won't be able to use the charset parameter | |
285 * to determine the encoding. | |
286 * | |
287 * If [mimeType] and/or [parameters] are supplied, they are added to the | |
288 * created URI. If any of these contain characters that are not allowed | |
289 * in the data URI, the character is percent-escaped. If the character is | |
290 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
291 * encoded. An omitted [mimeType] in a data URI means `text/plain`, just | |
292 * as an omitted `charset` parameter defaults to meaning `US-ASCII`. | |
293 * | |
294 * To read the content back, use [UriData.contentAsString]. | |
295 */ | |
296 factory Uri.dataFromString(String content, | |
297 {String mimeType, | |
298 Encoding encoding, | |
299 Map<String, String> parameters, | |
300 bool base64: false}) { | |
301 UriData data = new UriData.fromString(content, | |
302 mimeType: mimeType, | |
303 encoding: encoding, | |
304 parameters: parameters, | |
305 base64: base64); | |
306 return data.uri; | |
244 } | 307 } |
245 | 308 |
246 /** | 309 /** |
310 * Creates a `data:` URI containing an encoding of [bytes]. | |
311 * | |
312 * Defaults to Base64 encoding the bytes, but if [percentEncoded] | |
313 * is `true`, the bytes will instead be percent encoded (any non-ASCII | |
314 * or non-valid-ASCII-character byte is replaced by a percent encoding). | |
315 * | |
316 * To read the bytes back, use [UriData.contentAsBytes]. | |
317 * | |
318 * It defaults to having the mime-type `application/octet-stream`. | |
319 * The [mimeType] and [parameters] are added to the created URI. | |
320 * If any of these contain characters that are not allowed | |
321 * in the data URI, the character is percent-escaped. If the character is | |
322 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
323 * encoded. | |
324 */ | |
325 factory Uri.dataFromBytes(List<int> bytes, | |
326 {mimeType: "application/octet-stream", | |
327 Map<String, String> parameters, | |
328 percentEncoded: false}) { | |
329 UriData data = new UriData.fromBytes(bytes, | |
330 mimeType: mimeType, | |
331 parameters: parameters, | |
332 percentEncoded: percentEncoded); | |
333 return data.uri; | |
334 } | |
335 | |
336 /** | |
337 * The scheme component of the URI. | |
338 * | |
339 * Returns the empty string if there is no scheme component. | |
340 * | |
341 * A URI scheme is case insensitive. | |
342 * The returned scheme is canonicalized to lowercase letters. | |
343 */ | |
344 String get scheme; | |
345 | |
346 /** | |
247 * Returns the authority component. | 347 * Returns the authority component. |
248 * | 348 * |
249 * The authority is formatted from the [userInfo], [host] and [port] | 349 * The authority is formatted from the [userInfo], [host] and [port] |
250 * parts. | 350 * parts. |
251 * | 351 * |
252 * Returns the empty string if there is no authority component. | 352 * Returns the empty string if there is no authority component. |
253 */ | 353 */ |
254 String get authority { | 354 String get authority; |
255 if (!hasAuthority) return ""; | |
256 var sb = new StringBuffer(); | |
257 _writeAuthority(sb); | |
258 return sb.toString(); | |
259 } | |
260 | 355 |
261 /** | 356 /** |
262 * Returns the user info part of the authority component. | 357 * Returns the user info part of the authority component. |
263 * | 358 * |
264 * Returns the empty string if there is no user info in the | 359 * Returns the empty string if there is no user info in the |
265 * authority component. | 360 * authority component. |
266 */ | 361 */ |
267 String get userInfo => _userInfo; | 362 String get userInfo; |
268 | 363 |
269 /** | 364 /** |
270 * Returns the host part of the authority component. | 365 * Returns the host part of the authority component. |
271 * | 366 * |
272 * Returns the empty string if there is no authority component and | 367 * Returns the empty string if there is no authority component and |
273 * hence no host. | 368 * hence no host. |
274 * | 369 * |
275 * If the host is an IP version 6 address, the surrounding `[` and `]` is | 370 * If the host is an IP version 6 address, the surrounding `[` and `]` is |
276 * removed. | 371 * removed. |
277 * | 372 * |
278 * The host string is case-insensitive. | 373 * The host string is case-insensitive. |
279 * The returned host name is canonicalized to lower-case | 374 * The returned host name is canonicalized to lower-case |
280 * with upper-case percent-escapes. | 375 * with upper-case percent-escapes. |
281 */ | 376 */ |
282 String get host { | 377 String get host; |
283 if (_host == null) return ""; | |
284 if (_host.startsWith('[')) { | |
285 return _host.substring(1, _host.length - 1); | |
286 } | |
287 return _host; | |
288 } | |
289 | 378 |
290 /** | 379 /** |
291 * Returns the port part of the authority component. | 380 * Returns the port part of the authority component. |
292 * | 381 * |
293 * Returns the defualt port if there is no port number in the authority | 382 * Returns the defualt port if there is no port number in the authority |
294 * component. That's 80 for http, 443 for https, and 0 for everything else. | 383 * component. That's 80 for http, 443 for https, and 0 for everything else. |
295 */ | 384 */ |
296 int get port { | 385 int get port; |
297 if (_port == null) return _defaultPort(scheme); | |
298 return _port; | |
299 } | |
300 | |
301 // The default port for the scheme of this Uri.. | |
302 static int _defaultPort(String scheme) { | |
303 if (scheme == "http") return 80; | |
304 if (scheme == "https") return 443; | |
305 return 0; | |
306 } | |
307 | 386 |
308 /** | 387 /** |
309 * Returns the path component. | 388 * Returns the path component. |
310 * | 389 * |
311 * The returned path is encoded. To get direct access to the decoded | 390 * The returned path is encoded. To get direct access to the decoded |
312 * path use [pathSegments]. | 391 * path use [pathSegments]. |
313 * | 392 * |
314 * Returns the empty string if there is no path component. | 393 * Returns the empty string if there is no path component. |
315 */ | 394 */ |
316 String get path => _path; | 395 String get path; |
317 | 396 |
318 /** | 397 /** |
319 * Returns the query component. The returned query is encoded. To get | 398 * Returns the query component. The returned query is encoded. To get |
320 * direct access to the decoded query use [queryParameters]. | 399 * direct access to the decoded query use [queryParameters]. |
321 * | 400 * |
322 * Returns the empty string if there is no query component. | 401 * Returns the empty string if there is no query component. |
323 */ | 402 */ |
324 String get query => (_query == null) ? "" : _query; | 403 String get query; |
325 | 404 |
326 /** | 405 /** |
327 * Returns the fragment identifier component. | 406 * Returns the fragment identifier component. |
328 * | 407 * |
329 * Returns the empty string if there is no fragment identifier | 408 * Returns the empty string if there is no fragment identifier |
330 * component. | 409 * component. |
331 */ | 410 */ |
332 String get fragment => (_fragment == null) ? "" : _fragment; | 411 String get fragment; |
412 | |
413 /** | |
414 * Returns the URI path split into its segments. Each of the segments in the | |
415 * returned list have been decoded. If the path is empty the empty list will | |
416 * be returned. A leading slash `/` does not affect the segments returned. | |
417 * | |
418 * The returned list is unmodifiable and will throw [UnsupportedError] on any | |
419 * calls that would mutate it. | |
420 */ | |
421 List<String> get pathSegments; | |
422 | |
423 /** | |
424 * Returns the URI query split into a map according to the rules | |
425 * specified for FORM post in the [HTML 4.01 specification section | |
426 * 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"). | |
427 * Each key and value in the returned map has been decoded. | |
428 * If there is no query the empty map is returned. | |
429 * | |
430 * Keys in the query string that have no value are mapped to the | |
431 * empty string. | |
432 * If a key occurs more than once in the query string, it is mapped to | |
433 * an arbitrary choice of possible value. | |
434 * The [queryParametersAll] getter can provide a map | |
435 * that maps keys to all of their values. | |
436 * | |
437 * The returned map is unmodifiable. | |
438 */ | |
439 Map<String, String> get queryParameters; | |
440 | |
441 /** | |
442 * Returns the URI query split into a map according to the rules | |
443 * specified for FORM post in the [HTML 4.01 specification section | |
444 * 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"). | |
445 * Each key and value in the returned map has been decoded. If there is no | |
446 * query the empty map is returned. | |
447 * | |
448 * Keys are mapped to lists of their values. If a key occurs only once, | |
449 * its value is a singleton list. If a key occurs with no value, the | |
450 * empty string is used as the value for that occurrence. | |
451 * | |
452 * The returned map and the lists it contains are unmodifiable. | |
453 */ | |
454 Map<String, List<String>> get queryParametersAll; | |
455 | |
456 /** | |
457 * Returns whether the URI is absolute. | |
458 * | |
459 * A URI is an absolute URI in the sense of RFC 3986 if it has a scheme | |
460 * and no fragment. | |
461 */ | |
462 bool get isAbsolute; | |
463 | |
464 /** | |
465 * Returns whether the URI has a [scheme] component. | |
466 */ | |
467 bool get hasScheme => scheme.isNotEmpty; | |
468 | |
469 /** | |
470 * Returns whether the URI has an [authority] component. | |
471 */ | |
472 bool get hasAuthority; | |
473 | |
474 /** | |
475 * Returns whether the URI has an explicit port. | |
476 * | |
477 * If the port number is the default port number | |
478 * (zero for unrecognized schemes, with http (80) and https (443) being | |
479 * recognized), | |
480 * then the port is made implicit and omitted from the URI. | |
481 */ | |
482 bool get hasPort; | |
483 | |
484 /** | |
485 * Returns whether the URI has a query part. | |
486 */ | |
487 bool get hasQuery; | |
488 | |
489 /** | |
490 * Returns whether the URI has a fragment part. | |
491 */ | |
492 bool get hasFragment; | |
493 | |
494 /** | |
495 * Returns whether the URI has an empty path. | |
496 */ | |
497 bool get hasEmptyPath; | |
498 | |
499 /** | |
500 * Returns whether the URI has an absolute path (starting with '/'). | |
501 */ | |
502 bool get hasAbsolutePath; | |
503 | |
504 /** | |
505 * Returns the origin of the URI in the form scheme://host:port for the | |
506 * schemes http and https. | |
507 * | |
508 * It is an error if the scheme is not "http" or "https". | |
509 * | |
510 * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | |
511 */ | |
512 String get origin; | |
513 | |
514 /** | |
515 * Returns the file path from a file URI. | |
516 * | |
517 * The returned path has either Windows or non-Windows | |
518 * semantics. | |
519 * | |
520 * For non-Windows semantics the slash ("/") is used to separate | |
521 * path segments. | |
522 * | |
523 * For Windows semantics the backslash ("\") separator is used to | |
524 * separate path segments. | |
525 * | |
526 * If the URI is absolute the path starts with a path separator | |
527 * unless Windows semantics is used and the first path segment is a | |
528 * drive letter. When Windows semantics is used a host component in | |
529 * the uri in interpreted as a file server and a UNC path is | |
530 * returned. | |
531 * | |
532 * The default for whether to use Windows or non-Windows semantics | |
533 * determined from the platform Dart is running on. When running in | |
534 * the standalone VM this is detected by the VM based on the | |
535 * operating system. When running in a browser non-Windows semantics | |
536 * is always used. | |
537 * | |
538 * To override the automatic detection of which semantics to use pass | |
539 * a value for [windows]. Passing `true` will use Windows | |
540 * semantics and passing `false` will use non-Windows semantics. | |
541 * | |
542 * If the URI ends with a slash (i.e. the last path component is | |
543 * empty) the returned file path will also end with a slash. | |
544 * | |
545 * With Windows semantics URIs starting with a drive letter cannot | |
546 * be relative to the current drive on the designated drive. That is | |
547 * for the URI `file:///c:abc` calling `toFilePath` will throw as a | |
548 * path segment cannot contain colon on Windows. | |
549 * | |
550 * Examples using non-Windows semantics (resulting of calling | |
551 * toFilePath in comment): | |
552 * | |
553 * Uri.parse("xxx/yyy"); // xxx/yyy | |
554 * Uri.parse("xxx/yyy/"); // xxx/yyy/ | |
555 * Uri.parse("file:///xxx/yyy"); // /xxx/yyy | |
556 * Uri.parse("file:///xxx/yyy/"); // /xxx/yyy/ | |
557 * Uri.parse("file:///C:"); // /C: | |
558 * Uri.parse("file:///C:a"); // /C:a | |
559 * | |
560 * Examples using Windows semantics (resulting URI in comment): | |
561 * | |
562 * Uri.parse("xxx/yyy"); // xxx\yyy | |
563 * Uri.parse("xxx/yyy/"); // xxx\yyy\ | |
564 * Uri.parse("file:///xxx/yyy"); // \xxx\yyy | |
565 * Uri.parse("file:///xxx/yyy/"); // \xxx\yyy/ | |
566 * Uri.parse("file:///C:/xxx/yyy"); // C:\xxx\yyy | |
567 * Uri.parse("file:C:xxx/yyy"); // Throws as a path segment | |
568 * // cannot contain colon on Windows. | |
569 * Uri.parse("file://server/share/file"); // \\server\share\file | |
570 * | |
571 * If the URI is not a file URI calling this throws | |
572 * [UnsupportedError]. | |
573 * | |
574 * If the URI cannot be converted to a file path calling this throws | |
575 * [UnsupportedError]. | |
576 */ | |
577 // TODO(lrn): Deprecate and move functionality to File class or similar. | |
578 // The core libraries should not worry about the platform. | |
579 String toFilePath({bool windows}); | |
580 | |
581 /** | |
582 * Access the structure of a `data:` URI. | |
583 * | |
584 * Returns a [UriData] object for `data:` URIs and `null` for all other | |
585 * URIs. | |
586 * The [UriData] object can be used to access the media type and data | |
587 * of a `data:` URI. | |
588 */ | |
589 UriData get data; | |
590 | |
591 /// Returns a hash code computed as `toString().hashCode`. | |
592 /// | |
593 /// This guarantees that URIs with the same normalized | |
594 int get hashCode; | |
595 | |
596 /// A URI is equal to another URI with the same normalized representation. | |
597 bool operator==(Object other); | |
598 | |
599 /// Returns the normalized string representation of the URI. | |
600 String toString(); | |
601 | |
602 /** | |
603 * Returns a new `Uri` based on this one, but with some parts replaced. | |
604 * | |
605 * This method takes the same parameters as the [new Uri] constructor, | |
606 * and they have the same meaning. | |
607 * | |
608 * At most one of [path] and [pathSegments] must be provided. | |
609 * Likewise, at most one of [query] and [queryParameters] must be provided. | |
610 * | |
611 * Each part that is not provided will default to the corresponding | |
612 * value from this `Uri` instead. | |
613 * | |
614 * This method is different from [Uri.resolve] which overrides in a | |
615 * hierarchial manner, | |
616 * and can instead replace each part of a `Uri` individually. | |
617 * | |
618 * Example: | |
619 * | |
620 * Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); | |
621 * Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); | |
622 * print(uri2); // prints "A://b@c:4/D/E/E/?f#G" | |
623 * | |
624 * This method acts similarly to using the `new Uri` constructor with | |
625 * some of the arguments taken from this `Uri` . Example: | |
626 * | |
627 * Uri uri3 = new Uri( | |
628 * scheme: "A", | |
629 * userInfo: uri1.userInfo, | |
630 * host: uri1.host, | |
631 * port: uri1.port, | |
632 * path: "D/E/E", | |
633 * query: uri1.query, | |
634 * fragment: "G"); | |
635 * print(uri3); // prints "A://b@c:4/D/E/E/?f#G" | |
636 * print(uri2 == uri3); // prints true. | |
637 * | |
638 * Using this method can be seen as a shorthand for the `Uri` constructor | |
639 * call above, but may also be slightly faster because the parts taken | |
640 * from this `Uri` need not be checked for validity again. | |
641 */ | |
642 Uri replace({String scheme, | |
643 String userInfo, | |
644 String host, | |
645 int port, | |
646 String path, | |
647 Iterable<String> pathSegments, | |
648 String query, | |
649 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | |
650 String fragment}); | |
651 | |
652 /** | |
653 * Returns a `Uri` that differs from this only in not having a fragment. | |
654 * | |
655 * If this `Uri` does not have a fragment, it is itself returned. | |
656 */ | |
657 Uri removeFragment(); | |
658 | |
659 /** | |
660 * Resolve [reference] as an URI relative to `this`. | |
661 * | |
662 * First turn [reference] into a URI using [Uri.parse]. Then resolve the | |
663 * resulting URI relative to `this`. | |
664 * | |
665 * Returns the resolved URI. | |
666 * | |
667 * See [resolveUri] for details. | |
668 */ | |
669 Uri resolve(String reference); | |
670 | |
671 /** | |
672 * Resolve [reference] as an URI relative to `this`. | |
673 * | |
674 * Returns the resolved URI. | |
675 * | |
676 * The algorithm "Transform Reference" for resolving a reference is described | |
677 * in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1 123"). | |
678 * | |
679 * Updated to handle the case where the base URI is just a relative path - | |
680 * that is: when it has no scheme or authority and the path does not start | |
681 * with a slash. | |
682 * In that case, the paths are combined without removing leading "..", and | |
683 * an empty path is not converted to "/". | |
684 */ | |
685 Uri resolveUri(Uri reference); | |
686 | |
687 /** | |
688 * Returns a URI where the path has been normalized. | |
689 * | |
690 * A normalized path does not contain `.` segments or non-leading `..` | |
691 * segments. | |
692 * Only a relative path with no scheme or authority may contain | |
693 * leading `..` segments, | |
694 * a path that starts with `/` will also drop any leading `..` segments. | |
695 * | |
696 * This uses the same normalization strategy as `new Uri().resolve(this)`. | |
697 * | |
698 * Does not change any part of the URI except the path. | |
699 * | |
700 * The default implementation of `Uri` always normalizes paths, so calling | |
701 * this function has no effect. | |
702 */ | |
703 Uri normalizePath(); | |
333 | 704 |
334 /** | 705 /** |
335 * Creates a new `Uri` object by parsing a URI string. | 706 * Creates a new `Uri` object by parsing a URI string. |
336 * | 707 * |
337 * If [start] and [end] are provided, only the substring from `start` | 708 * If [start] and [end] are provided, only the substring from `start` |
338 * to `end` is parsed as a URI. | 709 * to `end` is parsed as a URI. |
339 * | 710 * |
340 * If the string is not valid as a URI or URI reference, | 711 * If the string is not valid as a URI or URI reference, |
341 * a [FormatException] is thrown. | 712 * a [FormatException] is thrown. |
342 */ | 713 */ |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
387 // segment = *pchar | 758 // segment = *pchar |
388 // segment-nz = 1*pchar | 759 // segment-nz = 1*pchar |
389 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 760 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
390 // ; non-zero-length segment without any colon ":" | 761 // ; non-zero-length segment without any colon ":" |
391 // | 762 // |
392 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 763 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
393 // | 764 // |
394 // query = *( pchar / "/" / "?" ) | 765 // query = *( pchar / "/" / "?" ) |
395 // | 766 // |
396 // fragment = *( pchar / "/" / "?" ) | 767 // fragment = *( pchar / "/" / "?" ) |
397 const int EOI = -1; | 768 end ??= uri.length; |
398 | 769 |
399 String scheme = ""; | 770 // Special case data:URIs. Ignore case when testing. |
400 String userinfo = ""; | 771 if (end >= start + 5) { |
401 String host = null; | 772 int dataDelta = _startsWithData(uri, start); |
402 int port = null; | 773 if (dataDelta == 0) { |
403 String path = null; | 774 // The case is right. |
404 String query = null; | 775 if (start > 0 || end < uri.length) uri = uri.substring(start, end); |
405 String fragment = null; | 776 return UriData._parse(uri, 5, null).uri; |
406 if (end == null) end = uri.length; | 777 } else if (dataDelta == 0x20) { |
407 | 778 return UriData._parse(uri.substring(start + 5, end), 0, null).uri; |
408 int index = start; | 779 } |
409 int pathStart = start; | 780 // Otherwise the URI doesn't start with "data:" or any case variant of it. |
410 // End of input-marker. | 781 } |
411 int char = EOI; | 782 |
412 | 783 // TODO(lrn): Consider inlining _scanUri here. |
413 void parseAuth() { | 784 // The following index-normalization belongs with the scanning, but is |
414 if (index == end) { | 785 // easier to do here because we already have extracted variables from the |
floitsch
2016/06/29 23:41:47
I think you should do the normalizations in _scanU
Lasse Reichstein Nielsen
2016/06/30 10:27:31
It probably shouldn't but in practice I see a ~10%
| |
415 char = EOI; | 786 // indices list. |
416 return; | 787 var indices = _scanUri(uri, start, end); |
417 } | 788 |
418 int authStart = index; | 789 int schemeEnd = indices[_schemeEndIndex]; |
419 int lastColon = -1; | 790 int hostStart = indices[_hostStartIndex] + 1; |
420 int lastAt = -1; | 791 int portStart = indices[_portStartIndex]; |
421 char = uri.codeUnitAt(index); | 792 int pathStart = indices[_pathStartIndex]; |
422 while (index < end) { | 793 int queryStart = indices[_queryStartIndex]; |
423 char = uri.codeUnitAt(index); | 794 int fragmentStart = indices[_fragmentStartIndex]; |
424 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { | 795 |
425 break; | 796 // We may discover scheme while handling special cases. |
426 } | 797 String scheme; |
427 if (char == _AT_SIGN) { | 798 |
428 lastAt = index; | 799 // Derive some indices that weren't set to normalize the indices. |
429 lastColon = -1; | 800 // If fragment but no query, set query to start at fragment. |
430 } else if (char == _COLON) { | 801 if (fragmentStart < queryStart) queryStart = fragmentStart; |
431 lastColon = index; | 802 // If scheme but no authority, the pathStart isn't set. |
432 } else if (char == _LEFT_BRACKET) { | 803 if (schemeEnd >= start && hostStart <= start) pathStart = schemeEnd + 1; |
433 lastColon = -1; | 804 // If scheme or authority but pathStart isn't set. |
434 int endBracket = uri.indexOf(']', index + 1); | 805 if (pathStart == start && (schemeEnd >= start || hostStart > start)) { |
435 if (endBracket == -1) { | 806 pathStart = queryStart; |
436 index = end; | 807 } |
437 char = EOI; | 808 // If authority and no port. |
438 break; | 809 // (including when user-info contains : and portStart >= 0). |
439 } else { | 810 if (portStart < hostStart) portStart = pathStart; |
440 index = endBracket; | 811 |
812 assert(hostStart == start || schemeEnd <= hostStart); | |
813 assert(hostStart <= portStart); | |
814 assert(schemeEnd <= pathStart); | |
815 assert(portStart <= pathStart); | |
816 assert(pathStart <= queryStart); | |
817 assert(queryStart <= fragmentStart); | |
818 | |
819 bool isSimple = indices[_notSimpleIndex] < start; | |
820 | |
821 if (isSimple) { | |
822 // Check/do normalizations that weren't detected by the scanner. | |
823 // This includes removal of empty port or userInfo, | |
824 // or scheme specific port and path normalizations. | |
825 if (hostStart > schemeEnd + 3) { | |
826 // Always be non-simple if URI contains user-info. | |
827 // The scanner doesn't set the not-simple position in this case because | |
828 // it's setting the host-start position instead. | |
829 isSimple = false; | |
830 } else if (portStart > start && portStart + 1 == pathStart) { | |
831 // If the port is empty, it should be omitted. | |
832 // Pathological case, don't bother correcting it. | |
833 isSimple = false; | |
834 } else if (hostStart == schemeEnd + 4) { | |
835 // If the userInfo is empty, it should be omitted. | |
836 // (4 is length of "://@"). | |
837 // Pathological case, don't bother correcting it. | |
838 isSimple = false; | |
839 } else if (queryStart < end && | |
840 (queryStart == pathStart + 2 && | |
841 uri.startsWith("..", pathStart)) || | |
842 (queryStart > pathStart + 2 && | |
843 uri.startsWith("/..", queryStart - 3))) { | |
844 // The path ends in a ".." segment. This should be normalized to "../". | |
845 // We didn't detect this while scanning because a query or fragment was | |
846 // detected at the same time (which is why we only need to check this | |
847 // if there is something after the path). | |
848 isSimple = false; | |
849 } else { | |
850 // There are a few scheme-based normalizations that | |
851 // the scanner couldn't check. | |
852 // That means that the input is very close to simple, so just do | |
853 // the normalizations. | |
854 if (schemeEnd == start + 4) { | |
855 // Do scheme based normalizations for file, http. | |
856 if (uri.startsWith("file", start)) { | |
857 scheme = "file"; | |
858 if (hostStart <= start) { | |
859 // File URIs should have an authority. | |
860 // Paths after an authority should be absolute. | |
861 String schemeAuth = "file://"; | |
862 int delta = 2; | |
863 if (!uri.startsWith("/", pathStart)) { | |
864 schemeAuth = "file:///"; | |
865 delta = 3; | |
866 } | |
867 uri = schemeAuth + uri.substring(pathStart, end); | |
868 schemeEnd -= start; | |
869 hostStart = 7; | |
870 portStart = 7; | |
871 pathStart = 7; | |
872 queryStart += delta - start; | |
873 fragmentStart += delta - start; | |
874 start = 0; | |
875 end = uri.length; | |
876 } else if (pathStart == queryStart) { | |
877 // Uri has authority and empty path. Add "/" as path. | |
878 uri = "${uri.substring(start, pathStart)}/" | |
879 "${uri.substring(queryStart, end)}"; | |
880 schemeEnd -= start; | |
881 hostStart -= start; | |
882 portStart -= start; | |
883 pathStart -= start; | |
884 queryStart += 1 - start; | |
885 fragmentStart += 1 - start; | |
886 start = 0; | |
887 end = uri.length; | |
888 } | |
889 } else if (uri.startsWith("http", start)) { | |
890 scheme = "http"; | |
891 // HTTP URIs should not have an explicit port of 80. | |
892 if (portStart > start && portStart + 3 == pathStart && | |
893 uri.startsWith("80", portStart + 1)) { | |
894 uri = uri.substring(start, portStart) + | |
895 uri.substring(pathStart, end); | |
896 schemeEnd -= start; | |
897 hostStart -= start; | |
898 portStart -= start; | |
899 pathStart -= 3 + start; | |
900 queryStart -= 3 + start; | |
901 fragmentStart -= 3 + start; | |
902 start = 0; | |
903 end = uri.length; | |
904 } | |
905 } | |
906 } else if (schemeEnd == start + 5 && uri.startsWith("https", start)) { | |
907 scheme = "https"; | |
908 // HTTPS URIs should not have an explicit port of 443. | |
909 if (portStart > start && portStart + 4 == pathStart && | |
910 uri.startsWith("443", portStart + 1)) { | |
911 uri = uri.substring(start, portStart) + | |
912 uri.substring(pathStart, end); | |
913 schemeEnd -= start; | |
914 hostStart -= start; | |
915 portStart -= start; | |
916 pathStart -= 4 + start; | |
917 queryStart -= 4 + start; | |
918 fragmentStart -= 4 + start; | |
919 start = 0; | |
920 end = uri.length; | |
441 } | 921 } |
442 } | 922 } |
443 index++; | 923 } |
444 char = EOI; | 924 } |
445 } | 925 |
446 int hostStart = authStart; | 926 if (isSimple) { |
447 int hostEnd = index; | 927 if (start > 0 || end < uri.length) { |
448 if (lastAt >= 0) { | 928 uri = uri.substring(start, end); |
449 userinfo = _makeUserInfo(uri, authStart, lastAt); | 929 if (schemeEnd >= 0) schemeEnd -= start; |
450 hostStart = lastAt + 1; | 930 if (hostStart > 0) { |
451 } | 931 hostStart -= start; |
452 if (lastColon >= 0) { | 932 portStart -= start; |
453 int portNumber; | 933 } |
454 if (lastColon + 1 < index) { | 934 pathStart -= start; |
455 portNumber = 0; | 935 queryStart -= start; |
456 for (int i = lastColon + 1; i < index; i++) { | 936 fragmentStart -= start; |
457 int digit = uri.codeUnitAt(i); | 937 } |
458 if (_ZERO > digit || _NINE < digit) { | 938 return new _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, |
459 _fail(uri, i, "Invalid port number"); | 939 queryStart, fragmentStart, scheme); |
460 } | 940 |
461 portNumber = portNumber * 10 + (digit - _ZERO); | 941 } |
942 | |
943 return new _Uri.notSimple(uri, start, end, schemeEnd, hostStart, portStart, | |
944 pathStart, queryStart, fragmentStart, scheme); | |
945 } | |
946 | |
947 /** | |
948 * Encode the string [component] using percent-encoding to make it | |
949 * safe for literal use as a URI component. | |
950 * | |
951 * All characters except uppercase and lowercase letters, digits and | |
952 * the characters `-_.!~*'()` are percent-encoded. This is the | |
953 * set of characters specified in RFC 2396 and the which is | |
954 * specified for the encodeUriComponent in ECMA-262 version 5.1. | |
955 * | |
956 * When manually encoding path segments or query components remember | |
957 * to encode each part separately before building the path or query | |
958 * string. | |
959 * | |
960 * For encoding the query part consider using | |
961 * [encodeQueryComponent]. | |
962 * | |
963 * To avoid the need for explicitly encoding use the [pathSegments] | |
964 * and [queryParameters] optional named arguments when constructing | |
965 * a [Uri]. | |
966 */ | |
967 static String encodeComponent(String component) { | |
968 return _Uri._uriEncode(_Uri._unreserved2396Table, component, UTF8, false); | |
969 } | |
970 | |
971 /** | |
972 * Encode the string [component] according to the HTML 4.01 rules | |
973 * for encoding the posting of a HTML form as a query string | |
974 * component. | |
975 * | |
976 * Encode the string [component] according to the HTML 4.01 rules | |
977 * for encoding the posting of a HTML form as a query string | |
978 * component. | |
979 | |
980 * The component is first encoded to bytes using [encoding]. | |
981 * The default is to use [UTF8] encoding, which preserves all | |
982 * the characters that don't need encoding. | |
983 | |
984 * Then the resulting bytes are "percent-encoded". This transforms | |
985 * spaces (U+0020) to a plus sign ('+') and all bytes that are not | |
986 * the ASCII decimal digits, letters or one of '-._~' are written as | |
987 * a percent sign '%' followed by the two-digit hexadecimal | |
988 * representation of the byte. | |
989 | |
990 * Note that the set of characters which are percent-encoded is a | |
991 * superset of what HTML 4.01 requires, since it refers to RFC 1738 | |
992 * for reserved characters. | |
993 * | |
994 * When manually encoding query components remember to encode each | |
995 * part separately before building the query string. | |
996 * | |
997 * To avoid the need for explicitly encoding the query use the | |
998 * [queryParameters] optional named arguments when constructing a | |
999 * [Uri]. | |
1000 * | |
1001 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | |
1002 * details. | |
1003 */ | |
1004 static String encodeQueryComponent(String component, | |
1005 {Encoding encoding: UTF8}) { | |
1006 return _Uri._uriEncode(_Uri._unreservedTable, component, encoding, true); | |
1007 } | |
1008 | |
1009 /** | |
1010 * Decodes the percent-encoding in [encodedComponent]. | |
1011 * | |
1012 * Note that decoding a URI component might change its meaning as | |
1013 * some of the decoded characters could be characters with are | |
1014 * delimiters for a given URI componene type. Always split a URI | |
1015 * component using the delimiters for the component before decoding | |
1016 * the individual parts. | |
1017 * | |
1018 * For handling the [path] and [query] components consider using | |
1019 * [pathSegments] and [queryParameters] to get the separated and | |
1020 * decoded component. | |
1021 */ | |
1022 static String decodeComponent(String encodedComponent) { | |
1023 return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | |
1024 UTF8, false); | |
1025 } | |
1026 | |
1027 /** | |
1028 * Decodes the percent-encoding in [encodedComponent], converting | |
1029 * pluses to spaces. | |
1030 * | |
1031 * It will create a byte-list of the decoded characters, and then use | |
1032 * [encoding] to decode the byte-list to a String. The default encoding is | |
1033 * UTF-8. | |
1034 */ | |
1035 static String decodeQueryComponent( | |
1036 String encodedComponent, | |
1037 {Encoding encoding: UTF8}) { | |
1038 return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | |
1039 encoding, true); | |
1040 } | |
1041 | |
1042 /** | |
1043 * Encode the string [uri] using percent-encoding to make it | |
1044 * safe for literal use as a full URI. | |
1045 * | |
1046 * All characters except uppercase and lowercase letters, digits and | |
1047 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | |
1048 * is the set of characters specified in in ECMA-262 version 5.1 for | |
1049 * the encodeURI function . | |
1050 */ | |
1051 static String encodeFull(String uri) { | |
1052 return _Uri._uriEncode(_Uri._encodeFullTable, uri, UTF8, false); | |
1053 } | |
1054 | |
1055 /** | |
1056 * Decodes the percent-encoding in [uri]. | |
1057 * | |
1058 * Note that decoding a full URI might change its meaning as some of | |
1059 * the decoded characters could be reserved characters. In most | |
1060 * cases an encoded URI should be parsed into components using | |
1061 * [Uri.parse] before decoding the separate components. | |
1062 */ | |
1063 static String decodeFull(String uri) { | |
1064 return _Uri._uriDecode(uri, 0, uri.length, UTF8, false); | |
1065 } | |
1066 | |
1067 /** | |
1068 * Returns the [query] split into a map according to the rules | |
1069 * specified for FORM post in the [HTML 4.01 specification section | |
1070 * 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"). | |
1071 * Each key and value in the returned map has been decoded. If the [query] | |
1072 * is the empty string an empty map is returned. | |
1073 * | |
1074 * Keys in the query string that have no value are mapped to the | |
1075 * empty string. | |
1076 * | |
1077 * Each query component will be decoded using [encoding]. The default encoding | |
1078 * is UTF-8. | |
1079 */ | |
1080 static Map<String, String> splitQueryString(String query, | |
1081 {Encoding encoding: UTF8}) { | |
1082 return query.split("&").fold({}, (map, element) { | |
1083 int index = element.indexOf("="); | |
1084 if (index == -1) { | |
1085 if (element != "") { | |
1086 map[decodeQueryComponent(element, encoding: encoding)] = ""; | |
1087 } | |
1088 } else if (index != 0) { | |
1089 var key = element.substring(0, index); | |
1090 var value = element.substring(index + 1); | |
1091 map[decodeQueryComponent(key, encoding: encoding)] = | |
1092 decodeQueryComponent(value, encoding: encoding); | |
1093 } | |
1094 return map; | |
1095 }); | |
1096 } | |
1097 | |
1098 | |
1099 /** | |
1100 * Parse the [host] as an IP version 4 (IPv4) address, returning the address | |
1101 * as a list of 4 bytes in network byte order (big endian). | |
1102 * | |
1103 * Throws a [FormatException] if [host] is not a valid IPv4 address | |
1104 * representation. | |
1105 */ | |
1106 static List<int> parseIPv4Address(String host) { | |
1107 void error(String msg) { | |
1108 throw new FormatException('Illegal IPv4 address, $msg', host); | |
1109 } | |
1110 var bytes = host.split('.'); | |
1111 if (bytes.length != 4) { | |
1112 error('IPv4 address should contain exactly 4 parts'); | |
1113 } | |
1114 var result = new Uint8List(4); | |
1115 for (int i = 0; i < 4; i++) { | |
1116 var byteString = bytes[i]; | |
1117 int byte = int.parse(byteString); | |
1118 if (byte < 0 || byte > 255) { | |
1119 error('each part must be in the range of `0..255`'); | |
1120 } | |
1121 result[i] = byte; | |
1122 } | |
1123 return result; | |
1124 } | |
1125 | |
1126 /** | |
1127 * Parse the [host] as an IP version 6 (IPv6) address, returning the address | |
1128 * as a list of 16 bytes in network byte order (big endian). | |
1129 * | |
1130 * Throws a [FormatException] if [host] is not a valid IPv6 address | |
1131 * representation. | |
1132 * | |
1133 * Acts on the substring from [start] to [end]. If [end] is omitted, it | |
1134 * defaults ot the end of the string. | |
1135 * | |
1136 * Some examples of IPv6 addresses: | |
1137 * * ::1 | |
1138 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | |
1139 * * 3ffe:2a00:100:7031::1 | |
1140 * * ::FFFF:129.144.52.38 | |
1141 * * 2010:836B:4179::836B:4179 | |
1142 */ | |
1143 static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | |
1144 if (end == null) end = host.length; | |
1145 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated | |
1146 // by `:`'s, with the following exceptions: | |
1147 // | |
1148 // - One (and only one) wildcard (`::`) may be present, representing a fill | |
1149 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. | |
1150 // - The last two parts may be replaced by an IPv4 address. | |
1151 void error(String msg, [position]) { | |
1152 throw new FormatException('Illegal IPv6 address, $msg', host, position); | |
1153 } | |
1154 int parseHex(int start, int end) { | |
floitsch
2016/06/29 23:41:47
newline before and after nested functions.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
| |
1155 if (end - start > 4) { | |
1156 error('an IPv6 part can only contain a maximum of 4 hex digits', start); | |
1157 } | |
1158 int value = int.parse(host.substring(start, end), radix: 16); | |
1159 if (value < 0 || value > (1 << 16) - 1) { | |
1160 error('each part must be in the range of `0x0..0xFFFF`', start); | |
1161 } | |
1162 return value; | |
1163 } | |
1164 if (host.length < 2) error('address is too short'); | |
1165 List<int> parts = []; | |
1166 bool wildcardSeen = false; | |
1167 int partStart = start; | |
1168 // Parse all parts, except a potential last one. | |
1169 for (int i = start; i < end; i++) { | |
1170 if (host.codeUnitAt(i) == _COLON) { | |
1171 if (i == start) { | |
1172 // If we see a `:` in the beginning, expect wildcard. | |
1173 i++; | |
1174 if (host.codeUnitAt(i) != _COLON) { | |
1175 error('invalid start colon.', i); | |
462 } | 1176 } |
1177 partStart = i; | |
463 } | 1178 } |
464 port = _makePort(portNumber, scheme); | 1179 if (i == partStart) { |
465 hostEnd = lastColon; | 1180 // Wildcard. We only allow one. |
466 } | 1181 if (wildcardSeen) { |
467 host = _makeHost(uri, hostStart, hostEnd, true); | 1182 error('only one wildcard `::` is allowed', i); |
468 if (index < end) { | 1183 } |
469 char = uri.codeUnitAt(index); | 1184 wildcardSeen = true; |
470 } | 1185 parts.add(-1); |
471 } | 1186 } else { |
472 | 1187 // Found a single colon. Parse [partStart..i] as a hex entry. |
473 // When reaching path parsing, the current character is known to not | 1188 parts.add(parseHex(partStart, i)); |
474 // be part of the path. | |
475 const int NOT_IN_PATH = 0; | |
476 // When reaching path parsing, the current character is part | |
477 // of the a non-empty path. | |
478 const int IN_PATH = 1; | |
479 // When reaching authority parsing, authority is possible. | |
480 // This is only true at start or right after scheme. | |
481 const int ALLOW_AUTH = 2; | |
482 | |
483 // Current state. | |
484 // Initialized to the default value that is used when exiting the | |
485 // scheme loop by reaching the end of input. | |
486 // All other breaks set their own state. | |
487 int state = NOT_IN_PATH; | |
488 int i = index; // Temporary alias for index to avoid bug 19550 in dart2js. | |
489 while (i < end) { | |
490 char = uri.codeUnitAt(i); | |
491 if (char == _QUESTION || char == _NUMBER_SIGN) { | |
492 state = NOT_IN_PATH; | |
493 break; | |
494 } | |
495 if (char == _SLASH) { | |
496 state = (i == start) ? ALLOW_AUTH : IN_PATH; | |
497 break; | |
498 } | |
499 if (char == _COLON) { | |
500 if (i == start) _fail(uri, start, "Invalid empty scheme"); | |
501 scheme = _makeScheme(uri, start, i); | |
502 i++; | |
503 if (scheme == "data") { | |
504 // This generates a URI that is (potentially) not path normalized. | |
505 // Applying part normalization to a non-hierarchial URI isn't | |
506 // meaningful. | |
507 return UriData._parse(uri, i, null).uri; | |
508 } | 1189 } |
509 pathStart = i; | 1190 partStart = i + 1; |
510 if (i == end) { | 1191 } |
511 char = EOI; | 1192 } |
512 state = NOT_IN_PATH; | 1193 if (parts.length == 0) error('too few parts'); |
513 } else { | 1194 bool atEnd = (partStart == end); |
514 char = uri.codeUnitAt(i); | 1195 bool isLastWildcard = (parts.last == -1); |
515 if (char == _QUESTION || char == _NUMBER_SIGN) { | 1196 if (atEnd && !isLastWildcard) { |
516 state = NOT_IN_PATH; | 1197 error('expected a part after last `:`', end); |
517 } else if (char == _SLASH) { | 1198 } |
518 state = ALLOW_AUTH; | 1199 if (!atEnd) { |
519 } else { | 1200 try { |
floitsch
2016/06/29 23:41:47
I would assume that the try/catch costs performanc
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Rewritten to not use try/catch.
This means that th
| |
520 state = IN_PATH; | 1201 parts.add(parseHex(partStart, end)); |
521 } | 1202 } catch (e) { |
1203 // Failed to parse the last chunk as hex. Try IPv4. | |
1204 try { | |
1205 List<int> last = parseIPv4Address(host.substring(partStart, end)); | |
1206 parts.add(last[0] << 8 | last[1]); | |
1207 parts.add(last[2] << 8 | last[3]); | |
1208 } catch (e) { | |
1209 error('invalid end of IPv6 address.', partStart); | |
522 } | 1210 } |
523 break; | 1211 } |
524 } | 1212 } |
525 i++; | 1213 if (wildcardSeen) { |
526 char = EOI; | 1214 if (parts.length > 7) { |
527 } | 1215 error('an address with a wildcard must have less than 7 parts'); |
528 index = i; // Remove alias when bug is fixed. | 1216 } |
529 | 1217 } else if (parts.length != 8) { |
530 if (state == ALLOW_AUTH) { | 1218 error('an address without a wildcard must contain exactly 8 parts'); |
531 assert(char == _SLASH); | 1219 } |
532 // Have seen one slash either at start or right after scheme. | 1220 List<int> bytes = new Uint8List(16); |
533 // If two slashes, it's an authority, otherwise it's just the path. | 1221 for (int i = 0, index = 0; i < parts.length; i++) { |
534 index++; | 1222 int value = parts[i]; |
535 if (index == end) { | 1223 if (value == -1) { |
536 char = EOI; | 1224 int wildCardLength = 9 - parts.length; |
537 state = NOT_IN_PATH; | 1225 for (int j = 0; j < wildCardLength; j++) { |
1226 bytes[index] = 0; | |
1227 bytes[index + 1] = 0; | |
1228 index += 2; | |
1229 } | |
538 } else { | 1230 } else { |
539 char = uri.codeUnitAt(index); | 1231 bytes[index] = value >> 8; |
540 if (char == _SLASH) { | 1232 bytes[index + 1] = value & 0xff; |
541 index++; | 1233 index += 2; |
542 parseAuth(); | 1234 } |
543 pathStart = index; | 1235 } |
544 } | 1236 return bytes; |
545 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { | 1237 } |
546 state = NOT_IN_PATH; | 1238 } |
547 } else { | 1239 |
548 state = IN_PATH; | 1240 class _Uri implements Uri { |
549 } | 1241 // We represent the missing scheme as an empty string. |
550 } | 1242 // A valid scheme cannot be empty. |
551 } | 1243 final String scheme; |
552 | 1244 |
553 assert(state == IN_PATH || state == NOT_IN_PATH); | 1245 /** |
554 if (state == IN_PATH) { | 1246 * The user-info part of the authority. |
555 // Characters from pathStart to index (inclusive) are known | 1247 * |
556 // to be part of the path. | 1248 * Does not distinguish between an empty user-info and an absent one. |
557 while (++index < end) { | 1249 * The value is always non-null. |
558 char = uri.codeUnitAt(index); | 1250 * Is considered absent if [_host] is `null`. |
559 if (char == _QUESTION || char == _NUMBER_SIGN) { | 1251 */ |
560 break; | 1252 final String _userInfo; |
561 } | 1253 |
562 char = EOI; | 1254 /** |
563 } | 1255 * The host name of the URI. |
564 state = NOT_IN_PATH; | 1256 * |
565 } | 1257 * Set to `null` if there is no authority in the URI. |
566 | 1258 * The host name is the only mandatory part of an authority, so we use |
567 assert(state == NOT_IN_PATH); | 1259 * it to mark whether an authority part was present or not. |
1260 */ | |
1261 final String _host; | |
1262 | |
1263 /** | |
1264 * The port number part of the authority. | |
1265 * | |
1266 * The port. Set to null if there is no port. Normalized to null if | |
1267 * the port is the default port for the scheme. | |
1268 */ | |
1269 int _port; | |
1270 | |
1271 /** | |
1272 * The path of the URI. | |
1273 * | |
1274 * Always non-null. | |
1275 */ | |
1276 String _path; | |
1277 | |
1278 // The query content, or null if there is no query. | |
1279 final String _query; | |
1280 | |
1281 // The fragment content, or null if there is no fragment. | |
1282 final String _fragment; | |
1283 | |
1284 /** | |
1285 * Cache the computed return value of [pathSegements]. | |
1286 */ | |
1287 List<String> _pathSegments; | |
1288 | |
1289 /** | |
1290 * Cache of the full normalized text representation of the URI. | |
1291 */ | |
1292 String _text; | |
1293 | |
1294 /** | |
1295 * Cache the computed return value of [queryParameters]. | |
1296 */ | |
1297 Map<String, String> _queryParameters; | |
1298 Map<String, List<String>> _queryParameterLists; | |
1299 | |
1300 /// Internal non-verifying constructor. Only call with validated arguments. | |
1301 _Uri._internal(this.scheme, | |
1302 this._userInfo, | |
1303 this._host, | |
1304 this._port, | |
1305 this._path, | |
1306 this._query, | |
1307 this._fragment); | |
1308 | |
1309 /// Create a [_Uri] from parts of [uri]. | |
1310 /// | |
1311 /// The parameters specify the start/end of particular components of the URI. | |
1312 /// The [scheme] may contain a string representing a normalized scheme | |
1313 /// component if one has already been discovered. | |
1314 factory _Uri.notSimple(String uri, int start, int end, int schemeEnd, | |
1315 int hostStart, int portStart, int pathStart, | |
1316 int queryStart, int fragmentStart, String scheme) { | |
1317 if (scheme == null) { | |
1318 scheme = ""; | |
1319 if (schemeEnd > start) { | |
1320 scheme = _makeScheme(uri, start, schemeEnd); | |
1321 } else if (schemeEnd == start) { | |
1322 _fail(uri, start, "Invalid empty scheme"); | |
1323 } | |
1324 } | |
1325 String userInfo = ""; | |
1326 String host; | |
1327 int port; | |
1328 if (hostStart > start) { | |
1329 int userInfoStart = schemeEnd + 3; | |
1330 if (userInfoStart < hostStart) { | |
1331 userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1); | |
1332 } | |
1333 host = _makeHost(uri, hostStart, portStart, false); | |
1334 if (portStart + 1 < pathStart) { | |
1335 // Should throw because invalid. | |
1336 port = int.parse(uri.substring(portStart + 1, pathStart), onError: (_) { | |
1337 throw new FormatException("Invalid port", uri, portStart + 1); | |
1338 }); | |
1339 port = _makePort(port, scheme); | |
1340 } | |
1341 } | |
1342 String path = _makePath(uri, pathStart, queryStart, null, | |
1343 scheme, host != null); | |
1344 String query; | |
1345 if (queryStart < fragmentStart) { | |
1346 query = _makeQuery(uri, queryStart + 1, fragmentStart, null); | |
1347 } | |
1348 String fragment; | |
1349 if (fragmentStart < end) { | |
1350 fragment = _makeFragment(uri, fragmentStart + 1, end); | |
1351 } | |
1352 return new _Uri._internal(scheme, | |
1353 userInfo, | |
1354 host, | |
1355 port, | |
1356 path, | |
1357 query, | |
1358 fragment); | |
1359 } | |
1360 | |
1361 /// Implementation of [Uri.Uri]. | |
1362 factory _Uri({String scheme, | |
1363 String userInfo, | |
1364 String host, | |
1365 int port, | |
1366 String path, | |
1367 Iterable<String> pathSegments, | |
1368 String query, | |
1369 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | |
1370 String fragment}) { | |
1371 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | |
1372 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
1373 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
1374 // Special case this constructor for backwards compatibility. | |
1375 if (query == "") query = null; | |
1376 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
1377 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
1378 port = _makePort(port, scheme); | |
1379 bool isFile = (scheme == "file"); | |
1380 if (host == null && | |
1381 (userInfo.isNotEmpty || port != null || isFile)) { | |
1382 host = ""; | |
1383 } | |
568 bool hasAuthority = (host != null); | 1384 bool hasAuthority = (host != null); |
569 path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); | 1385 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
570 | 1386 scheme, hasAuthority); |
571 if (char == _QUESTION) { | 1387 if (scheme.isEmpty && host == null && !path.startsWith('/')) { |
572 int numberSignIndex = -1; | 1388 path = _normalizeRelativePath(path); |
573 for (int i = index + 1; i < end; i++) { | 1389 } else { |
574 if (uri.codeUnitAt(i) == _NUMBER_SIGN) { | 1390 path = _removeDotSegments(path); |
575 numberSignIndex = i; | 1391 } |
576 break; | 1392 return new _Uri._internal(scheme, userInfo, host, port, |
577 } | 1393 path, query, fragment); |
578 } | 1394 } |
579 if (numberSignIndex < 0) { | 1395 |
580 query = _makeQuery(uri, index + 1, end, null); | 1396 /// Implementation of [Uri.http]. |
581 } else { | 1397 factory _Uri.http(String authority, |
582 query = _makeQuery(uri, index + 1, numberSignIndex, null); | 1398 String unencodedPath, |
583 fragment = _makeFragment(uri, numberSignIndex + 1, end); | 1399 [Map<String, String> queryParameters]) { |
584 } | 1400 return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
585 } else if (char == _NUMBER_SIGN) { | 1401 } |
586 fragment = _makeFragment(uri, index + 1, end); | 1402 |
587 } | 1403 /// Implementation of [Uri.https]. |
588 return new Uri._internal(scheme, | 1404 factory _Uri.https(String authority, |
589 userinfo, | 1405 String unencodedPath, |
590 host, | 1406 [Map<String, String> queryParameters]) { |
591 port, | 1407 return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
592 path, | 1408 } |
593 query, | 1409 |
594 fragment); | 1410 String get authority { |
595 } | 1411 if (!hasAuthority) return ""; |
1412 var sb = new StringBuffer(); | |
1413 _writeAuthority(sb); | |
1414 return sb.toString(); | |
1415 } | |
1416 | |
1417 String get userInfo => _userInfo; | |
1418 | |
1419 String get host { | |
1420 if (_host == null) return ""; | |
1421 if (_host.startsWith('[')) { | |
1422 return _host.substring(1, _host.length - 1); | |
1423 } | |
1424 return _host; | |
1425 } | |
1426 | |
1427 int get port { | |
1428 if (_port == null) return _defaultPort(scheme); | |
1429 return _port; | |
1430 } | |
1431 | |
1432 // The default port for the scheme of this Uri.. | |
floitsch
2016/06/29 23:41:47
Remove trailing ".".
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
| |
1433 static int _defaultPort(String scheme) { | |
1434 if (scheme == "http") return 80; | |
1435 if (scheme == "https") return 443; | |
1436 return 0; | |
1437 } | |
1438 | |
1439 String get path => _path; | |
1440 | |
1441 String get query => (_query == null) ? "" : _query; | |
floitsch
2016/06/29 23:41:47
_query ?? ""
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
| |
1442 | |
1443 String get fragment => (_fragment == null) ? "" : _fragment; | |
floitsch
2016/06/29 23:41:47
_fragment ?? ""
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
| |
596 | 1444 |
597 // Report a parse failure. | 1445 // Report a parse failure. |
598 static void _fail(String uri, int index, String message) { | 1446 static void _fail(String uri, int index, String message) { |
599 throw new FormatException(message, uri, index); | 1447 throw new FormatException(message, uri, index); |
600 } | 1448 } |
601 | 1449 |
602 static Uri _makeHttpUri(String scheme, | 1450 static Uri _makeHttpUri(String scheme, |
603 String authority, | 1451 String authority, |
604 String unencodedPath, | 1452 String unencodedPath, |
605 Map<String, String> queryParameters) { | 1453 Map<String, String> queryParameters) { |
606 var userInfo = ""; | 1454 var userInfo = ""; |
607 var host = null; | 1455 var host = null; |
608 var port = null; | 1456 var port = null; |
609 | 1457 |
610 if (authority != null && authority.isNotEmpty) { | 1458 if (authority != null && authority.isNotEmpty) { |
611 var hostStart = 0; | 1459 var hostStart = 0; |
612 // Split off the user info. | 1460 // Split off the user info. |
613 bool hasUserInfo = false; | 1461 bool hasUserInfo = false; |
614 for (int i = 0; i < authority.length; i++) { | 1462 for (int i = 0; i < authority.length; i++) { |
615 if (authority.codeUnitAt(i) == _AT_SIGN) { | 1463 const int atSign = 0x40; |
1464 if (authority.codeUnitAt(i) == atSign) { | |
616 hasUserInfo = true; | 1465 hasUserInfo = true; |
617 userInfo = authority.substring(0, i); | 1466 userInfo = authority.substring(0, i); |
618 hostStart = i + 1; | 1467 hostStart = i + 1; |
619 break; | 1468 break; |
620 } | 1469 } |
621 } | 1470 } |
622 var hostEnd = hostStart; | 1471 var hostEnd = hostStart; |
623 if (hostStart < authority.length && | 1472 if (hostStart < authority.length && |
624 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | 1473 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { |
625 // IPv6 host. | 1474 // IPv6 host. |
626 for (; hostEnd < authority.length; hostEnd++) { | 1475 for (; hostEnd < authority.length; hostEnd++) { |
627 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | 1476 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; |
628 } | 1477 } |
629 if (hostEnd == authority.length) { | 1478 if (hostEnd == authority.length) { |
630 throw new FormatException("Invalid IPv6 host entry.", | 1479 throw new FormatException("Invalid IPv6 host entry.", |
631 authority, hostStart); | 1480 authority, hostStart); |
632 } | 1481 } |
633 parseIPv6Address(authority, hostStart + 1, hostEnd); | 1482 Uri.parseIPv6Address(authority, hostStart + 1, hostEnd); |
634 hostEnd++; // Skip the closing bracket. | 1483 hostEnd++; // Skip the closing bracket. |
635 if (hostEnd != authority.length && | 1484 if (hostEnd != authority.length && |
636 authority.codeUnitAt(hostEnd) != _COLON) { | 1485 authority.codeUnitAt(hostEnd) != _COLON) { |
637 throw new FormatException("Invalid end of authority", | 1486 throw new FormatException("Invalid end of authority", |
638 authority, hostEnd); | 1487 authority, hostEnd); |
639 } | 1488 } |
640 } | 1489 } |
641 // Split host and port. | 1490 // Split host and port. |
642 bool hasPort = false; | 1491 bool hasPort = false; |
643 for (; hostEnd < authority.length; hostEnd++) { | 1492 for (; hostEnd < authority.length; hostEnd++) { |
644 if (authority.codeUnitAt(hostEnd) == _COLON) { | 1493 if (authority.codeUnitAt(hostEnd) == _COLON) { |
645 var portString = authority.substring(hostEnd + 1); | 1494 var portString = authority.substring(hostEnd + 1); |
646 // We allow the empty port - falling back to initial value. | 1495 // We allow the empty port - falling back to initial value. |
647 if (portString.isNotEmpty) port = int.parse(portString); | 1496 if (portString.isNotEmpty) port = int.parse(portString); |
648 break; | 1497 break; |
649 } | 1498 } |
650 } | 1499 } |
651 host = authority.substring(hostStart, hostEnd); | 1500 host = authority.substring(hostStart, hostEnd); |
652 } | 1501 } |
653 return new Uri(scheme: scheme, | 1502 return new Uri(scheme: scheme, |
654 userInfo: userInfo, | 1503 userInfo: userInfo, |
655 host: host, | 1504 host: host, |
656 port: port, | 1505 port: port, |
657 pathSegments: unencodedPath.split("/"), | 1506 pathSegments: unencodedPath.split("/"), |
658 queryParameters: queryParameters); | 1507 queryParameters: queryParameters); |
659 } | 1508 } |
660 | 1509 |
661 /** | 1510 /// Implementation of [Uri.file]. |
662 * Creates a new file URI from an absolute or relative file path. | 1511 factory _Uri.file(String path, {bool windows}) { |
663 * | 1512 windows = (windows == null) ? _Uri._isWindows : windows; |
664 * The file path is passed in [path]. | |
665 * | |
666 * This path is interpreted using either Windows or non-Windows | |
667 * semantics. | |
668 * | |
669 * With non-Windows semantics the slash ("/") is used to separate | |
670 * path segments. | |
671 * | |
672 * With Windows semantics, backslash ("\") and forward-slash ("/") | |
673 * are used to separate path segments, except if the path starts | |
674 * with "\\?\" in which case, only backslash ("\") separates path | |
675 * segments. | |
676 * | |
677 * If the path starts with a path separator an absolute URI is | |
678 * created. Otherwise a relative URI is created. One exception from | |
679 * this rule is that when Windows semantics is used and the path | |
680 * starts with a drive letter followed by a colon (":") and a | |
681 * path separator then an absolute URI is created. | |
682 * | |
683 * The default for whether to use Windows or non-Windows semantics | |
684 * determined from the platform Dart is running on. When running in | |
685 * the standalone VM this is detected by the VM based on the | |
686 * operating system. When running in a browser non-Windows semantics | |
687 * is always used. | |
688 * | |
689 * To override the automatic detection of which semantics to use pass | |
690 * a value for [windows]. Passing `true` will use Windows | |
691 * semantics and passing `false` will use non-Windows semantics. | |
692 * | |
693 * Examples using non-Windows semantics: | |
694 * | |
695 * ``` | |
696 * // xxx/yyy | |
697 * new Uri.file("xxx/yyy", windows: false); | |
698 * | |
699 * // xxx/yyy/ | |
700 * new Uri.file("xxx/yyy/", windows: false); | |
701 * | |
702 * // file:///xxx/yyy | |
703 * new Uri.file("/xxx/yyy", windows: false); | |
704 * | |
705 * // file:///xxx/yyy/ | |
706 * new Uri.file("/xxx/yyy/", windows: false); | |
707 * | |
708 * // C: | |
709 * new Uri.file("C:", windows: false); | |
710 * ``` | |
711 * | |
712 * Examples using Windows semantics: | |
713 * | |
714 * ``` | |
715 * // xxx/yyy | |
716 * new Uri.file(r"xxx\yyy", windows: true); | |
717 * | |
718 * // xxx/yyy/ | |
719 * new Uri.file(r"xxx\yyy\", windows: true); | |
720 * | |
721 * file:///xxx/yyy | |
722 * new Uri.file(r"\xxx\yyy", windows: true); | |
723 * | |
724 * file:///xxx/yyy/ | |
725 * new Uri.file(r"\xxx\yyy/", windows: true); | |
726 * | |
727 * // file:///C:/xxx/yyy | |
728 * new Uri.file(r"C:\xxx\yyy", windows: true); | |
729 * | |
730 * // This throws an error. A path with a drive letter is not absolute. | |
731 * new Uri.file(r"C:", windows: true); | |
732 * | |
733 * // This throws an error. A path with a drive letter is not absolute. | |
734 * new Uri.file(r"C:xxx\yyy", windows: true); | |
735 * | |
736 * // file://server/share/file | |
737 * new Uri.file(r"\\server\share\file", windows: true); | |
738 * ``` | |
739 * | |
740 * If the path passed is not a legal file path [ArgumentError] is thrown. | |
741 */ | |
742 factory Uri.file(String path, {bool windows}) { | |
743 windows = (windows == null) ? Uri._isWindows : windows; | |
744 return windows ? _makeWindowsFileUrl(path, false) | 1513 return windows ? _makeWindowsFileUrl(path, false) |
745 : _makeFileUri(path, false); | 1514 : _makeFileUri(path, false); |
746 } | 1515 } |
747 | 1516 |
748 /** | 1517 /// Implementation of [Uri.directory]. |
749 * Like [Uri.file] except that a non-empty URI path ends in a slash. | 1518 factory _Uri.directory(String path, {bool windows}) { |
750 * | 1519 windows = (windows == null) ? _Uri._isWindows : windows; |
751 * If [path] is not empty, and it doesn't end in a directory separator, | |
752 * then a slash is added to the returned URI's path. | |
753 * In all other cases, the result is the same as returned by `Uri.file`. | |
754 */ | |
755 factory Uri.directory(String path, {bool windows}) { | |
756 windows = (windows == null) ? Uri._isWindows : windows; | |
757 return windows ? _makeWindowsFileUrl(path, true) | 1520 return windows ? _makeWindowsFileUrl(path, true) |
758 : _makeFileUri(path, true); | 1521 : _makeFileUri(path, true); |
759 } | 1522 } |
760 | 1523 |
761 /** | |
762 * Creates a `data:` URI containing the [content] string. | |
763 * | |
764 * Converts the content to a bytes using [encoding] or the charset specified | |
765 * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), | |
766 * then encodes the bytes into the resulting data URI. | |
767 * | |
768 * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid | |
769 * bytes is replaced by a percent encoding). If [base64] is true, the bytes | |
770 * are instead encoded using [BASE64]. | |
771 * | |
772 * If [encoding] is not provided and [parameters] has a `charset` entry, | |
773 * that name is looked up using [Encoding.getByName], | |
774 * and if the lookup returns an encoding, that encoding is used to convert | |
775 * [content] to bytes. | |
776 * If providing both an [encoding] and a charset [parameter], they should | |
777 * agree, otherwise decoding won't be able to use the charset parameter | |
778 * to determine the encoding. | |
779 * | |
780 * If [mimeType] and/or [parameters] are supplied, they are added to the | |
781 * created URI. If any of these contain characters that are not allowed | |
782 * in the data URI, the character is percent-escaped. If the character is | |
783 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
784 * encoded. An omitted [mimeType] in a data URI means `text/plain`, just | |
785 * as an omitted `charset` parameter defaults to meaning `US-ASCII`. | |
786 * | |
787 * To read the content back, use [UriData.contentAsString]. | |
788 */ | |
789 factory Uri.dataFromString(String content, | |
790 {String mimeType, | |
791 Encoding encoding, | |
792 Map<String, String> parameters, | |
793 bool base64: false}) { | |
794 UriData data = new UriData.fromString(content, | |
795 mimeType: mimeType, | |
796 encoding: encoding, | |
797 parameters: parameters, | |
798 base64: base64); | |
799 return data.uri; | |
800 } | |
801 | 1524 |
802 /** | 1525 /// Used internally in path-related constructors. |
803 * Creates a `data:` URI containing an encoding of [bytes]. | |
804 * | |
805 * Defaults to Base64 encoding the bytes, but if [percentEncoded] | |
806 * is `true`, the bytes will instead be percent encoded (any non-ASCII | |
807 * or non-valid-ASCII-character byte is replaced by a percent encoding). | |
808 * | |
809 * To read the bytes back, use [UriData.contentAsBytes]. | |
810 * | |
811 * It defaults to having the mime-type `application/octet-stream`. | |
812 * The [mimeType] and [parameters] are added to the created URI. | |
813 * If any of these contain characters that are not allowed | |
814 * in the data URI, the character is percent-escaped. If the character is | |
815 * non-ASCII, it is first UTF-8 encoded and then the bytes are percent | |
816 * encoded. | |
817 */ | |
818 factory Uri.dataFromBytes(List<int> bytes, | |
819 {mimeType: "application/octet-stream", | |
820 Map<String, String> parameters, | |
821 percentEncoded: false}) { | |
822 UriData data = new UriData.fromBytes(bytes, | |
823 mimeType: mimeType, | |
824 parameters: parameters, | |
825 percentEncoded: percentEncoded); | |
826 return data.uri; | |
827 } | |
828 | |
829 /** | |
830 * Returns the natural base URI for the current platform. | |
831 * | |
832 * When running in a browser this is the current URL (from | |
833 * `window.location.href`). | |
834 * | |
835 * When not running in a browser this is the file URI referencing | |
836 * the current working directory. | |
837 */ | |
838 external static Uri get base; | |
839 | |
840 external static bool get _isWindows; | 1526 external static bool get _isWindows; |
841 | 1527 |
842 static _checkNonWindowsPathReservedCharacters(List<String> segments, | 1528 static _checkNonWindowsPathReservedCharacters(List<String> segments, |
843 bool argumentError) { | 1529 bool argumentError) { |
844 segments.forEach((segment) { | 1530 segments.forEach((segment) { |
845 if (segment.contains("/")) { | 1531 if (segment.contains("/")) { |
846 if (argumentError) { | 1532 if (argumentError) { |
847 throw new ArgumentError("Illegal path character $segment"); | 1533 throw new ArgumentError("Illegal path character $segment"); |
848 } else { | 1534 } else { |
849 throw new UnsupportedError("Illegal path character $segment"); | 1535 throw new UnsupportedError("Illegal path character $segment"); |
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
962 _checkWindowsPathReservedCharacters(pathSegments, true); | 1648 _checkWindowsPathReservedCharacters(pathSegments, true); |
963 if (slashTerminated && | 1649 if (slashTerminated && |
964 pathSegments.isNotEmpty && | 1650 pathSegments.isNotEmpty && |
965 pathSegments.last.isNotEmpty) { | 1651 pathSegments.last.isNotEmpty) { |
966 pathSegments.add(""); // Extra separator at end. | 1652 pathSegments.add(""); // Extra separator at end. |
967 } | 1653 } |
968 return new Uri(pathSegments: pathSegments); | 1654 return new Uri(pathSegments: pathSegments); |
969 } | 1655 } |
970 } | 1656 } |
971 | 1657 |
972 /** | |
973 * Returns a new `Uri` based on this one, but with some parts replaced. | |
974 * | |
975 * This method takes the same parameters as the [new Uri] constructor, | |
976 * and they have the same meaning. | |
977 * | |
978 * At most one of [path] and [pathSegments] must be provided. | |
979 * Likewise, at most one of [query] and [queryParameters] must be provided. | |
980 * | |
981 * Each part that is not provided will default to the corresponding | |
982 * value from this `Uri` instead. | |
983 * | |
984 * This method is different from [Uri.resolve] which overrides in a | |
985 * hierarchial manner, | |
986 * and can instead replace each part of a `Uri` individually. | |
987 * | |
988 * Example: | |
989 * | |
990 * Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); | |
991 * Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); | |
992 * print(uri2); // prints "A://b@c:4/D/E/E/?f#G" | |
993 * | |
994 * This method acts similarly to using the `new Uri` constructor with | |
995 * some of the arguments taken from this `Uri` . Example: | |
996 * | |
997 * Uri uri3 = new Uri( | |
998 * scheme: "A", | |
999 * userInfo: uri1.userInfo, | |
1000 * host: uri1.host, | |
1001 * port: uri1.port, | |
1002 * path: "D/E/E", | |
1003 * query: uri1.query, | |
1004 * fragment: "G"); | |
1005 * print(uri3); // prints "A://b@c:4/D/E/E/?f#G" | |
1006 * print(uri2 == uri3); // prints true. | |
1007 * | |
1008 * Using this method can be seen as a shorthand for the `Uri` constructor | |
1009 * call above, but may also be slightly faster because the parts taken | |
1010 * from this `Uri` need not be checked for validity again. | |
1011 */ | |
1012 Uri replace({String scheme, | 1658 Uri replace({String scheme, |
1013 String userInfo, | 1659 String userInfo, |
1014 String host, | 1660 String host, |
1015 int port, | 1661 int port, |
1016 String path, | 1662 String path, |
1017 Iterable<String> pathSegments, | 1663 Iterable<String> pathSegments, |
1018 String query, | 1664 String query, |
1019 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 1665 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
1020 String fragment}) { | 1666 String fragment}) { |
1021 // Set to true if the scheme has (potentially) changed. | 1667 // Set to true if the scheme has (potentially) changed. |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1068 } else { | 1714 } else { |
1069 query = this._query; | 1715 query = this._query; |
1070 } | 1716 } |
1071 | 1717 |
1072 if (fragment != null) { | 1718 if (fragment != null) { |
1073 fragment = _makeFragment(fragment, 0, fragment.length); | 1719 fragment = _makeFragment(fragment, 0, fragment.length); |
1074 } else { | 1720 } else { |
1075 fragment = this._fragment; | 1721 fragment = this._fragment; |
1076 } | 1722 } |
1077 | 1723 |
1078 return new Uri._internal( | 1724 return new _Uri._internal( |
1079 scheme, userInfo, host, port, path, query, fragment); | 1725 scheme, userInfo, host, port, path, query, fragment); |
1080 } | 1726 } |
1081 | 1727 |
1082 /** | |
1083 * Returns a `Uri` that differs from this only in not having a fragment. | |
1084 * | |
1085 * If this `Uri` does not have a fragment, it is itself returned. | |
1086 */ | |
1087 Uri removeFragment() { | 1728 Uri removeFragment() { |
1088 if (!this.hasFragment) return this; | 1729 if (!this.hasFragment) return this; |
1089 return new Uri._internal(scheme, _userInfo, _host, _port, | 1730 return new _Uri._internal(scheme, _userInfo, _host, _port, |
1090 _path, _query, null); | 1731 _path, _query, null); |
1091 } | 1732 } |
1092 | 1733 |
1093 /** | |
1094 * Returns the URI path split into its segments. Each of the segments in the | |
1095 * returned list have been decoded. If the path is empty the empty list will | |
1096 * be returned. A leading slash `/` does not affect the segments returned. | |
1097 * | |
1098 * The returned list is unmodifiable and will throw [UnsupportedError] on any | |
1099 * calls that would mutate it. | |
1100 */ | |
1101 List<String> get pathSegments { | 1734 List<String> get pathSegments { |
1102 var result = _pathSegments; | 1735 var result = _pathSegments; |
1103 if (result != null) return result; | 1736 if (result != null) return result; |
1104 | 1737 |
1105 var pathToSplit = path; | 1738 var pathToSplit = path; |
1106 if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { | 1739 if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { |
1107 pathToSplit = pathToSplit.substring(1); | 1740 pathToSplit = pathToSplit.substring(1); |
1108 } | 1741 } |
1109 result = (pathToSplit == "") | 1742 result = (pathToSplit == "") |
1110 ? const<String>[] | 1743 ? const<String>[] |
1111 : new List<String>.unmodifiable( | 1744 : new List<String>.unmodifiable( |
1112 pathToSplit.split("/").map(Uri.decodeComponent)); | 1745 pathToSplit.split("/").map(Uri.decodeComponent)); |
1113 _pathSegments = result; | 1746 _pathSegments = result; |
1114 return result; | 1747 return result; |
1115 } | 1748 } |
1116 | 1749 |
1117 /** | |
1118 * Returns the URI query split into a map according to the rules | |
1119 * specified for FORM post in the [HTML 4.01 specification section | |
1120 * 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"). | |
1121 * Each key and value in the returned map has been decoded. | |
1122 * If there is no query the empty map is returned. | |
1123 * | |
1124 * Keys in the query string that have no value are mapped to the | |
1125 * empty string. | |
1126 * If a key occurs more than once in the query string, it is mapped to | |
1127 * an arbitrary choice of possible value. | |
1128 * The [queryParametersAll] getter can provide a map | |
1129 * that maps keys to all of their values. | |
1130 * | |
1131 * The returned map is unmodifiable. | |
1132 */ | |
1133 Map<String, String> get queryParameters { | 1750 Map<String, String> get queryParameters { |
1134 if (_queryParameters == null) { | 1751 if (_queryParameters == null) { |
1135 _queryParameters = | 1752 _queryParameters = |
1136 new UnmodifiableMapView<String, String>(splitQueryString(query)); | 1753 new UnmodifiableMapView<String, String>(Uri.splitQueryString(query)); |
1137 } | 1754 } |
1138 return _queryParameters; | 1755 return _queryParameters; |
1139 } | 1756 } |
1140 | 1757 |
1141 /** | |
1142 * Returns the URI query split into a map according to the rules | |
1143 * specified for FORM post in the [HTML 4.01 specification section | |
1144 * 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"). | |
1145 * Each key and value in the returned map has been decoded. If there is no | |
1146 * query the empty map is returned. | |
1147 * | |
1148 * Keys are mapped to lists of their values. If a key occurs only once, | |
1149 * its value is a singleton list. If a key occurs with no value, the | |
1150 * empty string is used as the value for that occurrence. | |
1151 * | |
1152 * The returned map and the lists it contains are unmodifiable. | |
1153 */ | |
1154 Map<String, List<String>> get queryParametersAll { | 1758 Map<String, List<String>> get queryParametersAll { |
1155 if (_queryParameterLists == null) { | 1759 if (_queryParameterLists == null) { |
1156 Map queryParameterLists = _splitQueryStringAll(query); | 1760 Map queryParameterLists = _splitQueryStringAll(query); |
1157 for (var key in queryParameterLists.keys) { | 1761 for (var key in queryParameterLists.keys) { |
1158 queryParameterLists[key] = | 1762 queryParameterLists[key] = |
1159 new List<String>.unmodifiable(queryParameterLists[key]); | 1763 new List<String>.unmodifiable(queryParameterLists[key]); |
1160 } | 1764 } |
1161 _queryParameterLists = | 1765 _queryParameterLists = |
1162 new Map<String, List<String>>.unmodifiable(queryParameterLists); | 1766 new Map<String, List<String>>.unmodifiable(queryParameterLists); |
1163 } | 1767 } |
1164 return _queryParameterLists; | 1768 return _queryParameterLists; |
1165 } | 1769 } |
1166 | 1770 |
1167 /** | |
1168 * Returns a URI where the path has been normalized. | |
1169 * | |
1170 * A normalized path does not contain `.` segments or non-leading `..` | |
1171 * segments. | |
1172 * Only a relative path with no scheme or authority may contain | |
1173 * leading `..` segments, | |
1174 * a path that starts with `/` will also drop any leading `..` segments. | |
1175 * | |
1176 * This uses the same normalization strategy as `new Uri().resolve(this)`. | |
1177 * | |
1178 * Does not change any part of the URI except the path. | |
1179 * | |
1180 * The default implementation of `Uri` always normalizes paths, so calling | |
1181 * this function has no effect. | |
1182 */ | |
1183 Uri normalizePath() { | 1771 Uri normalizePath() { |
1184 String path = _normalizePath(_path, scheme, hasAuthority); | 1772 String path = _normalizePath(_path, scheme, hasAuthority); |
1185 if (identical(path, _path)) return this; | 1773 if (identical(path, _path)) return this; |
1186 return this.replace(path: path); | 1774 return this.replace(path: path); |
1187 } | 1775 } |
1188 | 1776 |
1189 static int _makePort(int port, String scheme) { | 1777 static int _makePort(int port, String scheme) { |
1190 // Perform scheme specific normalization. | 1778 // Perform scheme specific normalization. |
1191 if (port != null && port == _defaultPort(scheme)) return null; | 1779 if (port != null && port == _defaultPort(scheme)) return null; |
1192 return port; | 1780 return port; |
(...skipping 12 matching lines...) Expand all Loading... | |
1205 */ | 1793 */ |
1206 static String _makeHost(String host, int start, int end, bool strictIPv6) { | 1794 static String _makeHost(String host, int start, int end, bool strictIPv6) { |
1207 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? | 1795 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? |
1208 if (host == null) return null; | 1796 if (host == null) return null; |
1209 if (start == end) return ""; | 1797 if (start == end) return ""; |
1210 // Host is an IPv6 address if it starts with '[' or contains a colon. | 1798 // Host is an IPv6 address if it starts with '[' or contains a colon. |
1211 if (host.codeUnitAt(start) == _LEFT_BRACKET) { | 1799 if (host.codeUnitAt(start) == _LEFT_BRACKET) { |
1212 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { | 1800 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { |
1213 _fail(host, start, 'Missing end `]` to match `[` in host'); | 1801 _fail(host, start, 'Missing end `]` to match `[` in host'); |
1214 } | 1802 } |
1215 parseIPv6Address(host, start + 1, end - 1); | 1803 Uri.parseIPv6Address(host, start + 1, end - 1); |
1216 // RFC 5952 requires hex digits to be lower case. | 1804 // RFC 5952 requires hex digits to be lower case. |
1217 return host.substring(start, end).toLowerCase(); | 1805 return host.substring(start, end).toLowerCase(); |
1218 } | 1806 } |
1219 if (!strictIPv6) { | 1807 if (!strictIPv6) { |
1220 // TODO(lrn): skip if too short to be a valid IPv6 address? | 1808 // TODO(lrn): skip if too short to be a valid IPv6 address? |
1221 for (int i = start; i < end; i++) { | 1809 for (int i = start; i < end; i++) { |
1222 if (host.codeUnitAt(i) == _COLON) { | 1810 if (host.codeUnitAt(i) == _COLON) { |
1223 parseIPv6Address(host, start, end); | 1811 Uri.parseIPv6Address(host, start, end); |
1224 return '[$host]'; | 1812 return '[$host]'; |
1225 } | 1813 } |
1226 } | 1814 } |
1227 } | 1815 } |
1228 return _normalizeRegName(host, start, end); | 1816 return _normalizeRegName(host, start, end); |
1229 } | 1817 } |
1230 | 1818 |
1231 static bool _isRegNameChar(int char) { | 1819 static bool _isRegNameChar(int char) { |
1232 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | 1820 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; |
1233 } | 1821 } |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1326 final int codeUnit = scheme.codeUnitAt(i); | 1914 final int codeUnit = scheme.codeUnitAt(i); |
1327 if (!_isSchemeCharacter(codeUnit)) { | 1915 if (!_isSchemeCharacter(codeUnit)) { |
1328 _fail(scheme, i, "Illegal scheme character"); | 1916 _fail(scheme, i, "Illegal scheme character"); |
1329 } | 1917 } |
1330 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { | 1918 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { |
1331 containsUpperCase = true; | 1919 containsUpperCase = true; |
1332 } | 1920 } |
1333 } | 1921 } |
1334 scheme = scheme.substring(start, end); | 1922 scheme = scheme.substring(start, end); |
1335 if (containsUpperCase) scheme = scheme.toLowerCase(); | 1923 if (containsUpperCase) scheme = scheme.toLowerCase(); |
1924 return _canonicalizeScheme(scheme); | |
1925 } | |
1926 | |
1927 // Canonicalize a few often-used scheme strings. | |
1928 // | |
1929 // This improves memory usage and makes comparison faster. | |
1930 static String _canonicalizeScheme(String scheme) { | |
1931 if (scheme == "http") return "http"; | |
1932 if (scheme == "file") return "file"; | |
1933 if (scheme == "https") return "https"; | |
1934 if (scheme == "package") return "package"; | |
1336 return scheme; | 1935 return scheme; |
1337 } | 1936 } |
1338 | 1937 |
1339 static String _makeUserInfo(String userInfo, int start, int end) { | 1938 static String _makeUserInfo(String userInfo, int start, int end) { |
1340 if (userInfo == null) return ""; | 1939 if (userInfo == null) return ""; |
1341 return _normalize(userInfo, start, end, _userinfoTable); | 1940 return _normalize(userInfo, start, end, _userinfoTable); |
1342 } | 1941 } |
1343 | 1942 |
1344 static String _makePath(String path, int start, int end, | 1943 static String _makePath(String path, int start, int end, |
1345 Iterable<String> pathSegments, | 1944 Iterable<String> pathSegments, |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1412 } | 2011 } |
1413 }); | 2012 }); |
1414 return result.toString(); | 2013 return result.toString(); |
1415 } | 2014 } |
1416 | 2015 |
1417 static String _makeFragment(String fragment, int start, int end) { | 2016 static String _makeFragment(String fragment, int start, int end) { |
1418 if (fragment == null) return null; | 2017 if (fragment == null) return null; |
1419 return _normalize(fragment, start, end, _queryCharTable); | 2018 return _normalize(fragment, start, end, _queryCharTable); |
1420 } | 2019 } |
1421 | 2020 |
1422 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | |
1423 | |
1424 /** | 2021 /** |
1425 * Performs RFC 3986 Percent-Encoding Normalization. | 2022 * Performs RFC 3986 Percent-Encoding Normalization. |
1426 * | 2023 * |
1427 * Returns a replacement string that should be replace the original escape. | 2024 * Returns a replacement string that should be replace the original escape. |
1428 * Returns null if no replacement is necessary because the escape is | 2025 * Returns null if no replacement is necessary because the escape is |
1429 * not for an unreserved character and is already non-lower-case. | 2026 * not for an unreserved character and is already non-lower-case. |
1430 * | 2027 * |
1431 * Returns "%" if the escape is invalid (not two valid hex digits following | 2028 * Returns "%" if the escape is invalid (not two valid hex digits following |
1432 * the percent sign). The calling code should replace the percent | 2029 * the percent sign). The calling code should replace the percent |
1433 * sign with "%25", but leave the following two characters unmodified. | 2030 * sign with "%25", but leave the following two characters unmodified. |
(...skipping 24 matching lines...) Expand all Loading... | |
1458 return source.substring(index, index + 3).toUpperCase(); | 2055 return source.substring(index, index + 3).toUpperCase(); |
1459 } | 2056 } |
1460 // Escape is retained, and is already non-lower case, so return null to | 2057 // Escape is retained, and is already non-lower case, so return null to |
1461 // represent "no replacement necessary". | 2058 // represent "no replacement necessary". |
1462 return null; | 2059 return null; |
1463 } | 2060 } |
1464 | 2061 |
1465 // Converts a UTF-16 code-unit to its value as a hex digit. | 2062 // Converts a UTF-16 code-unit to its value as a hex digit. |
1466 // Returns -1 for non-hex digits. | 2063 // Returns -1 for non-hex digits. |
1467 static int _parseHexDigit(int char) { | 2064 static int _parseHexDigit(int char) { |
1468 int digit = char ^ Uri._ZERO; | 2065 const int zeroDigit = 0x30; |
2066 int digit = char ^ zeroDigit; | |
1469 if (digit <= 9) return digit; | 2067 if (digit <= 9) return digit; |
1470 int lowerCase = char | 0x20; | 2068 int lowerCase = char | 0x20; |
1471 if (Uri._LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { | 2069 if (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
1472 return lowerCase - (_LOWER_CASE_A - 10); | 2070 return lowerCase - (_LOWER_CASE_A - 10); |
1473 } | 2071 } |
1474 return -1; | 2072 return -1; |
1475 } | 2073 } |
1476 | 2074 |
1477 static String _escapeChar(int char) { | 2075 static String _escapeChar(int char) { |
1478 assert(char <= 0x10ffff); // It's a valid unicode code point. | 2076 assert(char <= 0x10ffff); // It's a valid unicode code point. |
1479 List<int> codeUnits; | 2077 List<int> codeUnits; |
1480 if (char < 0x80) { | 2078 if (char < 0x80) { |
1481 // ASCII, a single percent encoded sequence. | 2079 // ASCII, a single percent encoded sequence. |
(...skipping 214 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1696 output.add(segment); | 2294 output.add(segment); |
1697 } | 2295 } |
1698 } | 2296 } |
1699 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { | 2297 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { |
1700 return "./"; | 2298 return "./"; |
1701 } | 2299 } |
1702 if (appendSlash || output.last == '..') output.add(""); | 2300 if (appendSlash || output.last == '..') output.add(""); |
1703 return output.join("/"); | 2301 return output.join("/"); |
1704 } | 2302 } |
1705 | 2303 |
1706 /** | |
1707 * Resolve [reference] as an URI relative to `this`. | |
1708 * | |
1709 * First turn [reference] into a URI using [Uri.parse]. Then resolve the | |
1710 * resulting URI relative to `this`. | |
1711 * | |
1712 * Returns the resolved URI. | |
1713 * | |
1714 * See [resolveUri] for details. | |
1715 */ | |
1716 Uri resolve(String reference) { | 2304 Uri resolve(String reference) { |
1717 return resolveUri(Uri.parse(reference)); | 2305 return resolveUri(Uri.parse(reference)); |
1718 } | 2306 } |
1719 | 2307 |
1720 /** | |
1721 * Resolve [reference] as an URI relative to `this`. | |
1722 * | |
1723 * Returns the resolved URI. | |
1724 * | |
1725 * The algorithm "Transform Reference" for resolving a reference is described | |
1726 * in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1 123"). | |
1727 * | |
1728 * Updated to handle the case where the base URI is just a relative path - | |
1729 * that is: when it has no scheme or authority and the path does not start | |
1730 * with a slash. | |
1731 * In that case, the paths are combined without removing leading "..", and | |
1732 * an empty path is not converted to "/". | |
1733 */ | |
1734 Uri resolveUri(Uri reference) { | 2308 Uri resolveUri(Uri reference) { |
1735 // From RFC 3986. | 2309 // From RFC 3986. |
1736 String targetScheme; | 2310 String targetScheme; |
1737 String targetUserInfo = ""; | 2311 String targetUserInfo = ""; |
1738 String targetHost; | 2312 String targetHost; |
1739 int targetPort; | 2313 int targetPort; |
1740 String targetPath; | 2314 String targetPath; |
1741 String targetQuery; | 2315 String targetQuery; |
1742 if (reference.scheme.isNotEmpty) { | 2316 if (reference.scheme.isNotEmpty) { |
1743 targetScheme = reference.scheme; | 2317 targetScheme = reference.scheme; |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1781 targetPath = reference.path; | 2355 targetPath = reference.path; |
1782 } else { | 2356 } else { |
1783 // Add path normalization on top of RFC algorithm. | 2357 // Add path normalization on top of RFC algorithm. |
1784 targetPath = _removeDotSegments("/" + reference.path); | 2358 targetPath = _removeDotSegments("/" + reference.path); |
1785 } | 2359 } |
1786 } else { | 2360 } else { |
1787 var mergedPath = _mergePaths(this._path, reference.path); | 2361 var mergedPath = _mergePaths(this._path, reference.path); |
1788 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { | 2362 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { |
1789 targetPath = _removeDotSegments(mergedPath); | 2363 targetPath = _removeDotSegments(mergedPath); |
1790 } else { | 2364 } else { |
1791 // Non-RFC 3986 beavior. If both base and reference are relative | 2365 // Non-RFC 3986 behavior. |
1792 // path, allow the merged path to start with "..". | 2366 // If both base and reference are relative paths, |
2367 // allow the merged path to start with "..". | |
1793 // The RFC only specifies the case where the base has a scheme. | 2368 // The RFC only specifies the case where the base has a scheme. |
1794 targetPath = _normalizeRelativePath(mergedPath); | 2369 targetPath = _normalizeRelativePath(mergedPath); |
1795 } | 2370 } |
1796 } | 2371 } |
1797 } | 2372 } |
1798 if (reference.hasQuery) targetQuery = reference.query; | 2373 if (reference.hasQuery) targetQuery = reference.query; |
1799 } | 2374 } |
1800 } | 2375 } |
1801 } | 2376 } |
1802 String fragment = reference.hasFragment ? reference.fragment : null; | 2377 String fragment = reference.hasFragment ? reference.fragment : null; |
1803 return new Uri._internal(targetScheme, | 2378 return new _Uri._internal(targetScheme, |
1804 targetUserInfo, | 2379 targetUserInfo, |
1805 targetHost, | 2380 targetHost, |
1806 targetPort, | 2381 targetPort, |
1807 targetPath, | 2382 targetPath, |
1808 targetQuery, | 2383 targetQuery, |
1809 fragment); | 2384 fragment); |
1810 } | 2385 } |
1811 | 2386 |
1812 /** | |
1813 * Returns whether the URI has a [scheme] component. | |
1814 */ | |
1815 bool get hasScheme => scheme.isNotEmpty; | 2387 bool get hasScheme => scheme.isNotEmpty; |
1816 | 2388 |
1817 /** | |
1818 * Returns whether the URI has an [authority] component. | |
1819 */ | |
1820 bool get hasAuthority => _host != null; | 2389 bool get hasAuthority => _host != null; |
1821 | 2390 |
1822 /** | |
1823 * Returns whether the URI has an explicit port. | |
1824 * | |
1825 * If the port number is the default port number | |
1826 * (zero for unrecognized schemes, with http (80) and https (443) being | |
1827 * recognized), | |
1828 * then the port is made implicit and omitted from the URI. | |
1829 */ | |
1830 bool get hasPort => _port != null; | 2391 bool get hasPort => _port != null; |
1831 | 2392 |
1832 /** | |
1833 * Returns whether the URI has a query part. | |
1834 */ | |
1835 bool get hasQuery => _query != null; | 2393 bool get hasQuery => _query != null; |
1836 | 2394 |
1837 /** | |
1838 * Returns whether the URI has a fragment part. | |
1839 */ | |
1840 bool get hasFragment => _fragment != null; | 2395 bool get hasFragment => _fragment != null; |
1841 | 2396 |
1842 /** | |
1843 * Returns whether the URI has an empty path. | |
1844 */ | |
1845 bool get hasEmptyPath => _path.isEmpty; | 2397 bool get hasEmptyPath => _path.isEmpty; |
1846 | 2398 |
1847 /** | |
1848 * Returns whether the URI has an absolute path (starting with '/'). | |
1849 */ | |
1850 bool get hasAbsolutePath => _path.startsWith('/'); | 2399 bool get hasAbsolutePath => _path.startsWith('/'); |
1851 | 2400 |
1852 /** | |
1853 * Returns the origin of the URI in the form scheme://host:port for the | |
1854 * schemes http and https. | |
1855 * | |
1856 * It is an error if the scheme is not "http" or "https". | |
1857 * | |
1858 * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | |
1859 */ | |
1860 String get origin { | 2401 String get origin { |
1861 if (scheme == "" || _host == null || _host == "") { | 2402 if (scheme == "" || _host == null || _host == "") { |
1862 throw new StateError("Cannot use origin without a scheme: $this"); | 2403 throw new StateError("Cannot use origin without a scheme: $this"); |
1863 } | 2404 } |
1864 if (scheme != "http" && scheme != "https") { | 2405 if (scheme != "http" && scheme != "https") { |
1865 throw new StateError( | 2406 throw new StateError( |
1866 "Origin is only applicable schemes http and https: $this"); | 2407 "Origin is only applicable schemes http and https: $this"); |
1867 } | 2408 } |
1868 if (_port == null) return "$scheme://$_host"; | 2409 if (_port == null) return "$scheme://$_host"; |
1869 return "$scheme://$_host:$_port"; | 2410 return "$scheme://$_host:$_port"; |
1870 } | 2411 } |
1871 | 2412 |
1872 /** | |
1873 * Returns the file path from a file URI. | |
1874 * | |
1875 * The returned path has either Windows or non-Windows | |
1876 * semantics. | |
1877 * | |
1878 * For non-Windows semantics the slash ("/") is used to separate | |
1879 * path segments. | |
1880 * | |
1881 * For Windows semantics the backslash ("\") separator is used to | |
1882 * separate path segments. | |
1883 * | |
1884 * If the URI is absolute the path starts with a path separator | |
1885 * unless Windows semantics is used and the first path segment is a | |
1886 * drive letter. When Windows semantics is used a host component in | |
1887 * the uri in interpreted as a file server and a UNC path is | |
1888 * returned. | |
1889 * | |
1890 * The default for whether to use Windows or non-Windows semantics | |
1891 * determined from the platform Dart is running on. When running in | |
1892 * the standalone VM this is detected by the VM based on the | |
1893 * operating system. When running in a browser non-Windows semantics | |
1894 * is always used. | |
1895 * | |
1896 * To override the automatic detection of which semantics to use pass | |
1897 * a value for [windows]. Passing `true` will use Windows | |
1898 * semantics and passing `false` will use non-Windows semantics. | |
1899 * | |
1900 * If the URI ends with a slash (i.e. the last path component is | |
1901 * empty) the returned file path will also end with a slash. | |
1902 * | |
1903 * With Windows semantics URIs starting with a drive letter cannot | |
1904 * be relative to the current drive on the designated drive. That is | |
1905 * for the URI `file:///c:abc` calling `toFilePath` will throw as a | |
1906 * path segment cannot contain colon on Windows. | |
1907 * | |
1908 * Examples using non-Windows semantics (resulting of calling | |
1909 * toFilePath in comment): | |
1910 * | |
1911 * Uri.parse("xxx/yyy"); // xxx/yyy | |
1912 * Uri.parse("xxx/yyy/"); // xxx/yyy/ | |
1913 * Uri.parse("file:///xxx/yyy"); // /xxx/yyy | |
1914 * Uri.parse("file:///xxx/yyy/"); // /xxx/yyy/ | |
1915 * Uri.parse("file:///C:"); // /C: | |
1916 * Uri.parse("file:///C:a"); // /C:a | |
1917 * | |
1918 * Examples using Windows semantics (resulting URI in comment): | |
1919 * | |
1920 * Uri.parse("xxx/yyy"); // xxx\yyy | |
1921 * Uri.parse("xxx/yyy/"); // xxx\yyy\ | |
1922 * Uri.parse("file:///xxx/yyy"); // \xxx\yyy | |
1923 * Uri.parse("file:///xxx/yyy/"); // \xxx\yyy/ | |
1924 * Uri.parse("file:///C:/xxx/yyy"); // C:\xxx\yyy | |
1925 * Uri.parse("file:C:xxx/yyy"); // Throws as a path segment | |
1926 * // cannot contain colon on Windows. | |
1927 * Uri.parse("file://server/share/file"); // \\server\share\file | |
1928 * | |
1929 * If the URI is not a file URI calling this throws | |
1930 * [UnsupportedError]. | |
1931 * | |
1932 * If the URI cannot be converted to a file path calling this throws | |
1933 * [UnsupportedError]. | |
1934 */ | |
1935 String toFilePath({bool windows}) { | 2413 String toFilePath({bool windows}) { |
1936 if (scheme != "" && scheme != "file") { | 2414 if (scheme != "" && scheme != "file") { |
1937 throw new UnsupportedError( | 2415 throw new UnsupportedError( |
1938 "Cannot extract a file path from a $scheme URI"); | 2416 "Cannot extract a file path from a $scheme URI"); |
1939 } | 2417 } |
1940 if (query != "") { | 2418 if (query != "") { |
1941 throw new UnsupportedError( | 2419 throw new UnsupportedError( |
1942 "Cannot extract a file path from a URI with a query component"); | 2420 "Cannot extract a file path from a URI with a query component"); |
1943 } | 2421 } |
1944 if (fragment != "") { | 2422 if (fragment != "") { |
1945 throw new UnsupportedError( | 2423 throw new UnsupportedError( |
1946 "Cannot extract a file path from a URI with a fragment component"); | 2424 "Cannot extract a file path from a URI with a fragment component"); |
1947 } | 2425 } |
1948 if (windows == null) windows = _isWindows; | 2426 if (windows == null) windows = _isWindows; |
1949 return windows ? _toWindowsFilePath() : _toFilePath(); | 2427 return windows ? _toWindowsFilePath(this) : _toFilePath(); |
1950 } | 2428 } |
1951 | 2429 |
1952 String _toFilePath() { | 2430 String _toFilePath() { |
1953 if (host != "") { | 2431 if (hasAuthority && host != "") { |
1954 throw new UnsupportedError( | 2432 throw new UnsupportedError( |
1955 "Cannot extract a non-Windows file path from a file URI " | 2433 "Cannot extract a non-Windows file path from a file URI " |
1956 "with an authority"); | 2434 "with an authority"); |
1957 } | 2435 } |
2436 // Use path segments to have any escapes unescaped. | |
2437 var pathSegments = this.pathSegments; | |
1958 _checkNonWindowsPathReservedCharacters(pathSegments, false); | 2438 _checkNonWindowsPathReservedCharacters(pathSegments, false); |
1959 var result = new StringBuffer(); | 2439 var result = new StringBuffer(); |
1960 if (_isPathAbsolute) result.write("/"); | 2440 if (hasAbsolutePath) result.write("/"); |
1961 result.writeAll(pathSegments, "/"); | 2441 result.writeAll(pathSegments, "/"); |
1962 return result.toString(); | 2442 return result.toString(); |
1963 } | 2443 } |
1964 | 2444 |
1965 String _toWindowsFilePath() { | 2445 static String _toWindowsFilePath(Uri uri) { |
1966 bool hasDriveLetter = false; | 2446 bool hasDriveLetter = false; |
1967 var segments = pathSegments; | 2447 var segments = uri.pathSegments; |
1968 if (segments.length > 0 && | 2448 if (segments.length > 0 && |
1969 segments[0].length == 2 && | 2449 segments[0].length == 2 && |
1970 segments[0].codeUnitAt(1) == _COLON) { | 2450 segments[0].codeUnitAt(1) == _COLON) { |
1971 _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); | 2451 _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); |
1972 _checkWindowsPathReservedCharacters(segments, false, 1); | 2452 _checkWindowsPathReservedCharacters(segments, false, 1); |
1973 hasDriveLetter = true; | 2453 hasDriveLetter = true; |
1974 } else { | 2454 } else { |
1975 _checkWindowsPathReservedCharacters(segments, false); | 2455 _checkWindowsPathReservedCharacters(segments, false, 0); |
1976 } | 2456 } |
1977 var result = new StringBuffer(); | 2457 var result = new StringBuffer(); |
1978 if (_isPathAbsolute && !hasDriveLetter) result.write("\\"); | 2458 if (uri.hasAbsolutePath && !hasDriveLetter) result.write(r"\"); |
1979 if (host != "") { | 2459 if (uri.hasAuthority) { |
1980 result.write("\\"); | 2460 var host = uri.host; |
1981 result.write(host); | 2461 if (host.isNotEmpty) { |
1982 result.write("\\"); | 2462 result.write(r"\"); |
2463 result.write(host); | |
2464 result.write(r"\"); | |
2465 } | |
1983 } | 2466 } |
1984 result.writeAll(segments, "\\"); | 2467 result.writeAll(segments, r"\"); |
1985 if (hasDriveLetter && segments.length == 1) result.write("\\"); | 2468 if (hasDriveLetter && segments.length == 1) result.write(r"\"); |
1986 return result.toString(); | 2469 return result.toString(); |
1987 } | 2470 } |
1988 | 2471 |
1989 bool get _isPathAbsolute { | 2472 bool get _isPathAbsolute { |
1990 if (path == null || path.isEmpty) return false; | 2473 return _path != null && _path.startsWith('/'); |
1991 return path.startsWith('/'); | |
1992 } | 2474 } |
1993 | 2475 |
1994 void _writeAuthority(StringSink ss) { | 2476 void _writeAuthority(StringSink ss) { |
1995 if (_userInfo.isNotEmpty) { | 2477 if (_userInfo.isNotEmpty) { |
1996 ss.write(_userInfo); | 2478 ss.write(_userInfo); |
1997 ss.write("@"); | 2479 ss.write("@"); |
1998 } | 2480 } |
1999 if (_host != null) ss.write(_host); | 2481 if (_host != null) ss.write(_host); |
2000 if (_port != null) { | 2482 if (_port != null) { |
2001 ss.write(":"); | 2483 ss.write(":"); |
2002 ss.write(_port); | 2484 ss.write(_port); |
2003 } | 2485 } |
2004 } | 2486 } |
2005 | 2487 |
2006 /** | 2488 /** |
2007 * Access the structure of a `data:` URI. | 2489 * Access the structure of a `data:` URI. |
2008 * | 2490 * |
2009 * Returns a [UriData] object for `data:` URIs and `null` for all other | 2491 * Returns a [UriData] object for `data:` URIs and `null` for all other |
2010 * URIs. | 2492 * URIs. |
2011 * The [UriData] object can be used to access the media type and data | 2493 * The [UriData] object can be used to access the media type and data |
2012 * of a `data:` URI. | 2494 * of a `data:` URI. |
2013 */ | 2495 */ |
2014 UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; | 2496 UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; |
2015 | 2497 |
2016 String toString() { | 2498 String toString() { |
2499 if (_text != null) return _text; | |
2017 StringBuffer sb = new StringBuffer(); | 2500 StringBuffer sb = new StringBuffer(); |
2018 _addIfNonEmpty(sb, scheme, scheme, ':'); | 2501 if (scheme.isNotEmpty) sb..write(scheme)..write(":"); |
2019 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | 2502 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { |
2020 // File URIS always have the authority, even if it is empty. | 2503 // File URIS always have the authority, even if it is empty. |
2021 // The empty URI means "localhost". | 2504 // The empty URI means "localhost". |
2022 sb.write("//"); | 2505 sb.write("//"); |
2023 _writeAuthority(sb); | 2506 _writeAuthority(sb); |
2024 } | 2507 } |
2025 sb.write(path); | 2508 sb.write(path); |
2026 if (_query != null) { sb..write("?")..write(_query); } | 2509 if (_query != null) sb..write("?")..write(_query); |
2027 if (_fragment != null) { sb..write("#")..write(_fragment); } | 2510 if (_fragment != null) sb..write("#")..write(_fragment); |
2028 return sb.toString(); | 2511 _text = sb.toString(); |
2512 return _text; | |
2029 } | 2513 } |
2030 | 2514 |
2031 bool operator==(other) { | 2515 bool operator==(other) { |
2032 if (other is! Uri) return false; | 2516 if (identical(this, other)) return true; |
2033 Uri uri = other; | 2517 if (other is Uri) { |
2034 return scheme == uri.scheme && | 2518 Uri uri = other; |
2035 hasAuthority == uri.hasAuthority && | 2519 return scheme == uri.scheme && |
2036 userInfo == uri.userInfo && | 2520 hasAuthority == uri.hasAuthority && |
2037 host == uri.host && | 2521 userInfo == uri.userInfo && |
2038 port == uri.port && | 2522 host == uri.host && |
2039 path == uri.path && | 2523 port == uri.port && |
2040 hasQuery == uri.hasQuery && | 2524 path == uri.path && |
2041 query == uri.query && | 2525 hasQuery == uri.hasQuery && |
2042 hasFragment == uri.hasFragment && | 2526 query == uri.query && |
2043 fragment == uri.fragment; | 2527 hasFragment == uri.hasFragment && |
2528 fragment == uri.fragment; | |
2529 } | |
2530 return false; | |
2044 } | 2531 } |
2045 | 2532 |
2046 int get hashCode { | 2533 int get hashCode { |
2047 int combine(part, current) { | 2534 return (_text ?? toString()).hashCode; |
2048 // The sum is truncated to 30 bits to make sure it fits into a Smi. | |
2049 return (current * 31 + part.hashCode) & 0x3FFFFFFF; | |
2050 } | |
2051 return combine(scheme, combine(userInfo, combine(host, combine(port, | |
2052 combine(path, combine(query, combine(fragment, 1))))))); | |
2053 } | |
2054 | |
2055 static void _addIfNonEmpty(StringBuffer sb, String test, | |
2056 String first, String second) { | |
2057 if ("" != test) { | |
2058 sb.write(first); | |
2059 sb.write(second); | |
2060 } | |
2061 } | |
2062 | |
2063 /** | |
2064 * Encode the string [component] using percent-encoding to make it | |
2065 * safe for literal use as a URI component. | |
2066 * | |
2067 * All characters except uppercase and lowercase letters, digits and | |
2068 * the characters `-_.!~*'()` are percent-encoded. This is the | |
2069 * set of characters specified in RFC 2396 and the which is | |
2070 * specified for the encodeUriComponent in ECMA-262 version 5.1. | |
2071 * | |
2072 * When manually encoding path segments or query components remember | |
2073 * to encode each part separately before building the path or query | |
2074 * string. | |
2075 * | |
2076 * For encoding the query part consider using | |
2077 * [encodeQueryComponent]. | |
2078 * | |
2079 * To avoid the need for explicitly encoding use the [pathSegments] | |
2080 * and [queryParameters] optional named arguments when constructing | |
2081 * a [Uri]. | |
2082 */ | |
2083 static String encodeComponent(String component) { | |
2084 return _uriEncode(_unreserved2396Table, component, UTF8, false); | |
2085 } | |
2086 | |
2087 /** | |
2088 * Encode the string [component] according to the HTML 4.01 rules | |
2089 * for encoding the posting of a HTML form as a query string | |
2090 * component. | |
2091 * | |
2092 * Encode the string [component] according to the HTML 4.01 rules | |
2093 * for encoding the posting of a HTML form as a query string | |
2094 * component. | |
2095 | |
2096 * The component is first encoded to bytes using [encoding]. | |
2097 * The default is to use [UTF8] encoding, which preserves all | |
2098 * the characters that don't need encoding. | |
2099 | |
2100 * Then the resulting bytes are "percent-encoded". This transforms | |
2101 * spaces (U+0020) to a plus sign ('+') and all bytes that are not | |
2102 * the ASCII decimal digits, letters or one of '-._~' are written as | |
2103 * a percent sign '%' followed by the two-digit hexadecimal | |
2104 * representation of the byte. | |
2105 | |
2106 * Note that the set of characters which are percent-encoded is a | |
2107 * superset of what HTML 4.01 requires, since it refers to RFC 1738 | |
2108 * for reserved characters. | |
2109 * | |
2110 * When manually encoding query components remember to encode each | |
2111 * part separately before building the query string. | |
2112 * | |
2113 * To avoid the need for explicitly encoding the query use the | |
2114 * [queryParameters] optional named arguments when constructing a | |
2115 * [Uri]. | |
2116 * | |
2117 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | |
2118 * details. | |
2119 */ | |
2120 static String encodeQueryComponent(String component, | |
2121 {Encoding encoding: UTF8}) { | |
2122 return _uriEncode(_unreservedTable, component, encoding, true); | |
2123 } | |
2124 | |
2125 /** | |
2126 * Decodes the percent-encoding in [encodedComponent]. | |
2127 * | |
2128 * Note that decoding a URI component might change its meaning as | |
2129 * some of the decoded characters could be characters with are | |
2130 * delimiters for a given URI componene type. Always split a URI | |
2131 * component using the delimiters for the component before decoding | |
2132 * the individual parts. | |
2133 * | |
2134 * For handling the [path] and [query] components consider using | |
2135 * [pathSegments] and [queryParameters] to get the separated and | |
2136 * decoded component. | |
2137 */ | |
2138 static String decodeComponent(String encodedComponent) { | |
2139 return _uriDecode(encodedComponent, 0, encodedComponent.length, | |
2140 UTF8, false); | |
2141 } | |
2142 | |
2143 /** | |
2144 * Decodes the percent-encoding in [encodedComponent], converting | |
2145 * pluses to spaces. | |
2146 * | |
2147 * It will create a byte-list of the decoded characters, and then use | |
2148 * [encoding] to decode the byte-list to a String. The default encoding is | |
2149 * UTF-8. | |
2150 */ | |
2151 static String decodeQueryComponent( | |
2152 String encodedComponent, | |
2153 {Encoding encoding: UTF8}) { | |
2154 return _uriDecode(encodedComponent, 0, encodedComponent.length, | |
2155 encoding, true); | |
2156 } | |
2157 | |
2158 /** | |
2159 * Encode the string [uri] using percent-encoding to make it | |
2160 * safe for literal use as a full URI. | |
2161 * | |
2162 * All characters except uppercase and lowercase letters, digits and | |
2163 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | |
2164 * is the set of characters specified in in ECMA-262 version 5.1 for | |
2165 * the encodeURI function . | |
2166 */ | |
2167 static String encodeFull(String uri) { | |
2168 return _uriEncode(_encodeFullTable, uri, UTF8, false); | |
2169 } | |
2170 | |
2171 /** | |
2172 * Decodes the percent-encoding in [uri]. | |
2173 * | |
2174 * Note that decoding a full URI might change its meaning as some of | |
2175 * the decoded characters could be reserved characters. In most | |
2176 * cases an encoded URI should be parsed into components using | |
2177 * [Uri.parse] before decoding the separate components. | |
2178 */ | |
2179 static String decodeFull(String uri) { | |
2180 return _uriDecode(uri, 0, uri.length, UTF8, false); | |
2181 } | |
2182 | |
2183 /** | |
2184 * Returns the [query] split into a map according to the rules | |
2185 * specified for FORM post in the [HTML 4.01 specification section | |
2186 * 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"). | |
2187 * Each key and value in the returned map has been decoded. If the [query] | |
2188 * is the empty string an empty map is returned. | |
2189 * | |
2190 * Keys in the query string that have no value are mapped to the | |
2191 * empty string. | |
2192 * | |
2193 * Each query component will be decoded using [encoding]. The default encoding | |
2194 * is UTF-8. | |
2195 */ | |
2196 static Map<String, String> splitQueryString(String query, | |
2197 {Encoding encoding: UTF8}) { | |
2198 return query.split("&").fold({}, (map, element) { | |
2199 int index = element.indexOf("="); | |
2200 if (index == -1) { | |
2201 if (element != "") { | |
2202 map[decodeQueryComponent(element, encoding: encoding)] = ""; | |
2203 } | |
2204 } else if (index != 0) { | |
2205 var key = element.substring(0, index); | |
2206 var value = element.substring(index + 1); | |
2207 map[Uri.decodeQueryComponent(key, encoding: encoding)] = | |
2208 decodeQueryComponent(value, encoding: encoding); | |
2209 } | |
2210 return map; | |
2211 }); | |
2212 } | 2535 } |
2213 | 2536 |
2214 static List _createList() => []; | 2537 static List _createList() => []; |
2215 | 2538 |
2216 static Map _splitQueryStringAll( | 2539 static Map _splitQueryStringAll( |
2217 String query, {Encoding encoding: UTF8}) { | 2540 String query, {Encoding encoding: UTF8}) { |
2218 Map result = {}; | 2541 Map result = {}; |
2219 int i = 0; | 2542 int i = 0; |
2220 int start = 0; | 2543 int start = 0; |
2221 int equalsIndex = -1; | 2544 int equalsIndex = -1; |
(...skipping 22 matching lines...) Expand all Loading... | |
2244 parsePair(start, equalsIndex, i); | 2567 parsePair(start, equalsIndex, i); |
2245 start = i + 1; | 2568 start = i + 1; |
2246 equalsIndex = -1; | 2569 equalsIndex = -1; |
2247 } | 2570 } |
2248 i++; | 2571 i++; |
2249 } | 2572 } |
2250 parsePair(start, equalsIndex, i); | 2573 parsePair(start, equalsIndex, i); |
2251 return result; | 2574 return result; |
2252 } | 2575 } |
2253 | 2576 |
2254 /** | |
2255 * Parse the [host] as an IP version 4 (IPv4) address, returning the address | |
2256 * as a list of 4 bytes in network byte order (big endian). | |
2257 * | |
2258 * Throws a [FormatException] if [host] is not a valid IPv4 address | |
2259 * representation. | |
2260 */ | |
2261 static List<int> parseIPv4Address(String host) { | |
2262 void error(String msg) { | |
2263 throw new FormatException('Illegal IPv4 address, $msg'); | |
2264 } | |
2265 var bytes = host.split('.'); | |
2266 if (bytes.length != 4) { | |
2267 error('IPv4 address should contain exactly 4 parts'); | |
2268 } | |
2269 // TODO(ajohnsen): Consider using Uint8List. | |
2270 return bytes | |
2271 .map((byteString) { | |
2272 int byte = int.parse(byteString); | |
2273 if (byte < 0 || byte > 255) { | |
2274 error('each part must be in the range of `0..255`'); | |
2275 } | |
2276 return byte; | |
2277 }) | |
2278 .toList(); | |
2279 } | |
2280 | |
2281 /** | |
2282 * Parse the [host] as an IP version 6 (IPv6) address, returning the address | |
2283 * as a list of 16 bytes in network byte order (big endian). | |
2284 * | |
2285 * Throws a [FormatException] if [host] is not a valid IPv6 address | |
2286 * representation. | |
2287 * | |
2288 * Acts on the substring from [start] to [end]. If [end] is omitted, it | |
2289 * defaults ot the end of the string. | |
2290 * | |
2291 * Some examples of IPv6 addresses: | |
2292 * * ::1 | |
2293 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | |
2294 * * 3ffe:2a00:100:7031::1 | |
2295 * * ::FFFF:129.144.52.38 | |
2296 * * 2010:836B:4179::836B:4179 | |
2297 */ | |
2298 static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | |
2299 if (end == null) end = host.length; | |
2300 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated | |
2301 // by `:`'s, with the following exceptions: | |
2302 // | |
2303 // - One (and only one) wildcard (`::`) may be present, representing a fill | |
2304 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. | |
2305 // - The last two parts may be replaced by an IPv4 address. | |
2306 void error(String msg, [position]) { | |
2307 throw new FormatException('Illegal IPv6 address, $msg', host, position); | |
2308 } | |
2309 int parseHex(int start, int end) { | |
2310 if (end - start > 4) { | |
2311 error('an IPv6 part can only contain a maximum of 4 hex digits', start); | |
2312 } | |
2313 int value = int.parse(host.substring(start, end), radix: 16); | |
2314 if (value < 0 || value > (1 << 16) - 1) { | |
2315 error('each part must be in the range of `0x0..0xFFFF`', start); | |
2316 } | |
2317 return value; | |
2318 } | |
2319 if (host.length < 2) error('address is too short'); | |
2320 List<int> parts = []; | |
2321 bool wildcardSeen = false; | |
2322 int partStart = start; | |
2323 // Parse all parts, except a potential last one. | |
2324 for (int i = start; i < end; i++) { | |
2325 if (host.codeUnitAt(i) == _COLON) { | |
2326 if (i == start) { | |
2327 // If we see a `:` in the beginning, expect wildcard. | |
2328 i++; | |
2329 if (host.codeUnitAt(i) != _COLON) { | |
2330 error('invalid start colon.', i); | |
2331 } | |
2332 partStart = i; | |
2333 } | |
2334 if (i == partStart) { | |
2335 // Wildcard. We only allow one. | |
2336 if (wildcardSeen) { | |
2337 error('only one wildcard `::` is allowed', i); | |
2338 } | |
2339 wildcardSeen = true; | |
2340 parts.add(-1); | |
2341 } else { | |
2342 // Found a single colon. Parse [partStart..i] as a hex entry. | |
2343 parts.add(parseHex(partStart, i)); | |
2344 } | |
2345 partStart = i + 1; | |
2346 } | |
2347 } | |
2348 if (parts.length == 0) error('too few parts'); | |
2349 bool atEnd = (partStart == end); | |
2350 bool isLastWildcard = (parts.last == -1); | |
2351 if (atEnd && !isLastWildcard) { | |
2352 error('expected a part after last `:`', end); | |
2353 } | |
2354 if (!atEnd) { | |
2355 try { | |
2356 parts.add(parseHex(partStart, end)); | |
2357 } catch (e) { | |
2358 // Failed to parse the last chunk as hex. Try IPv4. | |
2359 try { | |
2360 List<int> last = parseIPv4Address(host.substring(partStart, end)); | |
2361 parts.add(last[0] << 8 | last[1]); | |
2362 parts.add(last[2] << 8 | last[3]); | |
2363 } catch (e) { | |
2364 error('invalid end of IPv6 address.', partStart); | |
2365 } | |
2366 } | |
2367 } | |
2368 if (wildcardSeen) { | |
2369 if (parts.length > 7) { | |
2370 error('an address with a wildcard must have less than 7 parts'); | |
2371 } | |
2372 } else if (parts.length != 8) { | |
2373 error('an address without a wildcard must contain exactly 8 parts'); | |
2374 } | |
2375 List<int> bytes = new Uint8List(16); | |
2376 for (int i = 0, index = 0; i < parts.length; i++) { | |
2377 int value = parts[i]; | |
2378 if (value == -1) { | |
2379 int wildCardLength = 9 - parts.length; | |
2380 for (int j = 0; j < wildCardLength; j++) { | |
2381 bytes[index] = 0; | |
2382 bytes[index + 1] = 0; | |
2383 index += 2; | |
2384 } | |
2385 } else { | |
2386 bytes[index] = value >> 8; | |
2387 bytes[index + 1] = value & 0xff; | |
2388 index += 2; | |
2389 } | |
2390 } | |
2391 return bytes; | |
2392 } | |
2393 | |
2394 // Frequently used character codes. | |
2395 static const int _SPACE = 0x20; | |
2396 static const int _DOUBLE_QUOTE = 0x22; | |
2397 static const int _NUMBER_SIGN = 0x23; | |
2398 static const int _PERCENT = 0x25; | |
2399 static const int _ASTERISK = 0x2A; | |
2400 static const int _PLUS = 0x2B; | |
2401 static const int _DOT = 0x2E; | |
2402 static const int _SLASH = 0x2F; | |
2403 static const int _ZERO = 0x30; | |
2404 static const int _NINE = 0x39; | |
2405 static const int _COLON = 0x3A; | |
2406 static const int _LESS = 0x3C; | |
2407 static const int _GREATER = 0x3E; | |
2408 static const int _QUESTION = 0x3F; | |
2409 static const int _AT_SIGN = 0x40; | |
2410 static const int _UPPER_CASE_A = 0x41; | |
2411 static const int _UPPER_CASE_F = 0x46; | |
2412 static const int _UPPER_CASE_Z = 0x5A; | |
2413 static const int _LEFT_BRACKET = 0x5B; | |
2414 static const int _BACKSLASH = 0x5C; | |
2415 static const int _RIGHT_BRACKET = 0x5D; | |
2416 static const int _LOWER_CASE_A = 0x61; | |
2417 static const int _LOWER_CASE_F = 0x66; | |
2418 static const int _LOWER_CASE_Z = 0x7A; | |
2419 static const int _BAR = 0x7C; | |
2420 | |
2421 static const String _hexDigits = "0123456789ABCDEF"; | |
2422 | |
2423 external static String _uriEncode(List<int> canonicalTable, | 2577 external static String _uriEncode(List<int> canonicalTable, |
2424 String text, | 2578 String text, |
2425 Encoding encoding, | 2579 Encoding encoding, |
2426 bool spaceToPlus); | 2580 bool spaceToPlus); |
2427 | 2581 |
2428 /** | 2582 /** |
2429 * Convert a byte (2 character hex sequence) in string [s] starting | 2583 * Convert a byte (2 character hex sequence) in string [s] starting |
2430 * at position [pos] to its ordinal value | 2584 * at position [pos] to its ordinal value |
2431 */ | 2585 */ |
2432 static int _hexCharPairToByte(String s, int pos) { | 2586 static int _hexCharPairToByte(String s, int pos) { |
(...skipping 501 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2934 mimeType = ""; | 3088 mimeType = ""; |
2935 } | 3089 } |
2936 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { | 3090 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { |
2937 buffer.write(mimeType); // Common cases need no escaping. | 3091 buffer.write(mimeType); // Common cases need no escaping. |
2938 } else { | 3092 } else { |
2939 int slashIndex = _validateMimeType(mimeType); | 3093 int slashIndex = _validateMimeType(mimeType); |
2940 if (slashIndex < 0) { | 3094 if (slashIndex < 0) { |
2941 throw new ArgumentError.value(mimeType, "mimeType", | 3095 throw new ArgumentError.value(mimeType, "mimeType", |
2942 "Invalid MIME type"); | 3096 "Invalid MIME type"); |
2943 } | 3097 } |
2944 buffer.write(Uri._uriEncode(_tokenCharTable, | 3098 buffer.write(_Uri._uriEncode(_tokenCharTable, |
2945 mimeType.substring(0, slashIndex), | 3099 mimeType.substring(0, slashIndex), |
2946 UTF8, false)); | 3100 UTF8, false)); |
2947 buffer.write("/"); | 3101 buffer.write("/"); |
2948 buffer.write(Uri._uriEncode(_tokenCharTable, | 3102 buffer.write(_Uri._uriEncode(_tokenCharTable, |
2949 mimeType.substring(slashIndex + 1), | 3103 mimeType.substring(slashIndex + 1), |
2950 UTF8, false)); | 3104 UTF8, false)); |
2951 } | 3105 } |
2952 if (charsetName != null) { | 3106 if (charsetName != null) { |
2953 if (indices != null) { | 3107 if (indices != null) { |
2954 indices..add(buffer.length) | 3108 indices..add(buffer.length) |
2955 ..add(buffer.length + 8); | 3109 ..add(buffer.length + 8); |
2956 } | 3110 } |
2957 buffer.write(";charset="); | 3111 buffer.write(";charset="); |
2958 buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); | 3112 buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
2959 } | 3113 } |
2960 parameters?.forEach((var key, var value) { | 3114 parameters?.forEach((var key, var value) { |
2961 if (key.isEmpty) { | 3115 if (key.isEmpty) { |
2962 throw new ArgumentError.value("", "Parameter names must not be empty"); | 3116 throw new ArgumentError.value("", "Parameter names must not be empty"); |
2963 } | 3117 } |
2964 if (value.isEmpty) { | 3118 if (value.isEmpty) { |
2965 throw new ArgumentError.value("", "Parameter values must not be empty", | 3119 throw new ArgumentError.value("", "Parameter values must not be empty", |
2966 'parameters["$key"]'); | 3120 'parameters["$key"]'); |
2967 } | 3121 } |
2968 if (indices != null) indices.add(buffer.length); | 3122 if (indices != null) indices.add(buffer.length); |
2969 buffer.write(';'); | 3123 buffer.write(';'); |
2970 // Encode any non-RFC2045-token character and both '%' and '#'. | 3124 // Encode any non-RFC2045-token character and both '%' and '#'. |
2971 buffer.write(Uri._uriEncode(_tokenCharTable, key, UTF8, false)); | 3125 buffer.write(_Uri._uriEncode(_tokenCharTable, key, UTF8, false)); |
2972 if (indices != null) indices.add(buffer.length); | 3126 if (indices != null) indices.add(buffer.length); |
2973 buffer.write('='); | 3127 buffer.write('='); |
2974 buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); | 3128 buffer.write(_Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
2975 }); | 3129 }); |
2976 } | 3130 } |
2977 | 3131 |
2978 /** | 3132 /** |
2979 * Checks mimeType is valid-ish (`token '/' token`). | 3133 * Checks mimeType is valid-ish (`token '/' token`). |
2980 * | 3134 * |
2981 * Returns the index of the slash, or -1 if the mime type is not | 3135 * Returns the index of the slash, or -1 if the mime type is not |
2982 * considered valid. | 3136 * considered valid. |
2983 * | 3137 * |
2984 * Currently only looks for slashes, all other characters will be | 3138 * Currently only looks for slashes, all other characters will be |
2985 * percent-encoded as UTF-8 if necessary. | 3139 * percent-encoded as UTF-8 if necessary. |
2986 */ | 3140 */ |
2987 static int _validateMimeType(String mimeType) { | 3141 static int _validateMimeType(String mimeType) { |
2988 int slashIndex = -1; | 3142 int slashIndex = -1; |
2989 for (int i = 0; i < mimeType.length; i++) { | 3143 for (int i = 0; i < mimeType.length; i++) { |
2990 var char = mimeType.codeUnitAt(i); | 3144 var char = mimeType.codeUnitAt(i); |
2991 if (char != Uri._SLASH) continue; | 3145 if (char != _SLASH) continue; |
2992 if (slashIndex < 0) { | 3146 if (slashIndex < 0) { |
2993 slashIndex = i; | 3147 slashIndex = i; |
2994 continue; | 3148 continue; |
2995 } | 3149 } |
2996 return -1; | 3150 return -1; |
2997 } | 3151 } |
2998 return slashIndex; | 3152 return slashIndex; |
2999 } | 3153 } |
3000 | 3154 |
3001 /** | 3155 /** |
3002 * Parses a string as a `data` URI. | 3156 * Parses a string as a `data` URI. |
3003 * | 3157 * |
3004 * The string must have the format: | 3158 * The string must have the format: |
3005 * | 3159 * |
3006 * ``` | 3160 * ``` |
3007 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat a | 3161 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat a |
3008 * ```` | 3162 * ```` |
3009 * | 3163 * |
3010 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, | 3164 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, |
3011 * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). | 3165 * and `data` is a sequence of URI-characters (RFC-2396 `uric`). |
3012 * | 3166 * |
3013 * This means that all the characters must be ASCII, but the URI may contain | 3167 * This means that all the characters must be ASCII, but the URI may contain |
3014 * percent-escapes for non-ASCII byte values that need an interpretation | 3168 * percent-escapes for non-ASCII byte values that need an interpretation |
3015 * to be converted to the corresponding string. | 3169 * to be converted to the corresponding string. |
3016 * | 3170 * |
3017 * Parsing doesn't check the validity of any part, it just checks that the | 3171 * Parsing doesn't check the validity of any part, it just checks that the |
3018 * input has the correct structure with the correct sequence of `/`, `;`, `=` | 3172 * input has the correct structure with the correct sequence of `/`, `;`, `=` |
3019 * and `,` delimiters. | 3173 * and `,` delimiters. |
3020 * | 3174 * |
3021 * Accessing the individual parts may fail later if they turn out to have | 3175 * Accessing the individual parts may fail later if they turn out to have |
3022 * content that can't be decoded sucessfully as a string. | 3176 * content that can't be decoded successfully as a string. |
3023 */ | 3177 */ |
3024 static UriData parse(String uri) { | 3178 static UriData parse(String uri) { |
3025 if (!uri.startsWith("data:")) { | 3179 if (uri.length >= 5) { |
3026 throw new FormatException("Does not start with 'data:'", uri, 0); | 3180 int dataDelta = _startsWithData(uri, 0); |
floitsch
2016/06/29 23:41:47
startsWithData should return a bool, and you shoul
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Wouldn't work. I need to distinguish "data:" from
| |
3181 if (dataDelta == 0) { | |
3182 // Exact match on "data:". | |
3183 return _parse(uri, 5, null); | |
3184 } | |
3185 if (dataDelta == 0x20) { | |
3186 // Starts with a non-normalized "data" scheme containing upper-case | |
3187 // letters. Parse anyway, but throw away the scheme. | |
3188 return _parse(uri.substring(5), 0, null); | |
3189 } | |
3027 } | 3190 } |
3028 return _parse(uri, 5, null); | 3191 throw new FormatException("Does not start with 'data:'", uri, 0); |
3029 } | 3192 } |
3030 | 3193 |
3031 /** | 3194 /** |
3032 * The [Uri] that this `UriData` is giving access to. | 3195 * The [Uri] that this `UriData` is giving access to. |
3033 * | 3196 * |
3034 * Returns a `Uri` with scheme `data` and the remainder of the data URI | 3197 * Returns a `Uri` with scheme `data` and the remainder of the data URI |
3035 * as path. | 3198 * as path. |
3036 */ | 3199 */ |
3037 Uri get uri { | 3200 Uri get uri { |
3038 if (_uriCache != null) return _uriCache; | 3201 if (_uriCache != null) return _uriCache; |
3039 String path = _text; | 3202 String path = _text; |
3040 String query = null; | 3203 String query = null; |
3041 int colonIndex = _separatorIndices[0]; | 3204 int colonIndex = _separatorIndices[0]; |
3042 int queryIndex = _text.indexOf('?', colonIndex + 1); | 3205 int queryIndex = _text.indexOf('?', colonIndex + 1); |
3043 int end = null; | 3206 int end = null; |
3044 if (queryIndex >= 0) { | 3207 if (queryIndex >= 0) { |
3045 query = _text.substring(queryIndex + 1); | 3208 query = _text.substring(queryIndex + 1); |
3046 end = queryIndex; | 3209 end = queryIndex; |
3047 } | 3210 } |
3048 path = _text.substring(colonIndex + 1, end); | 3211 path = _text.substring(colonIndex + 1, end); |
3049 // TODO(lrn): This can generate a URI that isn't path normalized. | 3212 // TODO(lrn): This can generate a URI that isn't path normalized. |
3050 // That's perfectly reasonable - data URIs are not hierarchical, | 3213 // That's perfectly reasonable - data URIs are not hierarchical, |
3051 // but it may make some consumers stumble. | 3214 // but it may make some consumers stumble. |
3052 // Should we at least do escape normalization? | 3215 // Should we at least do escape normalization? |
3053 _uriCache = new Uri._internal("data", "", null, null, path, query, null); | 3216 _uriCache = new _Uri._internal("data", "", null, null, path, query, null); |
3054 return _uriCache; | 3217 return _uriCache; |
3055 } | 3218 } |
3056 | 3219 |
3057 /** | 3220 /** |
3058 * The MIME type of the data URI. | 3221 * The MIME type of the data URI. |
3059 * | 3222 * |
3060 * A data URI consists of a "media type" followed by data. | 3223 * A data URI consists of a "media type" followed by data. |
3061 * The media type starts with a MIME type and can be followed by | 3224 * The media type starts with a MIME type and can be followed by |
3062 * extra parameters. | 3225 * extra parameters. |
3063 * | 3226 * |
3064 * Example: | 3227 * Example: |
3065 * | 3228 * |
3066 * data:text/plain;charset=utf-8,Hello%20World! | 3229 * data:text/plain;charset=utf-8,Hello%20World! |
3067 * | 3230 * |
3068 * This data URI has the media type `text/plain;charset=utf-8`, which is the | 3231 * This data URI has the media type `text/plain;charset=utf-8`, which is the |
3069 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. | 3232 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. |
3070 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. | 3233 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. |
3071 * | 3234 * |
3072 * If the first part of the data URI is empty, it defaults to `text/plain`. | 3235 * If the first part of the data URI is empty, it defaults to `text/plain`. |
3073 */ | 3236 */ |
3074 String get mimeType { | 3237 String get mimeType { |
3075 int start = _separatorIndices[0] + 1; | 3238 int start = _separatorIndices[0] + 1; |
3076 int end = _separatorIndices[1]; | 3239 int end = _separatorIndices[1]; |
3077 if (start == end) return "text/plain"; | 3240 if (start == end) return "text/plain"; |
3078 return Uri._uriDecode(_text, start, end, UTF8, false); | 3241 return _Uri._uriDecode(_text, start, end, UTF8, false); |
3079 } | 3242 } |
3080 | 3243 |
3081 /** | 3244 /** |
3082 * The charset parameter of the media type. | 3245 * The charset parameter of the media type. |
3083 * | 3246 * |
3084 * If the parameters of the media type contains a `charset` parameter | 3247 * If the parameters of the media type contains a `charset` parameter |
3085 * then this returns its value, otherwise it returns `US-ASCII`, | 3248 * then this returns its value, otherwise it returns `US-ASCII`, |
3086 * which is the default charset for data URIs. | 3249 * which is the default charset for data URIs. |
3087 */ | 3250 */ |
3088 String get charset { | 3251 String get charset { |
3089 int parameterStart = 1; | 3252 int parameterStart = 1; |
3090 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. | 3253 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. |
3091 if (isBase64) { | 3254 if (isBase64) { |
3092 // There is a ";base64" separator, so subtract one for that as well. | 3255 // There is a ";base64" separator, so subtract one for that as well. |
3093 parameterEnd -= 1; | 3256 parameterEnd -= 1; |
3094 } | 3257 } |
3095 for (int i = parameterStart; i < parameterEnd; i += 2) { | 3258 for (int i = parameterStart; i < parameterEnd; i += 2) { |
3096 var keyStart = _separatorIndices[i] + 1; | 3259 var keyStart = _separatorIndices[i] + 1; |
3097 var keyEnd = _separatorIndices[i + 1]; | 3260 var keyEnd = _separatorIndices[i + 1]; |
3098 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { | 3261 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { |
3099 return Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], | 3262 return _Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], |
3100 UTF8, false); | 3263 UTF8, false); |
3101 } | 3264 } |
3102 } | 3265 } |
3103 return "US-ASCII"; | 3266 return "US-ASCII"; |
3104 } | 3267 } |
3105 | 3268 |
3106 /** | 3269 /** |
3107 * Whether the data is Base64 encoded or not. | 3270 * Whether the data is Base64 encoded or not. |
3108 */ | 3271 */ |
3109 bool get isBase64 => _separatorIndices.length.isOdd; | 3272 bool get isBase64 => _separatorIndices.length.isOdd; |
3110 | 3273 |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3148 result.setRange(0, length, text.codeUnits, start); | 3311 result.setRange(0, length, text.codeUnits, start); |
3149 return result; | 3312 return result; |
3150 } | 3313 } |
3151 int index = 0; | 3314 int index = 0; |
3152 for (int i = start; i < text.length; i++) { | 3315 for (int i = start; i < text.length; i++) { |
3153 var codeUnit = text.codeUnitAt(i); | 3316 var codeUnit = text.codeUnitAt(i); |
3154 if (codeUnit != percent) { | 3317 if (codeUnit != percent) { |
3155 result[index++] = codeUnit; | 3318 result[index++] = codeUnit; |
3156 } else { | 3319 } else { |
3157 if (i + 2 < text.length) { | 3320 if (i + 2 < text.length) { |
3158 var digit1 = Uri._parseHexDigit(text.codeUnitAt(i + 1)); | 3321 var digit1 = _Uri._parseHexDigit(text.codeUnitAt(i + 1)); |
3159 var digit2 = Uri._parseHexDigit(text.codeUnitAt(i + 2)); | 3322 var digit2 = _Uri._parseHexDigit(text.codeUnitAt(i + 2)); |
3160 if (digit1 >= 0 && digit2 >= 0) { | 3323 if (digit1 >= 0 && digit2 >= 0) { |
3161 int byte = digit1 * 16 + digit2; | 3324 int byte = digit1 * 16 + digit2; |
3162 result[index++] = byte; | 3325 result[index++] = byte; |
3163 i += 2; | 3326 i += 2; |
3164 continue; | 3327 continue; |
3165 } | 3328 } |
3166 } | 3329 } |
3167 throw new FormatException("Invalid percent escape", text, i); | 3330 throw new FormatException("Invalid percent escape", text, i); |
3168 } | 3331 } |
3169 } | 3332 } |
3170 assert(index == result.length); | 3333 assert(index == result.length); |
3171 return result; | 3334 return result; |
3172 } | 3335 } |
3173 | 3336 |
3174 /** | 3337 /** |
3175 * Returns a string created from the content of the data URI. | 3338 * Returns a string created from the content of the data URI. |
3176 * | 3339 * |
3177 * If the content is Base64 encoded, it will be decoded to bytes and then | 3340 * If the content is Base64 encoded, it will be decoded to bytes and then |
3178 * decoded to a string using [encoding]. | 3341 * decoded to a string using [encoding]. |
3179 * If encoding is omitted, the value of a `charset` parameter is used | 3342 * If encoding is omitted, the value of a `charset` parameter is used |
3180 * if it is recongized by [Encoding.getByName], otherwise it defaults to | 3343 * if it is recognized by [Encoding.getByName], otherwise it defaults to |
3181 * the [ASCII] encoding, which is the default encoding for data URIs | 3344 * the [ASCII] encoding, which is the default encoding for data URIs |
3182 * that do not specify an encoding. | 3345 * that do not specify an encoding. |
3183 * | 3346 * |
3184 * If the content is not Base64 encoded, it will first have percent-escapes | 3347 * If the content is not Base64 encoded, it will first have percent-escapes |
3185 * converted to bytes and then the character codes and byte values are | 3348 * converted to bytes and then the character codes and byte values are |
3186 * decoded using [encoding]. | 3349 * decoded using [encoding]. |
3187 */ | 3350 */ |
3188 String contentAsString({Encoding encoding}) { | 3351 String contentAsString({Encoding encoding}) { |
3189 if (encoding == null) { | 3352 if (encoding == null) { |
3190 var charset = this.charset; // Returns "US-ASCII" if not present. | 3353 var charset = this.charset; // Returns "US-ASCII" if not present. |
3191 encoding = Encoding.getByName(charset); | 3354 encoding = Encoding.getByName(charset); |
3192 if (encoding == null) { | 3355 if (encoding == null) { |
3193 throw new UnsupportedError("Unknown charset: $charset"); | 3356 throw new UnsupportedError("Unknown charset: $charset"); |
3194 } | 3357 } |
3195 } | 3358 } |
3196 String text = _text; | 3359 String text = _text; |
3197 int start = _separatorIndices.last + 1; | 3360 int start = _separatorIndices.last + 1; |
3198 if (isBase64) { | 3361 if (isBase64) { |
3199 var converter = BASE64.decoder.fuse(encoding.decoder); | 3362 var converter = BASE64.decoder.fuse(encoding.decoder); |
3200 return converter.convert(text.substring(start)); | 3363 return converter.convert(text.substring(start)); |
3201 } | 3364 } |
3202 return Uri._uriDecode(text, start, text.length, encoding, false); | 3365 return _Uri._uriDecode(text, start, text.length, encoding, false); |
3203 } | 3366 } |
3204 | 3367 |
3205 /** | 3368 /** |
3206 * A map representing the parameters of the media type. | 3369 * A map representing the parameters of the media type. |
3207 * | 3370 * |
3208 * A data URI may contain parameters between the MIME type and the | 3371 * A data URI may contain parameters between the MIME type and the |
3209 * data. This converts these parameters to a map from parameter name | 3372 * data. This converts these parameters to a map from parameter name |
3210 * to parameter value. | 3373 * to parameter value. |
3211 * The map only contains parameters that actually occur in the URI. | 3374 * The map only contains parameters that actually occur in the URI. |
3212 * The `charset` parameter has a default value even if it doesn't occur | 3375 * The `charset` parameter has a default value even if it doesn't occur |
3213 * in the URI, which is reflected by the [charset] getter. This means that | 3376 * in the URI, which is reflected by the [charset] getter. This means that |
3214 * [charset] may return a value even if `parameters["charset"]` is `null`. | 3377 * [charset] may return a value even if `parameters["charset"]` is `null`. |
3215 * | 3378 * |
3216 * If the values contain non-ASCII values or percent escapes, they default | 3379 * If the values contain non-ASCII values or percent escapes, they default |
3217 * to being decoded as UTF-8. | 3380 * to being decoded as UTF-8. |
3218 */ | 3381 */ |
3219 Map<String, String> get parameters { | 3382 Map<String, String> get parameters { |
3220 var result = <String, String>{}; | 3383 var result = <String, String>{}; |
3221 for (int i = 3; i < _separatorIndices.length; i += 2) { | 3384 for (int i = 3; i < _separatorIndices.length; i += 2) { |
3222 var start = _separatorIndices[i - 2] + 1; | 3385 var start = _separatorIndices[i - 2] + 1; |
3223 var equals = _separatorIndices[i - 1]; | 3386 var equals = _separatorIndices[i - 1]; |
3224 var end = _separatorIndices[i]; | 3387 var end = _separatorIndices[i]; |
3225 String key = Uri._uriDecode(_text, start, equals, UTF8, false); | 3388 String key = _Uri._uriDecode(_text, start, equals, UTF8, false); |
3226 String value = Uri._uriDecode(_text,equals + 1, end, UTF8, false); | 3389 String value = _Uri._uriDecode(_text,equals + 1, end, UTF8, false); |
3227 result[key] = value; | 3390 result[key] = value; |
3228 } | 3391 } |
3229 return result; | 3392 return result; |
3230 } | 3393 } |
3231 | 3394 |
3232 static UriData _parse(String text, int start, Uri sourceUri) { | 3395 static UriData _parse(String text, int start, Uri sourceUri) { |
3233 assert(start == 0 || start == 5); | 3396 assert(start == 0 || start == 5); |
3234 assert((start == 5) == text.startsWith("data:")); | 3397 assert((start == 5) == text.startsWith("data:")); |
floitsch
2016/06/29 23:41:47
(start == 5) == text.toLowerCase().startsWith("dat
Lasse Reichstein Nielsen
2016/06/30 10:27:31
We *need* to have a lower-case scheme. We promise
| |
3235 | 3398 |
3236 /// Character codes. | 3399 /// Character codes. |
3237 const int comma = 0x2c; | 3400 const int comma = 0x2c; |
3238 const int slash = 0x2f; | 3401 const int slash = 0x2f; |
3239 const int semicolon = 0x3b; | 3402 const int semicolon = 0x3b; |
3240 const int equals = 0x3d; | 3403 const int equals = 0x3d; |
3241 List<int> indices = [start - 1]; | 3404 List<int> indices = [start - 1]; |
3242 int slashIndex = -1; | 3405 int slashIndex = -1; |
3243 var char; | 3406 var char; |
3244 int i = start; | 3407 int i = start; |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3299 // Encode the string into bytes then generate an ASCII only string | 3462 // Encode the string into bytes then generate an ASCII only string |
3300 // by percent encoding selected bytes. | 3463 // by percent encoding selected bytes. |
3301 int byteOr = 0; | 3464 int byteOr = 0; |
3302 for (int i = 0; i < bytes.length; i++) { | 3465 for (int i = 0; i < bytes.length; i++) { |
3303 int byte = bytes[i]; | 3466 int byte = bytes[i]; |
3304 byteOr |= byte; | 3467 byteOr |= byte; |
3305 if (byte < 128 && | 3468 if (byte < 128 && |
3306 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | 3469 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
3307 buffer.writeCharCode(byte); | 3470 buffer.writeCharCode(byte); |
3308 } else { | 3471 } else { |
3309 buffer.writeCharCode(Uri._PERCENT); | 3472 buffer.writeCharCode(_PERCENT); |
3310 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte >> 4)); | 3473 buffer.writeCharCode(_hexDigits.codeUnitAt(byte >> 4)); |
3311 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte & 0x0f)); | 3474 buffer.writeCharCode(_hexDigits.codeUnitAt(byte & 0x0f)); |
3312 } | 3475 } |
3313 } | 3476 } |
3314 if ((byteOr & ~0xFF) != 0) { | 3477 if ((byteOr & ~0xFF) != 0) { |
3315 for (int i = 0; i < bytes.length; i++) { | 3478 for (int i = 0; i < bytes.length; i++) { |
3316 var byte = bytes[i]; | 3479 var byte = bytes[i]; |
3317 if (byte < 0 || byte > 255) { | 3480 if (byte < 0 || byte > 255) { |
3318 throw new ArgumentError.value(byte, "non-byte value"); | 3481 throw new ArgumentError.value(byte, "non-byte value"); |
3319 } | 3482 } |
3320 } | 3483 } |
3321 } | 3484 } |
(...skipping 28 matching lines...) Expand all Loading... | |
3350 0x7fff]; // 0x70 - 0x7f 11111111 11111110 | 3513 0x7fff]; // 0x70 - 0x7f 11111111 11111110 |
3351 | 3514 |
3352 // All non-escape RFC-2396 uric characters. | 3515 // All non-escape RFC-2396 uric characters. |
3353 // | 3516 // |
3354 // uric = reserved | unreserved | escaped | 3517 // uric = reserved | unreserved | escaped |
3355 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," | 3518 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," |
3356 // unreserved = alphanum | mark | 3519 // unreserved = alphanum | mark |
3357 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" | 3520 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" |
3358 // | 3521 // |
3359 // This is the same characters as in a URI query (which is URI pchar plus '?') | 3522 // This is the same characters as in a URI query (which is URI pchar plus '?') |
3360 static const _uricTable = Uri._queryCharTable; | 3523 static const _uricTable = _Uri._queryCharTable; |
3361 } | 3524 } |
3525 | |
3526 // -------------------------------------------------------------------- | |
3527 // Constants used to read the scanner result. | |
3528 // The indices points into the table returned by [_scanUri] which contains | |
3529 // recognized positions in the scanned URI. | |
3530 // The `0` index is only used internally. | |
3531 | |
3532 /// Index of the position of that `:` after a scheme. | |
3533 const int _schemeEndIndex = 1; | |
3534 /// Index of the position of the character just before the host name. | |
3535 const int _hostStartIndex = 2; | |
3536 /// Index of the position of the `:` before a port value. | |
3537 const int _portStartIndex = 3; | |
3538 /// Index of the position of the first character of a path. | |
3539 const int _pathStartIndex = 4; | |
3540 /// Index of the position of the `?` before a query. | |
3541 const int _queryStartIndex = 5; | |
3542 /// Index of the position of the `#` before a fragment. | |
3543 const int _fragmentStartIndex = 6; | |
3544 /// Index of a position where the URI was determined to be "non-simple". | |
3545 const int _notSimpleIndex = 7; | |
3546 | |
3547 // Initial state for scanner. | |
3548 const int _uriStart = 00; | |
3549 | |
3550 // If scanning of a URI terminates in this state or above, | |
3551 // consider the URI non-simple | |
3552 const int _nonSimpleEndStates = 14; | |
3553 | |
3554 // Initial state for scheme validation. | |
3555 const int _schemeStart = 20; | |
3556 | |
3557 /// Transition tables used to scan a URI to determine its structure. | |
3558 /// | |
3559 /// The tables represent a state machine with output. | |
3560 /// | |
3561 /// To scan the URI, start in the [_uriStart] state, then read each character | |
3562 /// of the URI in order, from start to end, and for each character perform a | |
3563 /// transition to a new state while writing the current position into the output | |
3564 /// buffer at a designated index. | |
3565 /// | |
3566 /// Each state, represented by an integer which is an index into | |
3567 /// [_scannerTables], has a set of transitions, one for each character. | |
3568 /// The transitions are encoded as a 5-bit integer representing the next state | |
3569 /// and a 3-bit index into the output table. | |
3570 /// | |
3571 /// For URI scanning, only characters in the range U+0020 through U+007E are | |
3572 /// interesting, all characters outside that range are treated the same. | |
3573 /// The tables only contain 96 entries, representing that characters in the | |
3574 /// interesting range, plus one more to represent all values outside the range. | |
3575 /// The character entries are stored in one `Uint8List` per state, with the | |
3576 /// transition for a character at position `character ^ 0x60`, | |
3577 /// which maps the range U+0020 .. U+007F into positions 0 .. 95. | |
3578 /// All remaining characters are mapped to position 31 (`0x7f ^ 0x60`) which | |
3579 /// represents the transition for all remaining characters. | |
3580 final List<Uint8List> _scannerTables = _createTables(); | |
3581 | |
3582 // ---------------------------------------------------------------------- | |
3583 // Code to create the URI scanner table. | |
3584 | |
3585 /// Creates the tables for [_scannerTables] used by [_scanUri]. | |
3586 /// | |
3587 /// See [_scannerTables] for the generated format. | |
3588 /// | |
3589 /// The concrete tables are chosen as a trade-off between the number of states | |
3590 /// needed and the precision of the result. | |
3591 /// This allows definitely recognizing the general structure of the URI | |
3592 /// (presence and location of scheme, user-info, host, port, path, query and | |
3593 /// fragment) while at the same time detecting that some components are not | |
3594 /// in canonical form (anything containing a `%`, a host-name containing a | |
3595 /// capital letter). Since the scanner doesn't know whether something is a | |
3596 /// scheme or a path until it sees `:`, or user-info or host until it sees | |
3597 /// a `@`, a second pass is needed to validate the scheme and any user-info | |
3598 /// is considered non-canonical by default. | |
3599 List<Uint8List> _createTables() { | |
3600 // TODO(lrn): Use a precomputed table. | |
3601 | |
3602 // Total number of states for the scanner. | |
3603 const int stateCount = 22; | |
3604 | |
3605 // States used to scan a URI from scratch. | |
3606 const int schemeOrPath = 01; | |
3607 const int authOrPath = 02; | |
3608 const int authOrPathSlash = 03; | |
3609 const int uinfoOrHost0 = 04; | |
3610 const int uinfoOrHost = 05; | |
3611 const int uinfoOrPort0 = 06; | |
3612 const int uinfoOrPort = 07; | |
3613 const int ipv6Host = 08; | |
3614 const int relPathSeg = 09; | |
3615 const int pathSeg = 10; | |
3616 const int path = 11; | |
3617 const int query = 12; | |
3618 const int fragment = 13; | |
3619 const int schemeOrPathDot = 14; | |
3620 const int schemeOrPathDot2 = 15; | |
3621 const int relPathSegDot = 16; | |
3622 const int relPathSegDot2 = 17; | |
3623 const int pathSegDot = 18; | |
3624 const int pathSegDot2 = 19; | |
3625 | |
3626 // States used to validate a scheme after its end position has been found. | |
3627 const int scheme0 = _schemeStart; | |
3628 const int scheme = 21; | |
3629 | |
3630 // Constants encoding the write-index for the state transition into the top 5 | |
3631 // bits of a byte. | |
3632 const int schemeEnd = _schemeEndIndex << 5; | |
3633 const int hostStart = _hostStartIndex << 5; | |
3634 const int portStart = _portStartIndex << 5; | |
3635 const int pathStart = _pathStartIndex << 5; | |
3636 const int queryStart = _queryStartIndex << 5; | |
3637 const int fragmentStart = _fragmentStartIndex << 5; | |
3638 const int notSimple = _notSimpleIndex << 5; | |
3639 | |
3640 /// The `unreserved` characters of RFC 3986. | |
3641 const unreserved = | |
3642 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~" ; | |
3643 /// The `sub-delim` characters of RFC 3986. | |
3644 const subDelims = r"!$&'()*+,;="; | |
3645 // The `pchar` characters of RFC 3986: characters that may occur in a path, | |
3646 // excluding escapes. | |
3647 const pchar = "$unreserved$subDelims"; | |
3648 | |
3649 var tables = new List<Uint8List>.generate(stateCount, | |
3650 (_) => new Uint8List(96)); | |
3651 | |
3652 // Helper function which initialize the table for [state] with a default | |
3653 // transition and returns the table. | |
3654 Uint8List build(state, defaultTransition) => | |
3655 tables[state]..fillRange(0, 96, defaultTransition); | |
3656 | |
3657 // Helper function which sets the transition for each character in [chars] | |
3658 // to [transition] in the [target] table. | |
3659 // The [chars] string must contain only characters in the U+0020 .. U+007E | |
3660 // range. | |
3661 void setChars(Uint8List target, String chars, int transition) { | |
3662 for (int i = 0; i < chars.length; i++) { | |
3663 var char = chars.codeUnitAt(i); | |
3664 target[char ^ 0x60] = transition; | |
3665 } | |
3666 } | |
3667 | |
3668 /// Helper function which sets the transition for all characters in the | |
3669 /// range from `range[0]` to `range[1]` to [transition] in the [target] table. | |
3670 /// | |
3671 /// The [range] must be a two-character string where both characters are in | |
3672 /// the U+0020 .. U+007E range and the former character must have a lower | |
3673 /// code point than the latter. | |
3674 void setRange(Uint8List target, String range, int transition) { | |
3675 for (int i = range.codeUnitAt(0), n = range.codeUnitAt(1); i <= n; i++) { | |
3676 target[i ^ 0x60] = transition; | |
3677 } | |
3678 } | |
3679 | |
3680 // Create the transitions for each state. | |
3681 var b; | |
3682 | |
3683 // Validate as path, if it is a scheme, we handle it later. | |
3684 b = build(_uriStart, schemeOrPath | notSimple); | |
3685 setChars(b, pchar, schemeOrPath); | |
3686 setChars(b, ".", schemeOrPathDot); | |
3687 setChars(b, ":", authOrPath | schemeEnd); // Handle later. | |
3688 setChars(b, "/", authOrPathSlash); | |
3689 setChars(b, "?", query | queryStart); | |
3690 setChars(b, "#", fragment | fragmentStart); | |
3691 | |
3692 b = build(schemeOrPathDot, schemeOrPath | notSimple); | |
3693 setChars(b, pchar, schemeOrPath); | |
3694 setChars(b, ".", schemeOrPathDot2); | |
3695 setChars(b, ':', authOrPath | schemeEnd); | |
3696 setChars(b, "/", pathSeg | notSimple); | |
3697 setChars(b, "?", query | queryStart); | |
3698 setChars(b, "#", fragment | fragmentStart); | |
3699 | |
3700 b = build(schemeOrPathDot2, schemeOrPath | notSimple); | |
3701 setChars(b, pchar, schemeOrPath); | |
3702 setChars(b, "%", schemeOrPath | notSimple); | |
3703 setChars(b, ':', authOrPath | schemeEnd); | |
3704 setChars(b, "/", relPathSeg); | |
3705 setChars(b, "?", query | queryStart); | |
3706 setChars(b, "#", fragment | fragmentStart); | |
3707 | |
3708 b = build(schemeOrPath, schemeOrPath | notSimple); | |
3709 setChars(b, pchar, schemeOrPath); | |
3710 setChars(b, ':', authOrPath | schemeEnd); | |
3711 setChars(b, "/", pathSeg); | |
3712 setChars(b, "?", query | queryStart); | |
3713 setChars(b, "#", fragment | fragmentStart); | |
3714 | |
3715 b = build(authOrPath, path | notSimple); | |
3716 setChars(b, pchar, path); | |
3717 setChars(b, "/", authOrPathSlash); | |
3718 setChars(b, ".", pathSegDot); | |
3719 setChars(b, "?", query | queryStart); | |
3720 setChars(b, "#", fragment | fragmentStart); | |
3721 | |
3722 b = build(authOrPathSlash, path | notSimple); | |
3723 setChars(b, pchar, path); | |
3724 setChars(b, "/", uinfoOrHost0 | hostStart); | |
3725 setChars(b, ".", pathSegDot); | |
3726 setChars(b, "?", query | queryStart); | |
3727 setChars(b, "#", fragment | fragmentStart); | |
3728 | |
3729 b = build(uinfoOrHost0, uinfoOrHost | notSimple); | |
3730 setChars(b, pchar, uinfoOrHost); | |
3731 setRange(b, "AZ", uinfoOrHost | notSimple); | |
3732 setChars(b, ":", uinfoOrPort0 | portStart); | |
3733 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3734 setChars(b, "[", ipv6Host | notSimple); | |
3735 setChars(b, "/", pathSeg | pathStart); | |
3736 setChars(b, "?", query | queryStart); | |
3737 setChars(b, "#", fragment | fragmentStart); | |
3738 | |
3739 b = build(uinfoOrHost, uinfoOrHost | notSimple); | |
3740 setChars(b, pchar, uinfoOrHost); | |
3741 setRange(b, "AZ", uinfoOrHost | notSimple); | |
3742 setChars(b, ":", uinfoOrPort0 | portStart); | |
3743 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3744 setChars(b, "/", pathSeg | pathStart); | |
3745 setChars(b, "?", query | queryStart); | |
3746 setChars(b, "#", fragment | fragmentStart); | |
3747 | |
3748 b = build(uinfoOrPort0, uinfoOrPort | notSimple); | |
3749 setRange(b, "19", uinfoOrPort); | |
3750 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3751 setChars(b, "/", pathSeg | pathStart); | |
3752 setChars(b, "?", query | queryStart); | |
3753 setChars(b, "#", fragment | fragmentStart); | |
3754 | |
3755 b = build(uinfoOrPort, uinfoOrPort | notSimple); | |
3756 setRange(b, "09", uinfoOrPort); | |
3757 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3758 setChars(b, "/", pathSeg | pathStart); | |
3759 setChars(b, "?", query | queryStart); | |
3760 setChars(b, "#", fragment | fragmentStart); | |
3761 | |
3762 b = build(ipv6Host, ipv6Host); | |
3763 setChars(b, "]", uinfoOrHost); | |
3764 | |
3765 b = build(relPathSeg, path | notSimple); | |
3766 setChars(b, pchar, path); | |
3767 setChars(b, ".", relPathSegDot); | |
3768 setChars(b, "/", pathSeg | notSimple); | |
3769 setChars(b, "?", query | queryStart); | |
3770 setChars(b, "#", fragment | fragmentStart); | |
3771 | |
3772 b = build(relPathSegDot, path | notSimple); | |
3773 setChars(b, pchar, path); | |
3774 setChars(b, ".", relPathSegDot2); | |
3775 setChars(b, "/", pathSeg | notSimple); | |
3776 setChars(b, "?", query | queryStart); | |
3777 setChars(b, "#", fragment | fragmentStart); | |
3778 | |
3779 b = build(relPathSegDot2, path | notSimple); | |
3780 setChars(b, pchar, path); | |
3781 setChars(b, "/", relPathSeg); | |
3782 setChars(b, "?", query | queryStart); // This should be non-simple. | |
3783 setChars(b, "#", fragment | fragmentStart); // This should be non-simple. | |
3784 | |
3785 b = build(pathSeg, path | notSimple); | |
3786 setChars(b, pchar, path); | |
3787 setChars(b, ".", pathSegDot); | |
3788 setChars(b, "/", pathSeg | notSimple); | |
3789 setChars(b, "?", query | queryStart); | |
3790 setChars(b, "#", fragment | fragmentStart); | |
3791 | |
3792 b = build(pathSegDot, path | notSimple); | |
3793 setChars(b, pchar, path); | |
3794 setChars(b, ".", pathSegDot2); | |
3795 setChars(b, "/", pathSeg | notSimple); | |
3796 setChars(b, "?", query | queryStart); | |
3797 setChars(b, "#", fragment | fragmentStart); | |
3798 | |
3799 b = build(pathSegDot2, path | notSimple); | |
3800 setChars(b, pchar, path); | |
3801 setChars(b, "/", pathSeg | notSimple); | |
3802 setChars(b, "?", query | queryStart); | |
3803 setChars(b, "#", fragment | fragmentStart); | |
3804 | |
3805 b = build(path, path | notSimple); | |
3806 setChars(b, pchar, path); | |
3807 setChars(b, "/", pathSeg); | |
3808 setChars(b, "?", query | queryStart); | |
3809 setChars(b, "#", fragment | fragmentStart); | |
3810 | |
3811 b = build(query, query | notSimple); | |
3812 setChars(b, pchar, query); | |
3813 setChars(b, "?", query); | |
3814 setChars(b, "#", fragment | fragmentStart); | |
3815 | |
3816 b = build(fragment, fragment | notSimple); | |
3817 setChars(b, pchar, fragment); | |
3818 setChars(b, "?", fragment); | |
3819 | |
3820 // A separate two-state validator for lower-case scheme names. | |
3821 // Any non-scheme character or upper-case letter is marked as non-simple. | |
3822 b = build(scheme0, scheme | notSimple); | |
3823 setRange(b, "az", scheme); | |
3824 | |
3825 b = build(scheme, scheme | notSimple); | |
3826 setRange(b, "az", scheme); | |
3827 setRange(b, "09", scheme); | |
3828 setChars(b, "+-.", scheme); | |
3829 | |
3830 return tables; | |
3831 } | |
3832 | |
3833 // -------------------------------------------------------------------- | |
3834 // Code that uses the URI scanner table. | |
3835 | |
3836 /// Scan a string using the [_scannerTables] state machine. | |
3837 /// | |
3838 /// Scans [uri] from [start] to [end], startig in state [state] and | |
3839 /// writing output into [indices]. | |
3840 /// | |
3841 /// Returns the final state. | |
3842 int _scan(String uri, int start, int end, int state, List<int> indices) { | |
3843 var tables = _scannerTables; | |
3844 assert(end <= uri.length); | |
3845 for (int i = start; i < end; i++) { | |
3846 var table = tables[state]; | |
3847 // Xor with 0x60 to move range 0x20-0x7f into 0x00-0x5f | |
3848 int char = uri.codeUnitAt(i) ^ 0x60; | |
3849 // Use 0x1f (nee 0x7f) to represent all unhandled characters. | |
3850 if (char > 0x5f) char = 0x1f; | |
3851 int transition = table[char]; | |
3852 state = transition & 0x1f; | |
3853 indices[transition >> 5] = i; | |
3854 } | |
3855 return state; | |
3856 } | |
3857 | |
3858 /// Scan a URI to determine its structure. | |
3859 /// | |
3860 /// Scans the [uri] from [start] to [end] and returns a list of indices | |
3861 /// representing significant positions in the URI. The positions are | |
3862 /// sufficient to split the URI into its structural parts. | |
3863 /// | |
3864 /// The returned list of positions have the following values at the given | |
3865 /// indices: | |
3866 /// | |
3867 /// - [_schemeEndIndex]: Is `start-1` if the URI has no scheme, | |
3868 /// otherwise it is the position of the `:` after the scheme. | |
3869 /// - [_hostStartIndex]: Either [start] if the URI has no authority component, | |
3870 /// otherwise either the position of the `@` after a user-info part or the | |
3871 /// position of the final `/` in the `//` leading the authority. | |
3872 /// - [_portStartIndex]: Either [start] if the URI has no authority component, o r | |
floitsch
2016/06/29 23:41:47
long line.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Reworded and moved after inlining _scanUri.
| |
3873 /// the position of the `:` leading a port, or the same position as the | |
3874 /// start of the path if the URI has no port. | |
3875 /// - [_pathStartIndex]: The position of the initial character of the path. | |
3876 /// If the path is empty, then the position may be [start] or the position | |
3877 /// of the initial `/` in the `//` leading an authority. | |
3878 /// If there is a scheme or authority, and the path start is before the | |
3879 /// query end or host start, the actual path is empty. | |
3880 /// - [_queryStartIndex]: The position of the `?` leading a query if the URI | |
3881 /// contains a query, otherwise [end]. | |
3882 /// - [_fragmentStartIndex]: The position of the `#` leading a fragment if the | |
3883 /// URI contains a fragment, otherwise [end]. | |
3884 /// - [_notSimpleIndex]: Equal to `start - 1` unless the URI is considered | |
3885 /// "not simple". This is elaborated below. | |
3886 /// | |
3887 /// The returned positions are limited by the scanners ability to write only | |
3888 /// one position per character, and only the current position. | |
3889 /// Scanning from left to right, we only know whether something is a scheme | |
3890 /// or a path when we see a `:` or `/`, and likewise we only know if the first | |
3891 /// `/` is part of the path or is leading an authority component when we see | |
3892 /// the next character. | |
3893 /// | |
3894 /// # Simple URIs | |
3895 /// A URI is considered "simple" if it is in a normalized form containing no | |
3896 /// escapes. This allows us to skip normalization and checking whether escapes | |
3897 /// are valid, and to extract components without worrying about unescaping. | |
3898 /// | |
3899 /// The scanner computes a conservative approximation of being "simple". | |
3900 /// It rejects any URI with an escape, with a user-info component (mainly | |
3901 /// because they are rare and would increase the number of states in the | |
3902 /// scanner significantly), with an IPV6 host or with a capital letter in | |
3903 /// the scheme or host name. | |
3904 /// Further, paths containing `..` or `.` path segments are considered | |
3905 /// non-simple except for relative paths (no scheme or authority) starting | |
3906 /// with a sequence of "../" segments. | |
3907 /// | |
3908 /// There are some scheme-based normalizations for `file`, `http` and `https` | |
3909 /// URIs that have to be handled after scanning, to either consider the URI | |
3910 /// non-simple or normalize the string. | |
3911 List<int> _scanUri(String uri, int start, int end) { | |
3912 var indices = new List<int>.filled(8, start - 1); | |
3913 // For some positions, pick a better default which will be the correct value | |
3914 // in some cases where the value isn't updated by the scanner. | |
3915 indices[_portStartIndex] = start; | |
3916 indices[_pathStartIndex] = start; | |
3917 indices[_queryStartIndex] = end; | |
3918 indices[_fragmentStartIndex] = end; | |
3919 var state = _scan(uri, start, end, _uriStart, indices); | |
3920 // Some states that should be non-simple, but the URI ended early. | |
3921 // In particular paths that end at a ".." must be normalized to end in "../". | |
3922 if (state >= _nonSimpleEndStates) { | |
3923 indices[_notSimpleIndex] = end; | |
3924 } | |
3925 int schemeEnd = indices[_schemeEndIndex]; | |
3926 if (schemeEnd >= start) { | |
3927 // Rescan the scheme part now that we know it's not a path. | |
3928 state = _scan(uri, start, schemeEnd, _schemeStart, indices); | |
3929 if (state == _schemeStart) { | |
3930 // Empty scheme. | |
3931 indices[_notSimpleIndex] = schemeEnd; | |
3932 } | |
3933 } | |
3934 return indices; | |
3935 } | |
3936 | |
3937 class _SimpleUri implements Uri { | |
3938 final String _uri; | |
3939 final int _schemeEnd; | |
3940 final int _hostStart; | |
3941 final int _portStart; | |
3942 final int _pathStart; | |
3943 final int _queryStart; | |
3944 final int _fragmentStart; | |
3945 /// The scheme is often used to distinguish URIs. | |
3946 /// To make comparisons more efficient, we cache the value, and | |
3947 /// canonicalize a few known types. | |
3948 String _schemeCache; | |
3949 | |
3950 _SimpleUri( | |
3951 this._uri, | |
3952 this._schemeEnd, | |
3953 this._hostStart, | |
3954 this._portStart, | |
3955 this._pathStart, | |
3956 this._queryStart, | |
3957 this._fragmentStart, | |
3958 this._schemeCache); | |
3959 | |
3960 bool get hasScheme => _schemeEnd > 0; | |
3961 bool get hasAuthority => _hostStart > 0; | |
3962 bool get hasUserInfo => _hostStart > _schemeEnd + 4; | |
3963 bool get hasPort => _hostStart > 0 && _portStart + 1 < _pathStart; | |
3964 bool get hasQuery => _queryStart < _fragmentStart; | |
3965 bool get hasFragment => _fragmentStart < _uri.length; | |
3966 | |
3967 bool get _isFile => _schemeEnd == 4 && _uri.startsWith("file"); | |
3968 bool get _isHttp => _schemeEnd == 4 && _uri.startsWith("http"); | |
3969 bool get _isHttps => _schemeEnd == 5 && _uri.startsWith("https"); | |
3970 bool get _isPackage => _schemeEnd == 7 && _uri.startsWith("package"); | |
3971 bool _isScheme(String scheme) => | |
3972 _schemeEnd == scheme.length && _uri.startsWith(scheme); | |
3973 | |
3974 bool get hasAbsolutePath => _uri.startsWith("/", _pathStart); | |
3975 bool get hasEmptyPath => _pathStart == _queryStart; | |
3976 | |
3977 bool get isAbsolute => hasScheme && !hasFragment; | |
3978 | |
3979 String get scheme { | |
3980 if (_schemeEnd <= 0) return ""; | |
3981 if (_schemeCache != null) return _schemeCache; | |
3982 if (_isHttp) { | |
3983 _schemeCache = "http"; | |
3984 } else if (_isHttps) { | |
3985 _schemeCache = "https"; | |
3986 } else if (_isFile) { | |
3987 _schemeCache = "file"; | |
3988 } else if (_isPackage) { | |
3989 _schemeCache = "package"; | |
3990 } else { | |
3991 _schemeCache = _uri.substring(0, _schemeEnd); | |
3992 } | |
3993 return _schemeCache; | |
3994 } | |
3995 String get authority => _hostStart > 0 ? | |
3996 _uri.substring(_schemeEnd + 3, _pathStart) : ""; | |
3997 String get userInfo => (_hostStart > _schemeEnd + 3) ? | |
3998 _uri.substring(_schemeEnd + 3, _hostStart - 1) : ""; | |
3999 String get host => | |
4000 _hostStart > 0 ? _uri.substring(_hostStart, _portStart) : ""; | |
4001 int get port { | |
4002 if (hasPort) return int.parse(_uri.substring(_portStart + 1, _pathStart)); | |
4003 if (_isHttp) return 80; | |
4004 if (_isHttps) return 443; | |
4005 return 0; | |
4006 } | |
4007 String get path =>_uri.substring(_pathStart, _queryStart); | |
4008 String get query => (_queryStart < _fragmentStart) ? | |
4009 _uri.substring(_queryStart + 1, _fragmentStart) : ""; | |
4010 String get fragment => (_fragmentStart < _uri.length) ? | |
4011 _uri.substring(_fragmentStart + 1) : ""; | |
4012 | |
4013 String get origin { | |
4014 // Check original behavior - W3C spec is wonky! | |
4015 bool isHttp = _isHttp; | |
4016 if (_schemeEnd < 0 || _hostStart == _portStart) { | |
4017 throw new StateError("Cannot use origin without a scheme: $this"); | |
4018 } | |
4019 if (!isHttp && !_isHttps) { | |
4020 throw new StateError( | |
4021 "Origin is only applicable schemes http and https: $this"); | |
4022 } | |
4023 if (_hostStart == _schemeEnd + 3) { | |
4024 return _uri.substring(0, _pathStart); | |
4025 } | |
4026 // Need to drop anon-empty userInfo. | |
4027 return _uri.substring(0, _schemeEnd + 3) + | |
4028 _uri.substring(_hostStart, _pathStart); | |
4029 } | |
4030 | |
4031 List<String> get pathSegments { | |
4032 int start = _pathStart; | |
4033 int end = _queryStart; | |
4034 if (_uri.startsWith("/", start)) start++; | |
4035 if (start == end) return const <String>[]; | |
4036 List<String> parts = []; | |
4037 for (int i = start; i < end; i++) { | |
4038 var char = _uri.codeUnitAt(i); | |
4039 if (char == _SLASH) { | |
4040 parts.add(_uri.substring(start, i)); | |
4041 start = i + 1; | |
4042 } | |
4043 } | |
4044 parts.add(_uri.substring(start, end)); | |
4045 return new List<String>.unmodifiable(parts); | |
4046 } | |
4047 | |
4048 Map<String, String> get queryParameters { | |
4049 if (!hasQuery) return const <String, String>{}; | |
4050 return new UnmodifiableMapView<String, String>( | |
4051 Uri.splitQueryString(query)); | |
4052 } | |
4053 | |
4054 Map<String, List<String>> get queryParametersAll { | |
4055 if (!hasQuery) return const <String, List<String>>{}; | |
4056 Map queryParameterLists = _Uri._splitQueryStringAll(query); | |
4057 for (var key in queryParameterLists.keys) { | |
4058 queryParameterLists[key] = | |
4059 new List<String>.unmodifiable(queryParameterLists[key]); | |
4060 } | |
4061 return new Map<String, List<String>>.unmodifiable(queryParameterLists); | |
4062 } | |
4063 | |
4064 bool _isPort(String port) { | |
4065 int portDigitStart = _portStart + 1; | |
4066 return portDigitStart + port.length == _pathStart && | |
4067 _uri.startsWith(port, portDigitStart); | |
4068 } | |
4069 | |
4070 Uri normalizePath() => this; | |
4071 | |
4072 Uri removeFragment() { | |
4073 if (!hasFragment) return this; | |
4074 return new _SimpleUri( | |
4075 _uri.substring(0, _fragmentStart), | |
4076 _schemeEnd, _hostStart, _portStart, | |
4077 _pathStart, _queryStart, _fragmentStart, _schemeCache); | |
4078 } | |
4079 | |
4080 Uri replace({String scheme, | |
4081 String userInfo, | |
4082 String host, | |
4083 int port, | |
4084 String path, | |
4085 Iterable<String> pathSegments, | |
4086 String query, | |
4087 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | |
4088 String fragment}) { | |
4089 bool schemeChanged = false; | |
4090 if (scheme != null) { | |
4091 scheme = _Uri._makeScheme(scheme, 0, scheme.length); | |
4092 schemeChanged = !_isScheme(scheme); | |
4093 } else { | |
4094 scheme = this.scheme; | |
4095 } | |
4096 bool isFile = (scheme == "file"); | |
4097 if (userInfo != null) { | |
4098 userInfo = _Uri._makeUserInfo(userInfo, 0, userInfo.length); | |
4099 } else if (_hostStart > 0) { | |
4100 userInfo = _uri.substring(_schemeEnd + 3, _hostStart); | |
4101 } else { | |
4102 userInfo = ""; | |
4103 } | |
4104 if (port != null) { | |
4105 port = _Uri._makePort(port, scheme); | |
4106 } else { | |
4107 port = this.hasPort ? this.port : null; | |
4108 if (schemeChanged) { | |
4109 // The default port might have changed. | |
4110 port = _Uri._makePort(port, scheme); | |
4111 } | |
4112 } | |
4113 if (host != null) { | |
4114 host = _Uri._makeHost(host, 0, host.length, false); | |
4115 } else if (_hostStart > 0) { | |
4116 host = _uri.substring(_hostStart, _portStart); | |
4117 } else if (userInfo.isNotEmpty || port != null || isFile) { | |
4118 host = ""; | |
4119 } | |
4120 | |
4121 bool hasAuthority = host != null; | |
4122 if (path != null || pathSegments != null) { | |
4123 path = _Uri._makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
4124 scheme, hasAuthority); | |
4125 } else { | |
4126 path = _uri.substring(_pathStart, _queryStart); | |
4127 if ((isFile || (hasAuthority && !path.isEmpty)) && | |
4128 !path.startsWith('/')) { | |
4129 path = "/" + path; | |
4130 } | |
4131 } | |
4132 | |
4133 if (query != null || queryParameters != null) { | |
4134 query = | |
4135 _Uri._makeQuery(query, 0, _stringOrNullLength(query), queryParameters) ; | |
floitsch
2016/06/29 23:41:47
long line.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
| |
4136 } else if (_queryStart < _fragmentStart) { | |
4137 query = _uri.substring(_queryStart, _fragmentStart); | |
4138 } | |
4139 | |
4140 if (fragment != null) { | |
4141 fragment = _Uri._makeFragment(fragment, 0, fragment.length); | |
4142 } else if (_fragmentStart < _uri.length) { | |
4143 fragment = _uri.substring(_fragmentStart); | |
4144 } | |
4145 | |
4146 return new _Uri._internal( | |
4147 scheme, userInfo, host, port, path, query, fragment); | |
4148 } | |
4149 | |
4150 Uri resolve(String reference) { | |
4151 return resolveUri(Uri.parse(reference)); | |
4152 } | |
4153 | |
4154 Uri resolveUri(Uri reference) { | |
4155 if (reference is _SimpleUri) { | |
4156 return _simpleMerge(this, reference); | |
4157 } | |
4158 return _toNonSimple().resolveUri(reference); | |
4159 } | |
4160 | |
4161 // Merge two simple URIs. This should always result in a prefix of | |
4162 // one concatentated with a suffix of the other, which is again simple. | |
4163 // In a few cases, there might be a need for extra normalization, when | |
4164 // resolving on top of a known scheme. | |
4165 Uri _simpleMerge(_SimpleUri base, _SimpleUri ref) { | |
4166 if (ref.hasScheme) return ref; | |
4167 if (ref.hasAuthority) { | |
4168 if (!base.hasScheme) return ref; | |
4169 bool isSimple = true; | |
4170 if (base._isFile) { | |
4171 isSimple = !ref.hasEmptyPath; | |
4172 } else if (base._isHttp) { | |
4173 isSimple = !ref._isPort("80"); | |
4174 } else if (base._isHttps) { | |
4175 isSimple = !ref._isPort("443"); | |
4176 } | |
4177 if (isSimple) { | |
4178 var delta = base._schemeEnd + 1; | |
4179 var newUri = base._uri.substring(0, base._schemeEnd + 1) + | |
4180 ref._uri.substring(ref._schemeEnd + 1); | |
4181 return new _SimpleUri(newUri, | |
4182 base._schemeEnd, | |
4183 ref._hostStart + delta, | |
4184 ref._portStart + delta, | |
4185 ref._pathStart + delta, | |
4186 ref._queryStart + delta, | |
4187 ref._fragmentStart + delta, | |
4188 base._schemeCache); | |
4189 } else { | |
4190 // This will require normalization, so use the _Uri implementation. | |
4191 return _toNonSimple().resolveUri(ref); | |
4192 } | |
4193 } | |
4194 if (ref.hasEmptyPath) { | |
4195 if (ref.hasQuery) { | |
4196 int delta = base._queryStart - ref._queryStart; | |
4197 var newUri = base._uri.substring(0, base._queryStart) + | |
4198 ref._uri.substring(ref._queryStart); | |
4199 return new _SimpleUri(newUri, | |
4200 base._schemeEnd, | |
4201 base._hostStart, | |
4202 base._portStart, | |
4203 base._pathStart, | |
4204 ref._queryStart + delta, | |
4205 ref._fragmentStart + delta, | |
4206 base._schemeCache); | |
4207 } | |
4208 if (ref.hasFragment) { | |
4209 int delta = base._fragmentStart - ref._fragmentStart; | |
4210 var newUri = base._uri.substring(0, base._fragmentStart) + | |
4211 ref._uri.substring(ref._fragmentStart); | |
4212 return new _SimpleUri(newUri, | |
4213 base._schemeEnd, | |
4214 base._hostStart, | |
4215 base._portStart, | |
4216 base._pathStart, | |
4217 base._queryStart, | |
4218 ref._fragmentStart + delta, | |
4219 base._schemeCache); | |
4220 } | |
4221 return base.removeFragment(); | |
4222 } | |
4223 if (ref.hasAbsolutePath) { | |
4224 var delta = base._pathStart - ref._pathStart; | |
4225 var newUri = base._uri.substring(0, base._pathStart) + | |
4226 ref._uri.substring(ref._pathStart); | |
4227 return new _SimpleUri(newUri, | |
4228 base._schemeEnd, | |
4229 base._hostStart, | |
4230 base._portStart, | |
4231 base._pathStart, | |
4232 ref._queryStart + delta, | |
4233 ref._fragmentStart + delta, | |
4234 base._schemeCache); | |
4235 } | |
4236 if (base.hasEmptyPath && base.hasAuthority) { | |
4237 // ref has relative non-empty path. | |
4238 var delta = base._pathStart - ref._pathStart + 1; | |
4239 var newUri = "${base._uri.substring(0, base._pathStart)}/" | |
4240 "${ref._uri.substring(ref._pathStart)}"; | |
4241 return new _SimpleUri(newUri, | |
4242 base._schemeEnd, | |
4243 base._hostStart, | |
4244 base._portStart, | |
4245 base._pathStart, | |
4246 ref._queryStart + delta, | |
4247 ref._fragmentStart + delta, | |
4248 base._schemeCache); | |
4249 } | |
4250 // Merge paths. | |
4251 if (base._uri.startsWith("../", base._pathStart)) { | |
4252 // Complex rare case, go slow. | |
4253 return _toNonSimple().resolveUri(ref); | |
4254 } | |
4255 | |
4256 // The RFC 3986 algorithm merges the base path without its final segment | |
4257 // (anything after the final "/", or everything if the base path doesn't | |
4258 // contain any "/"), and the reference path. | |
4259 // Then it removes "." and ".." segments using the remove-dot-segment | |
4260 // algorithm. | |
4261 // This code combines the two steps. It is simplified by knowing that | |
4262 // the base path contains no "." or ".." segments, and the reference | |
4263 // path can only contain leading ".." segments. | |
4264 | |
4265 String baseUri = base._uri; | |
4266 String refUri = ref._uri; | |
4267 int baseStart = base._pathStart; | |
4268 int baseEnd = base._queryStart; | |
4269 int refStart = ref._pathStart; | |
4270 int refEnd = ref._queryStart; | |
4271 int backCount = 1; | |
4272 | |
4273 int slashCount = 0; | |
4274 | |
4275 // Count leading ".." segments in reference path. | |
4276 while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) { | |
4277 refStart += 3; | |
4278 backCount += 1; | |
4279 } | |
4280 | |
4281 // Extra slash inserted between base and reference path parts if | |
4282 // the base path contains any slashes. | |
4283 // (We could use a slash from the base path in most cases, but not if | |
4284 // we remove the entire base path). | |
4285 String insert = ""; | |
4286 while (baseEnd > baseStart) { | |
4287 baseEnd--; | |
4288 int char = baseUri.codeUnitAt(baseEnd); | |
4289 if (char == _SLASH) { | |
4290 insert = "/"; | |
4291 backCount--; | |
4292 if (backCount == 0) break; | |
4293 } | |
4294 } | |
4295 | |
4296 var delta = baseEnd - refStart + insert.length; | |
4297 var newUri = "${base._uri.substring(0, baseEnd)}$insert" | |
4298 "${ref._uri.substring(refStart)}"; | |
4299 | |
4300 return new _SimpleUri(newUri, | |
4301 base._schemeEnd, | |
4302 base._hostStart, | |
4303 base._portStart, | |
4304 base._pathStart, | |
4305 ref._queryStart + delta, | |
4306 ref._fragmentStart + delta, | |
4307 base._schemeCache); | |
4308 } | |
4309 | |
4310 String toFilePath({bool windows}) { | |
4311 if (_schemeEnd >= 0 && !_isFile) { | |
4312 throw new UnsupportedError( | |
4313 "Cannot extract a file path from a $scheme URI"); | |
4314 } | |
4315 if (_queryStart < _uri.length) { | |
4316 if (_queryStart < _fragmentStart) { | |
4317 throw new UnsupportedError( | |
4318 "Cannot extract a file path from a URI with a query component"); | |
4319 } | |
4320 throw new UnsupportedError( | |
4321 "Cannot extract a file path from a URI with a fragment component"); | |
4322 } | |
4323 if (windows == null) windows = _Uri._isWindows; | |
4324 return windows ? _Uri._toWindowsFilePath(this) : _toFilePath(); | |
4325 } | |
4326 | |
4327 String _toFilePath() { | |
4328 if (_hostStart < _portStart) { | |
4329 // Has authority and non-empty host. | |
4330 throw new UnsupportedError( | |
4331 "Cannot extract a non-Windows file path from a file URI " | |
4332 "with an authority"); | |
4333 } | |
4334 return this.path; | |
4335 } | |
4336 | |
4337 UriData get data { | |
4338 assert(scheme != "data"); | |
4339 return null; | |
4340 } | |
4341 | |
4342 int get hashCode => _uri.hashCode; | |
4343 | |
4344 bool operator==(Object other) { | |
4345 if (identical(this, other)) return true; | |
4346 if (other is Uri) return _uri == other.toString(); | |
4347 return false; | |
4348 } | |
4349 | |
4350 Uri _toNonSimple() { | |
4351 return new _Uri._internal( | |
4352 this.scheme, | |
4353 this.userInfo, | |
4354 this.hasAuthority ? this.host: null, | |
4355 this.hasPort ? this.port : null, | |
4356 this.path, | |
4357 this.hasQuery ? this.query : null, | |
4358 this.hasFragment ? this.fragment : null | |
4359 ); | |
4360 } | |
4361 | |
4362 String toString() => _uri; | |
4363 } | |
4364 | |
4365 /// Checks whether [text] starts with "data:" at position [start]. | |
4366 /// | |
4367 /// The text must be long enough to allow reading five characters | |
4368 /// from the [start] position. | |
4369 /// | |
4370 /// Returns an integer value which is zero if text starts with all-lowercase | |
4371 /// "data:" and 0x20 if the text starts with "data:" that isn't all lower-case. | |
4372 /// All other values means the text starts with some other character. | |
4373 int _startsWithData(String text, int start) { | |
floitsch
2016/06/29 23:41:47
This should return a bool. (return delta == 0 || d
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Again, I actually need to distinguish the two case
floitsch
2016/06/30 18:15:22
I think that would be better. Even just changing t
| |
4374 // Multiply by 3 to avoid a non-colon character making delta be 0x20. | |
4375 int delta = (text.codeUnitAt(start + 4) ^ _COLON) * 3; | |
4376 delta |= text.codeUnitAt(start) ^ 0x64 /*d*/; | |
4377 delta |= text.codeUnitAt(start + 1) ^ 0x61 /*a*/; | |
4378 delta |= text.codeUnitAt(start + 2) ^ 0x74 /*t*/; | |
4379 delta |= text.codeUnitAt(start + 3) ^ 0x61 /*a*/; | |
4380 return delta; | |
4381 } | |
4382 | |
4383 /// Helper function returning the length of a string, or `0` for `null`. | |
4384 int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | |
OLD | NEW |