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 | |
25 /** | 7 /** |
26 * A parsed URI, such as a URL. | 8 * A parsed URI, such as a URL. |
27 * | 9 * |
28 * **See also:** | 10 * **See also:** |
29 * | 11 * |
30 * * [URIs][uris] in the [library tour][libtour] | 12 * * [URIs][uris] in the [library tour][libtour] |
31 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) | 13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) |
32 * | 14 * |
33 * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris | 15 * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris |
34 * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.ht
ml | 16 * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.ht
ml |
35 */ | 17 */ |
36 abstract class Uri { | 18 class Uri { |
37 /** | 19 /** |
38 * Returns the natural base URI for the current platform. | 20 * The scheme component of the URI. |
39 * | 21 * |
40 * When running in a browser this is the current URL of the current page | 22 * Returns the empty string if there is no scheme component. |
41 * (from `window.location.href`). | |
42 * | 23 * |
43 * When not running in a browser this is the file URI referencing | 24 * A URI scheme is case insensitive. |
44 * the current working directory. | 25 * The returned scheme is canonicalized to lowercase letters. |
45 */ | 26 */ |
46 external static Uri get base; | 27 // We represent the missing scheme as an empty string. |
| 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); |
47 | 89 |
48 /** | 90 /** |
49 * Creates a new URI from its components. | 91 * Creates a new URI from its components. |
50 * | 92 * |
51 * Each component is set through a named argument. Any number of | 93 * Each component is set through a named argument. Any number of |
52 * components can be provided. The [path] and [query] components can be set | 94 * components can be provided. The [path] and [query] components can be set |
53 * using either of two different named arguments. | 95 * using either of two different named arguments. |
54 * | 96 * |
55 * The scheme component is set through [scheme]. The scheme is | 97 * The scheme component is set through [scheme]. The scheme is |
56 * normalized to all lowercase letters. If the scheme is omitted or empty, | 98 * 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... |
109 * use an empty map for `queryParameters`. | 151 * use an empty map for `queryParameters`. |
110 * | 152 * |
111 * If both `query` and `queryParameters` are omitted or `null`, | 153 * If both `query` and `queryParameters` are omitted or `null`, |
112 * the URI has no query part. | 154 * the URI has no query part. |
113 * | 155 * |
114 * The fragment component is set through [fragment]. | 156 * The fragment component is set through [fragment]. |
115 * It should be a valid URI fragment, but invalid characters other than | 157 * It should be a valid URI fragment, but invalid characters other than |
116 * general delimiters, are escaped if necessary. | 158 * general delimiters, are escaped if necessary. |
117 * If `fragment` is omitted or `null`, the URI has no fragment part. | 159 * If `fragment` is omitted or `null`, the URI has no fragment part. |
118 */ | 160 */ |
119 factory Uri({String scheme, | 161 factory Uri({String scheme : "", |
120 String userInfo, | 162 String userInfo : "", |
121 String host, | 163 String host, |
122 int port, | 164 int port, |
123 String path, | 165 String path, |
124 Iterable<String> pathSegments, | 166 Iterable<String> pathSegments, |
125 String query, | 167 String query, |
126 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 168 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
127 String fragment}) = _Uri; | 169 String fragment}) { |
| 170 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); |
| 171 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
| 172 host = _makeHost(host, 0, _stringOrNullLength(host), false); |
| 173 // Special case this constructor for backwards compatibility. |
| 174 if (query == "") query = null; |
| 175 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| 176 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
| 177 port = _makePort(port, scheme); |
| 178 bool isFile = (scheme == "file"); |
| 179 if (host == null && |
| 180 (userInfo.isNotEmpty || port != null || isFile)) { |
| 181 host = ""; |
| 182 } |
| 183 bool hasAuthority = (host != null); |
| 184 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| 185 scheme, hasAuthority); |
| 186 if (scheme.isEmpty && host == null && !path.startsWith('/')) { |
| 187 path = _normalizeRelativePath(path); |
| 188 } else { |
| 189 path = _removeDotSegments(path); |
| 190 } |
| 191 return new Uri._internal(scheme, userInfo, host, port, |
| 192 path, query, fragment); |
| 193 } |
128 | 194 |
129 /** | 195 /** |
130 * Creates a new `http` URI from authority, path and query. | 196 * Creates a new `http` URI from authority, path and query. |
131 * | 197 * |
132 * Examples: | 198 * Examples: |
133 * | 199 * |
134 * ``` | 200 * ``` |
135 * // http://example.org/path?q=dart. | 201 * // http://example.org/path?q=dart. |
136 * new Uri.http("google.com", "/search", { "q" : "dart" }); | 202 * new Uri.http("google.com", "/search", { "q" : "dart" }); |
137 * | 203 * |
(...skipping 16 matching lines...) Expand all Loading... |
154 * | 220 * |
155 * The `path` component is set from the [unencodedPath] | 221 * The `path` component is set from the [unencodedPath] |
156 * argument. The path passed must not be encoded as this constructor | 222 * argument. The path passed must not be encoded as this constructor |
157 * encodes the path. | 223 * encodes the path. |
158 * | 224 * |
159 * The `query` component is set from the optional [queryParameters] | 225 * The `query` component is set from the optional [queryParameters] |
160 * argument. | 226 * argument. |
161 */ | 227 */ |
162 factory Uri.http(String authority, | 228 factory Uri.http(String authority, |
163 String unencodedPath, | 229 String unencodedPath, |
164 [Map<String, String> queryParameters]) = _Uri.http; | 230 [Map<String, String> queryParameters]) { |
| 231 return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
| 232 } |
165 | 233 |
166 /** | 234 /** |
167 * Creates a new `https` URI from authority, path and query. | 235 * Creates a new `https` URI from authority, path and query. |
168 * | 236 * |
169 * This constructor is the same as [Uri.http] except for the scheme | 237 * This constructor is the same as [Uri.http] except for the scheme |
170 * which is set to `https`. | 238 * which is set to `https`. |
171 */ | 239 */ |
172 factory Uri.https(String authority, | 240 factory Uri.https(String authority, |
173 String unencodedPath, | 241 String unencodedPath, |
174 [Map<String, String> queryParameters]) = _Uri.https; | 242 [Map<String, String> queryParameters]) { |
175 | 243 return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
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; | |
307 } | 244 } |
308 | 245 |
309 /** | 246 /** |
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 /** | |
347 * Returns the authority component. | 247 * Returns the authority component. |
348 * | 248 * |
349 * The authority is formatted from the [userInfo], [host] and [port] | 249 * The authority is formatted from the [userInfo], [host] and [port] |
350 * parts. | 250 * parts. |
351 * | 251 * |
352 * Returns the empty string if there is no authority component. | 252 * Returns the empty string if there is no authority component. |
353 */ | 253 */ |
354 String get authority; | 254 String get authority { |
| 255 if (!hasAuthority) return ""; |
| 256 var sb = new StringBuffer(); |
| 257 _writeAuthority(sb); |
| 258 return sb.toString(); |
| 259 } |
355 | 260 |
356 /** | 261 /** |
357 * Returns the user info part of the authority component. | 262 * Returns the user info part of the authority component. |
358 * | 263 * |
359 * Returns the empty string if there is no user info in the | 264 * Returns the empty string if there is no user info in the |
360 * authority component. | 265 * authority component. |
361 */ | 266 */ |
362 String get userInfo; | 267 String get userInfo => _userInfo; |
363 | 268 |
364 /** | 269 /** |
365 * Returns the host part of the authority component. | 270 * Returns the host part of the authority component. |
366 * | 271 * |
367 * Returns the empty string if there is no authority component and | 272 * Returns the empty string if there is no authority component and |
368 * hence no host. | 273 * hence no host. |
369 * | 274 * |
370 * If the host is an IP version 6 address, the surrounding `[` and `]` is | 275 * If the host is an IP version 6 address, the surrounding `[` and `]` is |
371 * removed. | 276 * removed. |
372 * | 277 * |
373 * The host string is case-insensitive. | 278 * The host string is case-insensitive. |
374 * The returned host name is canonicalized to lower-case | 279 * The returned host name is canonicalized to lower-case |
375 * with upper-case percent-escapes. | 280 * with upper-case percent-escapes. |
376 */ | 281 */ |
377 String get host; | 282 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 } |
378 | 289 |
379 /** | 290 /** |
380 * Returns the port part of the authority component. | 291 * Returns the port part of the authority component. |
381 * | 292 * |
382 * Returns the defualt port if there is no port number in the authority | 293 * Returns the defualt port if there is no port number in the authority |
383 * component. That's 80 for http, 443 for https, and 0 for everything else. | 294 * component. That's 80 for http, 443 for https, and 0 for everything else. |
384 */ | 295 */ |
385 int get port; | 296 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 } |
386 | 307 |
387 /** | 308 /** |
388 * Returns the path component. | 309 * Returns the path component. |
389 * | 310 * |
390 * The returned path is encoded. To get direct access to the decoded | 311 * The returned path is encoded. To get direct access to the decoded |
391 * path use [pathSegments]. | 312 * path use [pathSegments]. |
392 * | 313 * |
393 * Returns the empty string if there is no path component. | 314 * Returns the empty string if there is no path component. |
394 */ | 315 */ |
395 String get path; | 316 String get path => _path; |
396 | 317 |
397 /** | 318 /** |
398 * Returns the query component. The returned query is encoded. To get | 319 * Returns the query component. The returned query is encoded. To get |
399 * direct access to the decoded query use [queryParameters]. | 320 * direct access to the decoded query use [queryParameters]. |
400 * | 321 * |
401 * Returns the empty string if there is no query component. | 322 * Returns the empty string if there is no query component. |
402 */ | 323 */ |
403 String get query; | 324 String get query => (_query == null) ? "" : _query; |
404 | 325 |
405 /** | 326 /** |
406 * Returns the fragment identifier component. | 327 * Returns the fragment identifier component. |
407 * | 328 * |
408 * Returns the empty string if there is no fragment identifier | 329 * Returns the empty string if there is no fragment identifier |
409 * component. | 330 * component. |
410 */ | 331 */ |
411 String get fragment; | 332 String get fragment => (_fragment == null) ? "" : _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(); | |
704 | 333 |
705 /** | 334 /** |
706 * Creates a new `Uri` object by parsing a URI string. | 335 * Creates a new `Uri` object by parsing a URI string. |
707 * | 336 * |
708 * If [start] and [end] are provided, only the substring from `start` | 337 * If [start] and [end] are provided, only the substring from `start` |
709 * to `end` is parsed as a URI. | 338 * to `end` is parsed as a URI. |
710 * | 339 * |
711 * If the string is not valid as a URI or URI reference, | 340 * If the string is not valid as a URI or URI reference, |
712 * a [FormatException] is thrown. | 341 * a [FormatException] is thrown. |
713 */ | 342 */ |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
758 // segment = *pchar | 387 // segment = *pchar |
759 // segment-nz = 1*pchar | 388 // segment-nz = 1*pchar |
760 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 389 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
761 // ; non-zero-length segment without any colon ":" | 390 // ; non-zero-length segment without any colon ":" |
762 // | 391 // |
763 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 392 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
764 // | 393 // |
765 // query = *( pchar / "/" / "?" ) | 394 // query = *( pchar / "/" / "?" ) |
766 // | 395 // |
767 // fragment = *( pchar / "/" / "?" ) | 396 // fragment = *( pchar / "/" / "?" ) |
768 end ??= uri.length; | 397 const int EOI = -1; |
769 | 398 |
770 // Special case data:URIs. Ignore case when testing. | 399 String scheme = ""; |
771 if (end >= start + 5) { | 400 String userinfo = ""; |
772 int dataDelta = _startsWithData(uri, start); | 401 String host = null; |
773 if (dataDelta == 0) { | 402 int port = null; |
774 // The case is right. | 403 String path = null; |
775 if (start > 0 || end < uri.length) uri = uri.substring(start, end); | 404 String query = null; |
776 return UriData._parse(uri, 5, null).uri; | 405 String fragment = null; |
777 } else if (dataDelta == 0x20) { | 406 if (end == null) end = uri.length; |
778 return UriData._parse(uri.substring(start + 5, end), 0, null).uri; | 407 |
779 } | 408 int index = start; |
780 // Otherwise the URI doesn't start with "data:" or any case variant of it. | 409 int pathStart = start; |
781 } | 410 // End of input-marker. |
782 | 411 int char = EOI; |
783 // The following index-normalization belongs with the scanning, but is | 412 |
784 // easier to do here because we already have extracted variables from the | 413 void parseAuth() { |
785 // indices list. | 414 if (index == end) { |
786 var indices = new List<int>(8);//new List<int>.filled(8, start - 1); | 415 char = EOI; |
787 | 416 return; |
788 // Set default values for each position. | 417 } |
789 // The value will either be correct in some cases where it isn't set | 418 int authStart = index; |
790 // by the scanner, or it is clearly recognizable as an unset value. | 419 int lastColon = -1; |
791 indices | 420 int lastAt = -1; |
792 ..[0] = 0 | 421 char = uri.codeUnitAt(index); |
793 ..[_schemeEndIndex] = start - 1 | 422 while (index < end) { |
794 ..[_hostStartIndex] = start - 1 | 423 char = uri.codeUnitAt(index); |
795 ..[_notSimpleIndex] = start - 1 | 424 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { |
796 ..[_portStartIndex] = start | 425 break; |
797 ..[_pathStartIndex] = start | 426 } |
798 ..[_queryStartIndex] = end | 427 if (char == _AT_SIGN) { |
799 ..[_fragmentStartIndex] = end; | 428 lastAt = index; |
800 var state = _scan(uri, start, end, _uriStart, indices); | 429 lastColon = -1; |
801 // Some states that should be non-simple, but the URI ended early. | 430 } else if (char == _COLON) { |
802 // Paths that end at a ".." must be normalized to end in "../". | 431 lastColon = index; |
803 if (state >= _nonSimpleEndStates) { | 432 } else if (char == _LEFT_BRACKET) { |
804 indices[_notSimpleIndex] = end; | 433 lastColon = -1; |
805 } | 434 int endBracket = uri.indexOf(']', index + 1); |
806 int schemeEnd = indices[_schemeEndIndex]; | 435 if (endBracket == -1) { |
807 if (schemeEnd >= start) { | 436 index = end; |
808 // Rescan the scheme part now that we know it's not a path. | 437 char = EOI; |
809 state = _scan(uri, start, schemeEnd, _schemeStart, indices); | 438 break; |
810 if (state == _schemeStart) { | 439 } else { |
811 // Empty scheme. | 440 index = endBracket; |
812 indices[_notSimpleIndex] = schemeEnd; | 441 } |
813 } | 442 } |
814 } | 443 index++; |
815 // The returned positions are limited by the scanners ability to write only | 444 char = EOI; |
816 // one position per character, and only the current position. | 445 } |
817 // Scanning from left to right, we only know whether something is a scheme | 446 int hostStart = authStart; |
818 // or a path when we see a `:` or `/`, and likewise we only know if the firs
t | 447 int hostEnd = index; |
819 // `/` is part of the path or is leading an authority component when we see | 448 if (lastAt >= 0) { |
820 // the next character. | 449 userinfo = _makeUserInfo(uri, authStart, lastAt); |
821 | 450 hostStart = lastAt + 1; |
822 int hostStart = indices[_hostStartIndex] + 1; | 451 } |
823 int portStart = indices[_portStartIndex]; | 452 if (lastColon >= 0) { |
824 int pathStart = indices[_pathStartIndex]; | 453 int portNumber; |
825 int queryStart = indices[_queryStartIndex]; | 454 if (lastColon + 1 < index) { |
826 int fragmentStart = indices[_fragmentStartIndex]; | 455 portNumber = 0; |
827 | 456 for (int i = lastColon + 1; i < index; i++) { |
828 // We may discover scheme while handling special cases. | 457 int digit = uri.codeUnitAt(i); |
829 String scheme; | 458 if (_ZERO > digit || _NINE < digit) { |
830 | 459 _fail(uri, i, "Invalid port number"); |
831 // Derive some positions that weren't set to normalize the indices. | 460 } |
832 // If pathStart isn't set (it's before scheme end or host start), then | 461 portNumber = portNumber * 10 + (digit - _ZERO); |
833 // the path is empty. | 462 } |
834 if (fragmentStart < queryStart) queryStart = fragmentStart; | 463 } |
835 if (pathStart < hostStart || pathStart <= schemeEnd) { | 464 port = _makePort(portNumber, scheme); |
836 pathStart = queryStart; | 465 hostEnd = lastColon; |
837 } | 466 } |
838 // If there is an authority with no port, set the port position | 467 host = _makeHost(uri, hostStart, hostEnd, true); |
839 // to be at the end of the authority (equal to pathStart). | 468 if (index < end) { |
840 // This also handles a ":" in a user-info component incorrectly setting | 469 char = uri.codeUnitAt(index); |
841 // the port start position. | 470 } |
842 if (portStart < hostStart) portStart = pathStart; | 471 } |
843 | 472 |
844 assert(hostStart == start || schemeEnd <= hostStart); | 473 // When reaching path parsing, the current character is known to not |
845 assert(hostStart <= portStart); | 474 // be part of the path. |
846 assert(schemeEnd <= pathStart); | 475 const int NOT_IN_PATH = 0; |
847 assert(portStart <= pathStart); | 476 // When reaching path parsing, the current character is part |
848 assert(pathStart <= queryStart); | 477 // of the a non-empty path. |
849 assert(queryStart <= fragmentStart); | 478 const int IN_PATH = 1; |
850 | 479 // When reaching authority parsing, authority is possible. |
851 bool isSimple = indices[_notSimpleIndex] < start; | 480 // This is only true at start or right after scheme. |
852 | 481 const int ALLOW_AUTH = 2; |
853 if (isSimple) { | 482 |
854 // Check/do normalizations that weren't detected by the scanner. | 483 // Current state. |
855 // This includes removal of empty port or userInfo, | 484 // Initialized to the default value that is used when exiting the |
856 // or scheme specific port and path normalizations. | 485 // scheme loop by reaching the end of input. |
857 if (hostStart > schemeEnd + 3) { | 486 // All other breaks set their own state. |
858 // Always be non-simple if URI contains user-info. | 487 int state = NOT_IN_PATH; |
859 // The scanner doesn't set the not-simple position in this case because | 488 int i = index; // Temporary alias for index to avoid bug 19550 in dart2js. |
860 // it's setting the host-start position instead. | 489 while (i < end) { |
861 isSimple = false; | 490 char = uri.codeUnitAt(i); |
862 } else if (portStart > start && portStart + 1 == pathStart) { | 491 if (char == _QUESTION || char == _NUMBER_SIGN) { |
863 // If the port is empty, it should be omitted. | 492 state = NOT_IN_PATH; |
864 // Pathological case, don't bother correcting it. | 493 break; |
865 isSimple = false; | 494 } |
866 } else if (queryStart < end && | 495 if (char == _SLASH) { |
867 (queryStart == pathStart + 2 && | 496 state = (i == start) ? ALLOW_AUTH : IN_PATH; |
868 uri.startsWith("..", pathStart)) || | 497 break; |
869 (queryStart > pathStart + 2 && | 498 } |
870 uri.startsWith("/..", queryStart - 3))) { | 499 if (char == _COLON) { |
871 // The path ends in a ".." segment. This should be normalized to "../". | 500 if (i == start) _fail(uri, start, "Invalid empty scheme"); |
872 // We didn't detect this while scanning because a query or fragment was | 501 scheme = _makeScheme(uri, start, i); |
873 // detected at the same time (which is why we only need to check this | 502 i++; |
874 // if there is something after the path). | 503 if (scheme == "data") { |
875 isSimple = false; | 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 } |
| 509 pathStart = i; |
| 510 if (i == end) { |
| 511 char = EOI; |
| 512 state = NOT_IN_PATH; |
| 513 } else { |
| 514 char = uri.codeUnitAt(i); |
| 515 if (char == _QUESTION || char == _NUMBER_SIGN) { |
| 516 state = NOT_IN_PATH; |
| 517 } else if (char == _SLASH) { |
| 518 state = ALLOW_AUTH; |
| 519 } else { |
| 520 state = IN_PATH; |
| 521 } |
| 522 } |
| 523 break; |
| 524 } |
| 525 i++; |
| 526 char = EOI; |
| 527 } |
| 528 index = i; // Remove alias when bug is fixed. |
| 529 |
| 530 if (state == ALLOW_AUTH) { |
| 531 assert(char == _SLASH); |
| 532 // Have seen one slash either at start or right after scheme. |
| 533 // If two slashes, it's an authority, otherwise it's just the path. |
| 534 index++; |
| 535 if (index == end) { |
| 536 char = EOI; |
| 537 state = NOT_IN_PATH; |
876 } else { | 538 } else { |
877 // There are a few scheme-based normalizations that | 539 char = uri.codeUnitAt(index); |
878 // the scanner couldn't check. | 540 if (char == _SLASH) { |
879 // That means that the input is very close to simple, so just do | 541 index++; |
880 // the normalizations. | 542 parseAuth(); |
881 if (schemeEnd == start + 4) { | 543 pathStart = index; |
882 // Do scheme based normalizations for file, http. | 544 } |
883 if (uri.startsWith("file", start)) { | 545 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { |
884 scheme = "file"; | 546 state = NOT_IN_PATH; |
885 if (hostStart <= start) { | 547 } else { |
886 // File URIs should have an authority. | 548 state = IN_PATH; |
887 // Paths after an authority should be absolute. | 549 } |
888 String schemeAuth = "file://"; | 550 } |
889 int delta = 2; | 551 } |
890 if (!uri.startsWith("/", pathStart)) { | 552 |
891 schemeAuth = "file:///"; | 553 assert(state == IN_PATH || state == NOT_IN_PATH); |
892 delta = 3; | 554 if (state == IN_PATH) { |
893 } | 555 // Characters from pathStart to index (inclusive) are known |
894 uri = schemeAuth + uri.substring(pathStart, end); | 556 // to be part of the path. |
895 schemeEnd -= start; | 557 while (++index < end) { |
896 hostStart = 7; | 558 char = uri.codeUnitAt(index); |
897 portStart = 7; | 559 if (char == _QUESTION || char == _NUMBER_SIGN) { |
898 pathStart = 7; | 560 break; |
899 queryStart += delta - start; | 561 } |
900 fragmentStart += delta - start; | 562 char = EOI; |
901 start = 0; | 563 } |
902 end = uri.length; | 564 state = NOT_IN_PATH; |
903 } else if (pathStart == queryStart) { | 565 } |
904 // Uri has authority and empty path. Add "/" as path. | 566 |
905 if (start == 0 && end == uri.length) { | 567 assert(state == NOT_IN_PATH); |
906 uri = uri.replaceRange(pathStart, queryStart, "/"); | 568 bool hasAuthority = (host != null); |
907 queryStart += 1; | 569 path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); |
908 fragmentStart += 1; | 570 |
909 end += 1; | 571 if (char == _QUESTION) { |
910 } else { | 572 int numberSignIndex = -1; |
911 uri = "${uri.substring(start, pathStart)}/" | 573 for (int i = index + 1; i < end; i++) { |
912 "${uri.substring(queryStart, end)}"; | 574 if (uri.codeUnitAt(i) == _NUMBER_SIGN) { |
913 schemeEnd -= start; | 575 numberSignIndex = i; |
914 hostStart -= start; | 576 break; |
915 portStart -= start; | 577 } |
916 pathStart -= start; | 578 } |
917 queryStart += 1 - start; | 579 if (numberSignIndex < 0) { |
918 fragmentStart += 1 - start; | 580 query = _makeQuery(uri, index + 1, end, null); |
919 start = 0; | 581 } else { |
920 end = uri.length; | 582 query = _makeQuery(uri, index + 1, numberSignIndex, null); |
921 } | 583 fragment = _makeFragment(uri, numberSignIndex + 1, end); |
922 } | 584 } |
923 } else if (uri.startsWith("http", start)) { | 585 } else if (char == _NUMBER_SIGN) { |
924 scheme = "http"; | 586 fragment = _makeFragment(uri, index + 1, end); |
925 // HTTP URIs should not have an explicit port of 80. | 587 } |
926 if (portStart > start && portStart + 3 == pathStart && | 588 return new Uri._internal(scheme, |
927 uri.startsWith("80", portStart + 1)) { | 589 userinfo, |
928 if (start == 0 && end == uri.length) { | 590 host, |
929 uri = uri.replaceRange(portStart, pathStart, ""); | 591 port, |
930 pathStart -= 3; | 592 path, |
931 queryStart -= 3; | 593 query, |
932 fragmentStart -= 3; | 594 fragment); |
933 end -= 3; | |
934 } else { | |
935 uri = uri.substring(start, portStart) + | |
936 uri.substring(pathStart, end); | |
937 schemeEnd -= start; | |
938 hostStart -= start; | |
939 portStart -= start; | |
940 pathStart -= 3 + start; | |
941 queryStart -= 3 + start; | |
942 fragmentStart -= 3 + start; | |
943 start = 0; | |
944 end = uri.length; | |
945 } | |
946 } | |
947 } | |
948 } else if (schemeEnd == start + 5 && uri.startsWith("https", start)) { | |
949 scheme = "https"; | |
950 // HTTPS URIs should not have an explicit port of 443. | |
951 if (portStart > start && portStart + 4 == pathStart && | |
952 uri.startsWith("443", portStart + 1)) { | |
953 if (start == 0 && end == uri.length) { | |
954 uri = uri.replaceRange(portStart, pathStart, ""); | |
955 pathStart -= 4; | |
956 queryStart -= 4; | |
957 fragmentStart -= 4; | |
958 end -= 3; | |
959 } else { | |
960 uri = uri.substring(start, portStart) + | |
961 uri.substring(pathStart, end); | |
962 schemeEnd -= start; | |
963 hostStart -= start; | |
964 portStart -= start; | |
965 pathStart -= 4 + start; | |
966 queryStart -= 4 + start; | |
967 fragmentStart -= 4 + start; | |
968 start = 0; | |
969 end = uri.length; | |
970 } | |
971 } | |
972 } | |
973 } | |
974 } | |
975 | |
976 if (isSimple) { | |
977 if (start > 0 || end < uri.length) { | |
978 uri = uri.substring(start, end); | |
979 schemeEnd -= start; | |
980 hostStart -= start; | |
981 portStart -= start; | |
982 pathStart -= start; | |
983 queryStart -= start; | |
984 fragmentStart -= start; | |
985 } | |
986 return new _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, | |
987 queryStart, fragmentStart, scheme); | |
988 | |
989 } | |
990 | |
991 return new _Uri.notSimple(uri, start, end, schemeEnd, hostStart, portStart, | |
992 pathStart, queryStart, fragmentStart, scheme); | |
993 } | 595 } |
994 | 596 |
995 /** | |
996 * Encode the string [component] using percent-encoding to make it | |
997 * safe for literal use as a URI component. | |
998 * | |
999 * All characters except uppercase and lowercase letters, digits and | |
1000 * the characters `-_.!~*'()` are percent-encoded. This is the | |
1001 * set of characters specified in RFC 2396 and the which is | |
1002 * specified for the encodeUriComponent in ECMA-262 version 5.1. | |
1003 * | |
1004 * When manually encoding path segments or query components remember | |
1005 * to encode each part separately before building the path or query | |
1006 * string. | |
1007 * | |
1008 * For encoding the query part consider using | |
1009 * [encodeQueryComponent]. | |
1010 * | |
1011 * To avoid the need for explicitly encoding use the [pathSegments] | |
1012 * and [queryParameters] optional named arguments when constructing | |
1013 * a [Uri]. | |
1014 */ | |
1015 static String encodeComponent(String component) { | |
1016 return _Uri._uriEncode(_Uri._unreserved2396Table, component, UTF8, false); | |
1017 } | |
1018 | |
1019 /** | |
1020 * Encode the string [component] according to the HTML 4.01 rules | |
1021 * for encoding the posting of a HTML form as a query string | |
1022 * component. | |
1023 * | |
1024 * Encode the string [component] according to the HTML 4.01 rules | |
1025 * for encoding the posting of a HTML form as a query string | |
1026 * component. | |
1027 | |
1028 * The component is first encoded to bytes using [encoding]. | |
1029 * The default is to use [UTF8] encoding, which preserves all | |
1030 * the characters that don't need encoding. | |
1031 | |
1032 * Then the resulting bytes are "percent-encoded". This transforms | |
1033 * spaces (U+0020) to a plus sign ('+') and all bytes that are not | |
1034 * the ASCII decimal digits, letters or one of '-._~' are written as | |
1035 * a percent sign '%' followed by the two-digit hexadecimal | |
1036 * representation of the byte. | |
1037 | |
1038 * Note that the set of characters which are percent-encoded is a | |
1039 * superset of what HTML 4.01 requires, since it refers to RFC 1738 | |
1040 * for reserved characters. | |
1041 * | |
1042 * When manually encoding query components remember to encode each | |
1043 * part separately before building the query string. | |
1044 * | |
1045 * To avoid the need for explicitly encoding the query use the | |
1046 * [queryParameters] optional named arguments when constructing a | |
1047 * [Uri]. | |
1048 * | |
1049 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | |
1050 * details. | |
1051 */ | |
1052 static String encodeQueryComponent(String component, | |
1053 {Encoding encoding: UTF8}) { | |
1054 return _Uri._uriEncode(_Uri._unreservedTable, component, encoding, true); | |
1055 } | |
1056 | |
1057 /** | |
1058 * Decodes the percent-encoding in [encodedComponent]. | |
1059 * | |
1060 * Note that decoding a URI component might change its meaning as | |
1061 * some of the decoded characters could be characters with are | |
1062 * delimiters for a given URI componene type. Always split a URI | |
1063 * component using the delimiters for the component before decoding | |
1064 * the individual parts. | |
1065 * | |
1066 * For handling the [path] and [query] components consider using | |
1067 * [pathSegments] and [queryParameters] to get the separated and | |
1068 * decoded component. | |
1069 */ | |
1070 static String decodeComponent(String encodedComponent) { | |
1071 return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | |
1072 UTF8, false); | |
1073 } | |
1074 | |
1075 /** | |
1076 * Decodes the percent-encoding in [encodedComponent], converting | |
1077 * pluses to spaces. | |
1078 * | |
1079 * It will create a byte-list of the decoded characters, and then use | |
1080 * [encoding] to decode the byte-list to a String. The default encoding is | |
1081 * UTF-8. | |
1082 */ | |
1083 static String decodeQueryComponent( | |
1084 String encodedComponent, | |
1085 {Encoding encoding: UTF8}) { | |
1086 return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, | |
1087 encoding, true); | |
1088 } | |
1089 | |
1090 /** | |
1091 * Encode the string [uri] using percent-encoding to make it | |
1092 * safe for literal use as a full URI. | |
1093 * | |
1094 * All characters except uppercase and lowercase letters, digits and | |
1095 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | |
1096 * is the set of characters specified in in ECMA-262 version 5.1 for | |
1097 * the encodeURI function . | |
1098 */ | |
1099 static String encodeFull(String uri) { | |
1100 return _Uri._uriEncode(_Uri._encodeFullTable, uri, UTF8, false); | |
1101 } | |
1102 | |
1103 /** | |
1104 * Decodes the percent-encoding in [uri]. | |
1105 * | |
1106 * Note that decoding a full URI might change its meaning as some of | |
1107 * the decoded characters could be reserved characters. In most | |
1108 * cases an encoded URI should be parsed into components using | |
1109 * [Uri.parse] before decoding the separate components. | |
1110 */ | |
1111 static String decodeFull(String uri) { | |
1112 return _Uri._uriDecode(uri, 0, uri.length, UTF8, false); | |
1113 } | |
1114 | |
1115 /** | |
1116 * Returns the [query] split into a map according to the rules | |
1117 * specified for FORM post in the [HTML 4.01 specification section | |
1118 * 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"). | |
1119 * Each key and value in the returned map has been decoded. If the [query] | |
1120 * is the empty string an empty map is returned. | |
1121 * | |
1122 * Keys in the query string that have no value are mapped to the | |
1123 * empty string. | |
1124 * | |
1125 * Each query component will be decoded using [encoding]. The default encoding | |
1126 * is UTF-8. | |
1127 */ | |
1128 static Map<String, String> splitQueryString(String query, | |
1129 {Encoding encoding: UTF8}) { | |
1130 return query.split("&").fold({}, (map, element) { | |
1131 int index = element.indexOf("="); | |
1132 if (index == -1) { | |
1133 if (element != "") { | |
1134 map[decodeQueryComponent(element, encoding: encoding)] = ""; | |
1135 } | |
1136 } else if (index != 0) { | |
1137 var key = element.substring(0, index); | |
1138 var value = element.substring(index + 1); | |
1139 map[decodeQueryComponent(key, encoding: encoding)] = | |
1140 decodeQueryComponent(value, encoding: encoding); | |
1141 } | |
1142 return map; | |
1143 }); | |
1144 } | |
1145 | |
1146 | |
1147 /** | |
1148 * Parse the [host] as an IP version 4 (IPv4) address, returning the address | |
1149 * as a list of 4 bytes in network byte order (big endian). | |
1150 * | |
1151 * Throws a [FormatException] if [host] is not a valid IPv4 address | |
1152 * representation. | |
1153 */ | |
1154 static List<int> parseIPv4Address(String host) => | |
1155 _parseIPv4Address(host, 0, host.length); | |
1156 | |
1157 /// Implementation of [parseIPv4Address] that can work on a substring. | |
1158 static List<int> _parseIPv4Address(String host, int start, int end) { | |
1159 void error(String msg, int position) { | |
1160 throw new FormatException('Illegal IPv4 address, $msg', host, position); | |
1161 } | |
1162 | |
1163 var result = new Uint8List(4); | |
1164 int partIndex = 0; | |
1165 int partStart = start; | |
1166 for (int i = start; i < end; i++) { | |
1167 int char = host.codeUnitAt(i); | |
1168 if (char != _DOT) { | |
1169 if (char ^ 0x30 > 9) { | |
1170 // Fail on a non-digit character. | |
1171 error("invalid character", i); | |
1172 } | |
1173 } else { | |
1174 if (partIndex == 3) { | |
1175 error('IPv4 address should contain exactly 4 parts', i); | |
1176 } | |
1177 int part = int.parse(host.substring(partStart, i)); | |
1178 if (part > 255) { | |
1179 error("each part must be in the range 0..255", partStart); | |
1180 } | |
1181 result[partIndex++] = part; | |
1182 partStart = i + 1; | |
1183 } | |
1184 } | |
1185 | |
1186 if (partIndex != 3) { | |
1187 error('IPv4 address should contain exactly 4 parts', end); | |
1188 } | |
1189 | |
1190 int part = int.parse(host.substring(partStart, end)); | |
1191 if (part > 255) { | |
1192 error("each part must be in the range 0..255", partStart); | |
1193 } | |
1194 result[partIndex] = part; | |
1195 | |
1196 return result; | |
1197 } | |
1198 | |
1199 /** | |
1200 * Parse the [host] as an IP version 6 (IPv6) address, returning the address | |
1201 * as a list of 16 bytes in network byte order (big endian). | |
1202 * | |
1203 * Throws a [FormatException] if [host] is not a valid IPv6 address | |
1204 * representation. | |
1205 * | |
1206 * Acts on the substring from [start] to [end]. If [end] is omitted, it | |
1207 * defaults ot the end of the string. | |
1208 * | |
1209 * Some examples of IPv6 addresses: | |
1210 * * ::1 | |
1211 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | |
1212 * * 3ffe:2a00:100:7031::1 | |
1213 * * ::FFFF:129.144.52.38 | |
1214 * * 2010:836B:4179::836B:4179 | |
1215 */ | |
1216 static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | |
1217 if (end == null) end = host.length; | |
1218 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, separated | |
1219 // by `:`'s, with the following exceptions: | |
1220 // | |
1221 // - One (and only one) wildcard (`::`) may be present, representing a fill | |
1222 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. | |
1223 // - The last two parts may be replaced by an IPv4 "dotted-quad" address. | |
1224 | |
1225 // Helper function for reporting a badly formatted IPv6 address. | |
1226 void error(String msg, [position]) { | |
1227 throw new FormatException('Illegal IPv6 address, $msg', host, position); | |
1228 } | |
1229 | |
1230 // Parse a hex block. | |
1231 int parseHex(int start, int end) { | |
1232 if (end - start > 4) { | |
1233 error('an IPv6 part can only contain a maximum of 4 hex digits', start); | |
1234 } | |
1235 int value = int.parse(host.substring(start, end), radix: 16); | |
1236 if (value < 0 || value > 0xFFFF) { | |
1237 error('each part must be in the range of `0x0..0xFFFF`', start); | |
1238 } | |
1239 return value; | |
1240 } | |
1241 | |
1242 if (host.length < 2) error('address is too short'); | |
1243 List<int> parts = []; | |
1244 bool wildcardSeen = false; | |
1245 // Set if seeing a ".", suggesting that there is an IPv4 address. | |
1246 bool seenDot = false; | |
1247 int partStart = start; | |
1248 // Parse all parts, except a potential last one. | |
1249 for (int i = start; i < end; i++) { | |
1250 int char = host.codeUnitAt(i); | |
1251 if (char == _COLON) { | |
1252 if (i == start) { | |
1253 // If we see a `:` in the beginning, expect wildcard. | |
1254 i++; | |
1255 if (host.codeUnitAt(i) != _COLON) { | |
1256 error('invalid start colon.', i); | |
1257 } | |
1258 partStart = i; | |
1259 } | |
1260 if (i == partStart) { | |
1261 // Wildcard. We only allow one. | |
1262 if (wildcardSeen) { | |
1263 error('only one wildcard `::` is allowed', i); | |
1264 } | |
1265 wildcardSeen = true; | |
1266 parts.add(-1); | |
1267 } else { | |
1268 // Found a single colon. Parse [partStart..i] as a hex entry. | |
1269 parts.add(parseHex(partStart, i)); | |
1270 } | |
1271 partStart = i + 1; | |
1272 } else if (char == _DOT) { | |
1273 seenDot = true; | |
1274 } | |
1275 } | |
1276 if (parts.length == 0) error('too few parts'); | |
1277 bool atEnd = (partStart == end); | |
1278 bool isLastWildcard = (parts.last == -1); | |
1279 if (atEnd && !isLastWildcard) { | |
1280 error('expected a part after last `:`', end); | |
1281 } | |
1282 if (!atEnd) { | |
1283 if (!seenDot) { | |
1284 parts.add(parseHex(partStart, end)); | |
1285 } else { | |
1286 List<int> last = _parseIPv4Address(host, partStart, end); | |
1287 parts.add(last[0] << 8 | last[1]); | |
1288 parts.add(last[2] << 8 | last[3]); | |
1289 } | |
1290 } | |
1291 if (wildcardSeen) { | |
1292 if (parts.length > 7) { | |
1293 error('an address with a wildcard must have less than 7 parts'); | |
1294 } | |
1295 } else if (parts.length != 8) { | |
1296 error('an address without a wildcard must contain exactly 8 parts'); | |
1297 } | |
1298 List<int> bytes = new Uint8List(16); | |
1299 for (int i = 0, index = 0; i < parts.length; i++) { | |
1300 int value = parts[i]; | |
1301 if (value == -1) { | |
1302 int wildCardLength = 9 - parts.length; | |
1303 for (int j = 0; j < wildCardLength; j++) { | |
1304 bytes[index] = 0; | |
1305 bytes[index + 1] = 0; | |
1306 index += 2; | |
1307 } | |
1308 } else { | |
1309 bytes[index] = value >> 8; | |
1310 bytes[index + 1] = value & 0xff; | |
1311 index += 2; | |
1312 } | |
1313 } | |
1314 return bytes; | |
1315 } | |
1316 } | |
1317 | |
1318 class _Uri implements Uri { | |
1319 // We represent the missing scheme as an empty string. | |
1320 // A valid scheme cannot be empty. | |
1321 final String scheme; | |
1322 | |
1323 /** | |
1324 * The user-info part of the authority. | |
1325 * | |
1326 * Does not distinguish between an empty user-info and an absent one. | |
1327 * The value is always non-null. | |
1328 * Is considered absent if [_host] is `null`. | |
1329 */ | |
1330 final String _userInfo; | |
1331 | |
1332 /** | |
1333 * The host name of the URI. | |
1334 * | |
1335 * Set to `null` if there is no authority in the URI. | |
1336 * The host name is the only mandatory part of an authority, so we use | |
1337 * it to mark whether an authority part was present or not. | |
1338 */ | |
1339 final String _host; | |
1340 | |
1341 /** | |
1342 * The port number part of the authority. | |
1343 * | |
1344 * The port. Set to null if there is no port. Normalized to null if | |
1345 * the port is the default port for the scheme. | |
1346 */ | |
1347 int _port; | |
1348 | |
1349 /** | |
1350 * The path of the URI. | |
1351 * | |
1352 * Always non-null. | |
1353 */ | |
1354 String _path; | |
1355 | |
1356 // The query content, or null if there is no query. | |
1357 final String _query; | |
1358 | |
1359 // The fragment content, or null if there is no fragment. | |
1360 final String _fragment; | |
1361 | |
1362 /** | |
1363 * Cache the computed return value of [pathSegements]. | |
1364 */ | |
1365 List<String> _pathSegments; | |
1366 | |
1367 /** | |
1368 * Cache of the full normalized text representation of the URI. | |
1369 */ | |
1370 String _text; | |
1371 | |
1372 /** | |
1373 * Cache of the hashCode of [_text]. | |
1374 * | |
1375 * Is null until computed. | |
1376 */ | |
1377 int _hashCodeCache; | |
1378 | |
1379 /** | |
1380 * Cache the computed return value of [queryParameters]. | |
1381 */ | |
1382 Map<String, String> _queryParameters; | |
1383 Map<String, List<String>> _queryParameterLists; | |
1384 | |
1385 /// Internal non-verifying constructor. Only call with validated arguments. | |
1386 _Uri._internal(this.scheme, | |
1387 this._userInfo, | |
1388 this._host, | |
1389 this._port, | |
1390 this._path, | |
1391 this._query, | |
1392 this._fragment); | |
1393 | |
1394 /// Create a [_Uri] from parts of [uri]. | |
1395 /// | |
1396 /// The parameters specify the start/end of particular components of the URI. | |
1397 /// The [scheme] may contain a string representing a normalized scheme | |
1398 /// component if one has already been discovered. | |
1399 factory _Uri.notSimple(String uri, int start, int end, int schemeEnd, | |
1400 int hostStart, int portStart, int pathStart, | |
1401 int queryStart, int fragmentStart, String scheme) { | |
1402 if (scheme == null) { | |
1403 scheme = ""; | |
1404 if (schemeEnd > start) { | |
1405 scheme = _makeScheme(uri, start, schemeEnd); | |
1406 } else if (schemeEnd == start) { | |
1407 _fail(uri, start, "Invalid empty scheme"); | |
1408 } | |
1409 } | |
1410 String userInfo = ""; | |
1411 String host; | |
1412 int port; | |
1413 if (hostStart > start) { | |
1414 int userInfoStart = schemeEnd + 3; | |
1415 if (userInfoStart < hostStart) { | |
1416 userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1); | |
1417 } | |
1418 host = _makeHost(uri, hostStart, portStart, false); | |
1419 if (portStart + 1 < pathStart) { | |
1420 // Should throw because invalid. | |
1421 port = int.parse(uri.substring(portStart + 1, pathStart), onError: (_) { | |
1422 throw new FormatException("Invalid port", uri, portStart + 1); | |
1423 }); | |
1424 port = _makePort(port, scheme); | |
1425 } | |
1426 } | |
1427 String path = _makePath(uri, pathStart, queryStart, null, | |
1428 scheme, host != null); | |
1429 String query; | |
1430 if (queryStart < fragmentStart) { | |
1431 query = _makeQuery(uri, queryStart + 1, fragmentStart, null); | |
1432 } | |
1433 String fragment; | |
1434 if (fragmentStart < end) { | |
1435 fragment = _makeFragment(uri, fragmentStart + 1, end); | |
1436 } | |
1437 return new _Uri._internal(scheme, | |
1438 userInfo, | |
1439 host, | |
1440 port, | |
1441 path, | |
1442 query, | |
1443 fragment); | |
1444 } | |
1445 | |
1446 /// Implementation of [Uri.Uri]. | |
1447 factory _Uri({String scheme, | |
1448 String userInfo, | |
1449 String host, | |
1450 int port, | |
1451 String path, | |
1452 Iterable<String> pathSegments, | |
1453 String query, | |
1454 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | |
1455 String fragment}) { | |
1456 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | |
1457 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
1458 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
1459 // Special case this constructor for backwards compatibility. | |
1460 if (query == "") query = null; | |
1461 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
1462 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
1463 port = _makePort(port, scheme); | |
1464 bool isFile = (scheme == "file"); | |
1465 if (host == null && | |
1466 (userInfo.isNotEmpty || port != null || isFile)) { | |
1467 host = ""; | |
1468 } | |
1469 bool hasAuthority = (host != null); | |
1470 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
1471 scheme, hasAuthority); | |
1472 if (scheme.isEmpty && host == null && !path.startsWith('/')) { | |
1473 path = _normalizeRelativePath(path); | |
1474 } else { | |
1475 path = _removeDotSegments(path); | |
1476 } | |
1477 return new _Uri._internal(scheme, userInfo, host, port, | |
1478 path, query, fragment); | |
1479 } | |
1480 | |
1481 /// Implementation of [Uri.http]. | |
1482 factory _Uri.http(String authority, | |
1483 String unencodedPath, | |
1484 [Map<String, String> queryParameters]) { | |
1485 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
1486 } | |
1487 | |
1488 /// Implementation of [Uri.https]. | |
1489 factory _Uri.https(String authority, | |
1490 String unencodedPath, | |
1491 [Map<String, String> queryParameters]) { | |
1492 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | |
1493 } | |
1494 | |
1495 String get authority { | |
1496 if (!hasAuthority) return ""; | |
1497 var sb = new StringBuffer(); | |
1498 _writeAuthority(sb); | |
1499 return sb.toString(); | |
1500 } | |
1501 | |
1502 String get userInfo => _userInfo; | |
1503 | |
1504 String get host { | |
1505 if (_host == null) return ""; | |
1506 if (_host.startsWith('[')) { | |
1507 return _host.substring(1, _host.length - 1); | |
1508 } | |
1509 return _host; | |
1510 } | |
1511 | |
1512 int get port { | |
1513 if (_port == null) return _defaultPort(scheme); | |
1514 return _port; | |
1515 } | |
1516 | |
1517 // The default port for the scheme of this Uri. | |
1518 static int _defaultPort(String scheme) { | |
1519 if (scheme == "http") return 80; | |
1520 if (scheme == "https") return 443; | |
1521 return 0; | |
1522 } | |
1523 | |
1524 String get path => _path; | |
1525 | |
1526 String get query => _query ?? ""; | |
1527 | |
1528 String get fragment => _fragment ?? ""; | |
1529 | |
1530 // Report a parse failure. | 597 // Report a parse failure. |
1531 static void _fail(String uri, int index, String message) { | 598 static void _fail(String uri, int index, String message) { |
1532 throw new FormatException(message, uri, index); | 599 throw new FormatException(message, uri, index); |
1533 } | 600 } |
1534 | 601 |
1535 static Uri _makeHttpUri(String scheme, | 602 static Uri _makeHttpUri(String scheme, |
1536 String authority, | 603 String authority, |
1537 String unencodedPath, | 604 String unencodedPath, |
1538 Map<String, String> queryParameters) { | 605 Map<String, String> queryParameters) { |
1539 var userInfo = ""; | 606 var userInfo = ""; |
1540 var host = null; | 607 var host = null; |
1541 var port = null; | 608 var port = null; |
1542 | 609 |
1543 if (authority != null && authority.isNotEmpty) { | 610 if (authority != null && authority.isNotEmpty) { |
1544 var hostStart = 0; | 611 var hostStart = 0; |
1545 // Split off the user info. | 612 // Split off the user info. |
1546 bool hasUserInfo = false; | 613 bool hasUserInfo = false; |
1547 for (int i = 0; i < authority.length; i++) { | 614 for (int i = 0; i < authority.length; i++) { |
1548 const int atSign = 0x40; | 615 if (authority.codeUnitAt(i) == _AT_SIGN) { |
1549 if (authority.codeUnitAt(i) == atSign) { | |
1550 hasUserInfo = true; | 616 hasUserInfo = true; |
1551 userInfo = authority.substring(0, i); | 617 userInfo = authority.substring(0, i); |
1552 hostStart = i + 1; | 618 hostStart = i + 1; |
1553 break; | 619 break; |
1554 } | 620 } |
1555 } | 621 } |
1556 var hostEnd = hostStart; | 622 var hostEnd = hostStart; |
1557 if (hostStart < authority.length && | 623 if (hostStart < authority.length && |
1558 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | 624 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { |
1559 // IPv6 host. | 625 // IPv6 host. |
1560 for (; hostEnd < authority.length; hostEnd++) { | 626 for (; hostEnd < authority.length; hostEnd++) { |
1561 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | 627 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; |
1562 } | 628 } |
1563 if (hostEnd == authority.length) { | 629 if (hostEnd == authority.length) { |
1564 throw new FormatException("Invalid IPv6 host entry.", | 630 throw new FormatException("Invalid IPv6 host entry.", |
1565 authority, hostStart); | 631 authority, hostStart); |
1566 } | 632 } |
1567 Uri.parseIPv6Address(authority, hostStart + 1, hostEnd); | 633 parseIPv6Address(authority, hostStart + 1, hostEnd); |
1568 hostEnd++; // Skip the closing bracket. | 634 hostEnd++; // Skip the closing bracket. |
1569 if (hostEnd != authority.length && | 635 if (hostEnd != authority.length && |
1570 authority.codeUnitAt(hostEnd) != _COLON) { | 636 authority.codeUnitAt(hostEnd) != _COLON) { |
1571 throw new FormatException("Invalid end of authority", | 637 throw new FormatException("Invalid end of authority", |
1572 authority, hostEnd); | 638 authority, hostEnd); |
1573 } | 639 } |
1574 } | 640 } |
1575 // Split host and port. | 641 // Split host and port. |
1576 bool hasPort = false; | 642 bool hasPort = false; |
1577 for (; hostEnd < authority.length; hostEnd++) { | 643 for (; hostEnd < authority.length; hostEnd++) { |
1578 if (authority.codeUnitAt(hostEnd) == _COLON) { | 644 if (authority.codeUnitAt(hostEnd) == _COLON) { |
1579 var portString = authority.substring(hostEnd + 1); | 645 var portString = authority.substring(hostEnd + 1); |
1580 // We allow the empty port - falling back to initial value. | 646 // We allow the empty port - falling back to initial value. |
1581 if (portString.isNotEmpty) port = int.parse(portString); | 647 if (portString.isNotEmpty) port = int.parse(portString); |
1582 break; | 648 break; |
1583 } | 649 } |
1584 } | 650 } |
1585 host = authority.substring(hostStart, hostEnd); | 651 host = authority.substring(hostStart, hostEnd); |
1586 } | 652 } |
1587 return new Uri(scheme: scheme, | 653 return new Uri(scheme: scheme, |
1588 userInfo: userInfo, | 654 userInfo: userInfo, |
1589 host: host, | 655 host: host, |
1590 port: port, | 656 port: port, |
1591 pathSegments: unencodedPath.split("/"), | 657 pathSegments: unencodedPath.split("/"), |
1592 queryParameters: queryParameters); | 658 queryParameters: queryParameters); |
1593 } | 659 } |
1594 | 660 |
1595 /// Implementation of [Uri.file]. | 661 /** |
1596 factory _Uri.file(String path, {bool windows}) { | 662 * Creates a new file URI from an absolute or relative file path. |
1597 windows = (windows == null) ? _Uri._isWindows : windows; | 663 * |
| 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; |
1598 return windows ? _makeWindowsFileUrl(path, false) | 744 return windows ? _makeWindowsFileUrl(path, false) |
1599 : _makeFileUri(path, false); | 745 : _makeFileUri(path, false); |
1600 } | 746 } |
1601 | 747 |
1602 /// Implementation of [Uri.directory]. | 748 /** |
1603 factory _Uri.directory(String path, {bool windows}) { | 749 * Like [Uri.file] except that a non-empty URI path ends in a slash. |
1604 windows = (windows == null) ? _Uri._isWindows : windows; | 750 * |
| 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; |
1605 return windows ? _makeWindowsFileUrl(path, true) | 757 return windows ? _makeWindowsFileUrl(path, true) |
1606 : _makeFileUri(path, true); | 758 : _makeFileUri(path, true); |
1607 } | 759 } |
1608 | 760 |
| 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 } |
1609 | 801 |
1610 /// Used internally in path-related constructors. | 802 /** |
| 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 |
1611 external static bool get _isWindows; | 840 external static bool get _isWindows; |
1612 | 841 |
1613 static _checkNonWindowsPathReservedCharacters(List<String> segments, | 842 static _checkNonWindowsPathReservedCharacters(List<String> segments, |
1614 bool argumentError) { | 843 bool argumentError) { |
1615 segments.forEach((segment) { | 844 segments.forEach((segment) { |
1616 if (segment.contains("/")) { | 845 if (segment.contains("/")) { |
1617 if (argumentError) { | 846 if (argumentError) { |
1618 throw new ArgumentError("Illegal path character $segment"); | 847 throw new ArgumentError("Illegal path character $segment"); |
1619 } else { | 848 } else { |
1620 throw new UnsupportedError("Illegal path character $segment"); | 849 throw new UnsupportedError("Illegal path character $segment"); |
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1733 _checkWindowsPathReservedCharacters(pathSegments, true); | 962 _checkWindowsPathReservedCharacters(pathSegments, true); |
1734 if (slashTerminated && | 963 if (slashTerminated && |
1735 pathSegments.isNotEmpty && | 964 pathSegments.isNotEmpty && |
1736 pathSegments.last.isNotEmpty) { | 965 pathSegments.last.isNotEmpty) { |
1737 pathSegments.add(""); // Extra separator at end. | 966 pathSegments.add(""); // Extra separator at end. |
1738 } | 967 } |
1739 return new Uri(pathSegments: pathSegments); | 968 return new Uri(pathSegments: pathSegments); |
1740 } | 969 } |
1741 } | 970 } |
1742 | 971 |
| 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 */ |
1743 Uri replace({String scheme, | 1012 Uri replace({String scheme, |
1744 String userInfo, | 1013 String userInfo, |
1745 String host, | 1014 String host, |
1746 int port, | 1015 int port, |
1747 String path, | 1016 String path, |
1748 Iterable<String> pathSegments, | 1017 Iterable<String> pathSegments, |
1749 String query, | 1018 String query, |
1750 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | 1019 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
1751 String fragment}) { | 1020 String fragment}) { |
1752 // Set to true if the scheme has (potentially) changed. | 1021 // Set to true if the scheme has (potentially) changed. |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1799 } else { | 1068 } else { |
1800 query = this._query; | 1069 query = this._query; |
1801 } | 1070 } |
1802 | 1071 |
1803 if (fragment != null) { | 1072 if (fragment != null) { |
1804 fragment = _makeFragment(fragment, 0, fragment.length); | 1073 fragment = _makeFragment(fragment, 0, fragment.length); |
1805 } else { | 1074 } else { |
1806 fragment = this._fragment; | 1075 fragment = this._fragment; |
1807 } | 1076 } |
1808 | 1077 |
1809 return new _Uri._internal( | 1078 return new Uri._internal( |
1810 scheme, userInfo, host, port, path, query, fragment); | 1079 scheme, userInfo, host, port, path, query, fragment); |
1811 } | 1080 } |
1812 | 1081 |
| 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 */ |
1813 Uri removeFragment() { | 1087 Uri removeFragment() { |
1814 if (!this.hasFragment) return this; | 1088 if (!this.hasFragment) return this; |
1815 return new _Uri._internal(scheme, _userInfo, _host, _port, | 1089 return new Uri._internal(scheme, _userInfo, _host, _port, |
1816 _path, _query, null); | 1090 _path, _query, null); |
1817 } | 1091 } |
1818 | 1092 |
| 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 */ |
1819 List<String> get pathSegments { | 1101 List<String> get pathSegments { |
1820 var result = _pathSegments; | 1102 var result = _pathSegments; |
1821 if (result != null) return result; | 1103 if (result != null) return result; |
1822 | 1104 |
1823 var pathToSplit = path; | 1105 var pathToSplit = path; |
1824 if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { | 1106 if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { |
1825 pathToSplit = pathToSplit.substring(1); | 1107 pathToSplit = pathToSplit.substring(1); |
1826 } | 1108 } |
1827 result = (pathToSplit == "") | 1109 result = (pathToSplit == "") |
1828 ? const<String>[] | 1110 ? const<String>[] |
1829 : new List<String>.unmodifiable( | 1111 : new List<String>.unmodifiable( |
1830 pathToSplit.split("/").map(Uri.decodeComponent)); | 1112 pathToSplit.split("/").map(Uri.decodeComponent)); |
1831 _pathSegments = result; | 1113 _pathSegments = result; |
1832 return result; | 1114 return result; |
1833 } | 1115 } |
1834 | 1116 |
| 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 */ |
1835 Map<String, String> get queryParameters { | 1133 Map<String, String> get queryParameters { |
1836 if (_queryParameters == null) { | 1134 if (_queryParameters == null) { |
1837 _queryParameters = | 1135 _queryParameters = |
1838 new UnmodifiableMapView<String, String>(Uri.splitQueryString(query)); | 1136 new UnmodifiableMapView<String, String>(splitQueryString(query)); |
1839 } | 1137 } |
1840 return _queryParameters; | 1138 return _queryParameters; |
1841 } | 1139 } |
1842 | 1140 |
| 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 */ |
1843 Map<String, List<String>> get queryParametersAll { | 1154 Map<String, List<String>> get queryParametersAll { |
1844 if (_queryParameterLists == null) { | 1155 if (_queryParameterLists == null) { |
1845 Map queryParameterLists = _splitQueryStringAll(query); | 1156 Map queryParameterLists = _splitQueryStringAll(query); |
1846 for (var key in queryParameterLists.keys) { | 1157 for (var key in queryParameterLists.keys) { |
1847 queryParameterLists[key] = | 1158 queryParameterLists[key] = |
1848 new List<String>.unmodifiable(queryParameterLists[key]); | 1159 new List<String>.unmodifiable(queryParameterLists[key]); |
1849 } | 1160 } |
1850 _queryParameterLists = | 1161 _queryParameterLists = |
1851 new Map<String, List<String>>.unmodifiable(queryParameterLists); | 1162 new Map<String, List<String>>.unmodifiable(queryParameterLists); |
1852 } | 1163 } |
1853 return _queryParameterLists; | 1164 return _queryParameterLists; |
1854 } | 1165 } |
1855 | 1166 |
| 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 */ |
1856 Uri normalizePath() { | 1183 Uri normalizePath() { |
1857 String path = _normalizePath(_path, scheme, hasAuthority); | 1184 String path = _normalizePath(_path, scheme, hasAuthority); |
1858 if (identical(path, _path)) return this; | 1185 if (identical(path, _path)) return this; |
1859 return this.replace(path: path); | 1186 return this.replace(path: path); |
1860 } | 1187 } |
1861 | 1188 |
1862 static int _makePort(int port, String scheme) { | 1189 static int _makePort(int port, String scheme) { |
1863 // Perform scheme specific normalization. | 1190 // Perform scheme specific normalization. |
1864 if (port != null && port == _defaultPort(scheme)) return null; | 1191 if (port != null && port == _defaultPort(scheme)) return null; |
1865 return port; | 1192 return port; |
(...skipping 12 matching lines...) Expand all Loading... |
1878 */ | 1205 */ |
1879 static String _makeHost(String host, int start, int end, bool strictIPv6) { | 1206 static String _makeHost(String host, int start, int end, bool strictIPv6) { |
1880 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? | 1207 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? |
1881 if (host == null) return null; | 1208 if (host == null) return null; |
1882 if (start == end) return ""; | 1209 if (start == end) return ""; |
1883 // Host is an IPv6 address if it starts with '[' or contains a colon. | 1210 // Host is an IPv6 address if it starts with '[' or contains a colon. |
1884 if (host.codeUnitAt(start) == _LEFT_BRACKET) { | 1211 if (host.codeUnitAt(start) == _LEFT_BRACKET) { |
1885 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { | 1212 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { |
1886 _fail(host, start, 'Missing end `]` to match `[` in host'); | 1213 _fail(host, start, 'Missing end `]` to match `[` in host'); |
1887 } | 1214 } |
1888 Uri.parseIPv6Address(host, start + 1, end - 1); | 1215 parseIPv6Address(host, start + 1, end - 1); |
1889 // RFC 5952 requires hex digits to be lower case. | 1216 // RFC 5952 requires hex digits to be lower case. |
1890 return host.substring(start, end).toLowerCase(); | 1217 return host.substring(start, end).toLowerCase(); |
1891 } | 1218 } |
1892 if (!strictIPv6) { | 1219 if (!strictIPv6) { |
1893 // TODO(lrn): skip if too short to be a valid IPv6 address? | 1220 // TODO(lrn): skip if too short to be a valid IPv6 address? |
1894 for (int i = start; i < end; i++) { | 1221 for (int i = start; i < end; i++) { |
1895 if (host.codeUnitAt(i) == _COLON) { | 1222 if (host.codeUnitAt(i) == _COLON) { |
1896 Uri.parseIPv6Address(host, start, end); | 1223 parseIPv6Address(host, start, end); |
1897 return '[$host]'; | 1224 return '[$host]'; |
1898 } | 1225 } |
1899 } | 1226 } |
1900 } | 1227 } |
1901 return _normalizeRegName(host, start, end); | 1228 return _normalizeRegName(host, start, end); |
1902 } | 1229 } |
1903 | 1230 |
1904 static bool _isRegNameChar(int char) { | 1231 static bool _isRegNameChar(int char) { |
1905 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | 1232 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; |
1906 } | 1233 } |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1999 final int codeUnit = scheme.codeUnitAt(i); | 1326 final int codeUnit = scheme.codeUnitAt(i); |
2000 if (!_isSchemeCharacter(codeUnit)) { | 1327 if (!_isSchemeCharacter(codeUnit)) { |
2001 _fail(scheme, i, "Illegal scheme character"); | 1328 _fail(scheme, i, "Illegal scheme character"); |
2002 } | 1329 } |
2003 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { | 1330 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { |
2004 containsUpperCase = true; | 1331 containsUpperCase = true; |
2005 } | 1332 } |
2006 } | 1333 } |
2007 scheme = scheme.substring(start, end); | 1334 scheme = scheme.substring(start, end); |
2008 if (containsUpperCase) scheme = scheme.toLowerCase(); | 1335 if (containsUpperCase) scheme = scheme.toLowerCase(); |
2009 return _canonicalizeScheme(scheme); | |
2010 } | |
2011 | |
2012 // Canonicalize a few often-used scheme strings. | |
2013 // | |
2014 // This improves memory usage and makes comparison faster. | |
2015 static String _canonicalizeScheme(String scheme) { | |
2016 if (scheme == "http") return "http"; | |
2017 if (scheme == "file") return "file"; | |
2018 if (scheme == "https") return "https"; | |
2019 if (scheme == "package") return "package"; | |
2020 return scheme; | 1336 return scheme; |
2021 } | 1337 } |
2022 | 1338 |
2023 static String _makeUserInfo(String userInfo, int start, int end) { | 1339 static String _makeUserInfo(String userInfo, int start, int end) { |
2024 if (userInfo == null) return ""; | 1340 if (userInfo == null) return ""; |
2025 return _normalize(userInfo, start, end, _userinfoTable); | 1341 return _normalize(userInfo, start, end, _userinfoTable); |
2026 } | 1342 } |
2027 | 1343 |
2028 static String _makePath(String path, int start, int end, | 1344 static String _makePath(String path, int start, int end, |
2029 Iterable<String> pathSegments, | 1345 Iterable<String> pathSegments, |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2096 } | 1412 } |
2097 }); | 1413 }); |
2098 return result.toString(); | 1414 return result.toString(); |
2099 } | 1415 } |
2100 | 1416 |
2101 static String _makeFragment(String fragment, int start, int end) { | 1417 static String _makeFragment(String fragment, int start, int end) { |
2102 if (fragment == null) return null; | 1418 if (fragment == null) return null; |
2103 return _normalize(fragment, start, end, _queryCharTable); | 1419 return _normalize(fragment, start, end, _queryCharTable); |
2104 } | 1420 } |
2105 | 1421 |
| 1422 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
| 1423 |
2106 /** | 1424 /** |
2107 * Performs RFC 3986 Percent-Encoding Normalization. | 1425 * Performs RFC 3986 Percent-Encoding Normalization. |
2108 * | 1426 * |
2109 * Returns a replacement string that should be replace the original escape. | 1427 * Returns a replacement string that should be replace the original escape. |
2110 * Returns null if no replacement is necessary because the escape is | 1428 * Returns null if no replacement is necessary because the escape is |
2111 * not for an unreserved character and is already non-lower-case. | 1429 * not for an unreserved character and is already non-lower-case. |
2112 * | 1430 * |
2113 * Returns "%" if the escape is invalid (not two valid hex digits following | 1431 * Returns "%" if the escape is invalid (not two valid hex digits following |
2114 * the percent sign). The calling code should replace the percent | 1432 * the percent sign). The calling code should replace the percent |
2115 * sign with "%25", but leave the following two characters unmodified. | 1433 * sign with "%25", but leave the following two characters unmodified. |
(...skipping 24 matching lines...) Expand all Loading... |
2140 return source.substring(index, index + 3).toUpperCase(); | 1458 return source.substring(index, index + 3).toUpperCase(); |
2141 } | 1459 } |
2142 // Escape is retained, and is already non-lower case, so return null to | 1460 // Escape is retained, and is already non-lower case, so return null to |
2143 // represent "no replacement necessary". | 1461 // represent "no replacement necessary". |
2144 return null; | 1462 return null; |
2145 } | 1463 } |
2146 | 1464 |
2147 // Converts a UTF-16 code-unit to its value as a hex digit. | 1465 // Converts a UTF-16 code-unit to its value as a hex digit. |
2148 // Returns -1 for non-hex digits. | 1466 // Returns -1 for non-hex digits. |
2149 static int _parseHexDigit(int char) { | 1467 static int _parseHexDigit(int char) { |
2150 const int zeroDigit = 0x30; | 1468 int digit = char ^ Uri._ZERO; |
2151 int digit = char ^ zeroDigit; | |
2152 if (digit <= 9) return digit; | 1469 if (digit <= 9) return digit; |
2153 int lowerCase = char | 0x20; | 1470 int lowerCase = char | 0x20; |
2154 if (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { | 1471 if (Uri._LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
2155 return lowerCase - (_LOWER_CASE_A - 10); | 1472 return lowerCase - (_LOWER_CASE_A - 10); |
2156 } | 1473 } |
2157 return -1; | 1474 return -1; |
2158 } | 1475 } |
2159 | 1476 |
2160 static String _escapeChar(int char) { | 1477 static String _escapeChar(int char) { |
2161 assert(char <= 0x10ffff); // It's a valid unicode code point. | 1478 assert(char <= 0x10ffff); // It's a valid unicode code point. |
2162 List<int> codeUnits; | 1479 List<int> codeUnits; |
2163 if (char < 0x80) { | 1480 if (char < 0x80) { |
2164 // ASCII, a single percent encoded sequence. | 1481 // ASCII, a single percent encoded sequence. |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2230 } | 1547 } |
2231 } else if (_isGeneralDelimiter(char)) { | 1548 } else if (_isGeneralDelimiter(char)) { |
2232 _fail(component, index, "Invalid character"); | 1549 _fail(component, index, "Invalid character"); |
2233 } else { | 1550 } else { |
2234 sourceLength = 1; | 1551 sourceLength = 1; |
2235 if ((char & 0xFC00) == 0xD800) { | 1552 if ((char & 0xFC00) == 0xD800) { |
2236 // Possible lead surrogate. | 1553 // Possible lead surrogate. |
2237 if (index + 1 < end) { | 1554 if (index + 1 < end) { |
2238 int tail = component.codeUnitAt(index + 1); | 1555 int tail = component.codeUnitAt(index + 1); |
2239 if ((tail & 0xFC00) == 0xDC00) { | 1556 if ((tail & 0xFC00) == 0xDC00) { |
2240 // Tail surrogate. | 1557 // Tail surrogat. |
2241 sourceLength = 2; | 1558 sourceLength = 2; |
2242 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | 1559 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); |
2243 } | 1560 } |
2244 } | 1561 } |
2245 } | 1562 } |
2246 replacement = _escapeChar(char); | 1563 replacement = _escapeChar(char); |
2247 } | 1564 } |
2248 if (buffer == null) buffer = new StringBuffer(); | 1565 if (buffer == null) buffer = new StringBuffer(); |
2249 buffer.write(component.substring(sectionStart, index)); | 1566 buffer.write(component.substring(sectionStart, index)); |
2250 buffer.write(replacement); | 1567 buffer.write(replacement); |
(...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2379 output.add(segment); | 1696 output.add(segment); |
2380 } | 1697 } |
2381 } | 1698 } |
2382 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { | 1699 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { |
2383 return "./"; | 1700 return "./"; |
2384 } | 1701 } |
2385 if (appendSlash || output.last == '..') output.add(""); | 1702 if (appendSlash || output.last == '..') output.add(""); |
2386 return output.join("/"); | 1703 return output.join("/"); |
2387 } | 1704 } |
2388 | 1705 |
| 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 */ |
2389 Uri resolve(String reference) { | 1716 Uri resolve(String reference) { |
2390 return resolveUri(Uri.parse(reference)); | 1717 return resolveUri(Uri.parse(reference)); |
2391 } | 1718 } |
2392 | 1719 |
| 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 */ |
2393 Uri resolveUri(Uri reference) { | 1734 Uri resolveUri(Uri reference) { |
2394 // From RFC 3986. | 1735 // From RFC 3986. |
2395 String targetScheme; | 1736 String targetScheme; |
2396 String targetUserInfo = ""; | 1737 String targetUserInfo = ""; |
2397 String targetHost; | 1738 String targetHost; |
2398 int targetPort; | 1739 int targetPort; |
2399 String targetPath; | 1740 String targetPath; |
2400 String targetQuery; | 1741 String targetQuery; |
2401 if (reference.scheme.isNotEmpty) { | 1742 if (reference.scheme.isNotEmpty) { |
2402 targetScheme = reference.scheme; | 1743 targetScheme = reference.scheme; |
(...skipping 25 matching lines...) Expand all Loading... |
2428 targetQuery = reference.query; | 1769 targetQuery = reference.query; |
2429 } else { | 1770 } else { |
2430 targetQuery = this._query; | 1771 targetQuery = this._query; |
2431 } | 1772 } |
2432 } else { | 1773 } else { |
2433 if (reference.hasAbsolutePath) { | 1774 if (reference.hasAbsolutePath) { |
2434 targetPath = _removeDotSegments(reference.path); | 1775 targetPath = _removeDotSegments(reference.path); |
2435 } else { | 1776 } else { |
2436 // This is the RFC 3986 behavior for merging. | 1777 // This is the RFC 3986 behavior for merging. |
2437 if (this.hasEmptyPath) { | 1778 if (this.hasEmptyPath) { |
2438 if (!this.hasAuthority) { | 1779 if (!this.hasScheme && !this.hasAuthority) { |
2439 if (!this.hasScheme) { | 1780 // Keep the path relative if no scheme or authority. |
2440 // Keep the path relative if no scheme or authority. | 1781 targetPath = reference.path; |
2441 targetPath = reference.path; | |
2442 } else { | |
2443 // Remove leading dot-segments if the path is put | |
2444 // beneath a scheme. | |
2445 targetPath = _removeDotSegments(reference.path); | |
2446 } | |
2447 } else { | 1782 } else { |
2448 // RFC algorithm for base with authority and empty path. | 1783 // Add path normalization on top of RFC algorithm. |
2449 targetPath = _removeDotSegments("/" + reference.path); | 1784 targetPath = _removeDotSegments("/" + reference.path); |
2450 } | 1785 } |
2451 } else { | 1786 } else { |
2452 var mergedPath = _mergePaths(this._path, reference.path); | 1787 var mergedPath = _mergePaths(this._path, reference.path); |
2453 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { | 1788 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { |
2454 targetPath = _removeDotSegments(mergedPath); | 1789 targetPath = _removeDotSegments(mergedPath); |
2455 } else { | 1790 } else { |
2456 // Non-RFC 3986 behavior. | 1791 // Non-RFC 3986 beavior. If both base and reference are relative |
2457 // If both base and reference are relative paths, | 1792 // path, allow the merged path to start with "..". |
2458 // allow the merged path to start with "..". | |
2459 // The RFC only specifies the case where the base has a scheme. | 1793 // The RFC only specifies the case where the base has a scheme. |
2460 targetPath = _normalizeRelativePath(mergedPath); | 1794 targetPath = _normalizeRelativePath(mergedPath); |
2461 } | 1795 } |
2462 } | 1796 } |
2463 } | 1797 } |
2464 if (reference.hasQuery) targetQuery = reference.query; | 1798 if (reference.hasQuery) targetQuery = reference.query; |
2465 } | 1799 } |
2466 } | 1800 } |
2467 } | 1801 } |
2468 String fragment = reference.hasFragment ? reference.fragment : null; | 1802 String fragment = reference.hasFragment ? reference.fragment : null; |
2469 return new _Uri._internal(targetScheme, | 1803 return new Uri._internal(targetScheme, |
2470 targetUserInfo, | 1804 targetUserInfo, |
2471 targetHost, | 1805 targetHost, |
2472 targetPort, | 1806 targetPort, |
2473 targetPath, | 1807 targetPath, |
2474 targetQuery, | 1808 targetQuery, |
2475 fragment); | 1809 fragment); |
2476 } | 1810 } |
2477 | 1811 |
| 1812 /** |
| 1813 * Returns whether the URI has a [scheme] component. |
| 1814 */ |
2478 bool get hasScheme => scheme.isNotEmpty; | 1815 bool get hasScheme => scheme.isNotEmpty; |
2479 | 1816 |
| 1817 /** |
| 1818 * Returns whether the URI has an [authority] component. |
| 1819 */ |
2480 bool get hasAuthority => _host != null; | 1820 bool get hasAuthority => _host != null; |
2481 | 1821 |
| 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 */ |
2482 bool get hasPort => _port != null; | 1830 bool get hasPort => _port != null; |
2483 | 1831 |
| 1832 /** |
| 1833 * Returns whether the URI has a query part. |
| 1834 */ |
2484 bool get hasQuery => _query != null; | 1835 bool get hasQuery => _query != null; |
2485 | 1836 |
| 1837 /** |
| 1838 * Returns whether the URI has a fragment part. |
| 1839 */ |
2486 bool get hasFragment => _fragment != null; | 1840 bool get hasFragment => _fragment != null; |
2487 | 1841 |
| 1842 /** |
| 1843 * Returns whether the URI has an empty path. |
| 1844 */ |
2488 bool get hasEmptyPath => _path.isEmpty; | 1845 bool get hasEmptyPath => _path.isEmpty; |
2489 | 1846 |
| 1847 /** |
| 1848 * Returns whether the URI has an absolute path (starting with '/'). |
| 1849 */ |
2490 bool get hasAbsolutePath => _path.startsWith('/'); | 1850 bool get hasAbsolutePath => _path.startsWith('/'); |
2491 | 1851 |
| 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 */ |
2492 String get origin { | 1860 String get origin { |
2493 if (scheme == "" || _host == null || _host == "") { | 1861 if (scheme == "" || _host == null || _host == "") { |
2494 throw new StateError("Cannot use origin without a scheme: $this"); | 1862 throw new StateError("Cannot use origin without a scheme: $this"); |
2495 } | 1863 } |
2496 if (scheme != "http" && scheme != "https") { | 1864 if (scheme != "http" && scheme != "https") { |
2497 throw new StateError( | 1865 throw new StateError( |
2498 "Origin is only applicable schemes http and https: $this"); | 1866 "Origin is only applicable schemes http and https: $this"); |
2499 } | 1867 } |
2500 if (_port == null) return "$scheme://$_host"; | 1868 if (_port == null) return "$scheme://$_host"; |
2501 return "$scheme://$_host:$_port"; | 1869 return "$scheme://$_host:$_port"; |
2502 } | 1870 } |
2503 | 1871 |
| 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 */ |
2504 String toFilePath({bool windows}) { | 1935 String toFilePath({bool windows}) { |
2505 if (scheme != "" && scheme != "file") { | 1936 if (scheme != "" && scheme != "file") { |
2506 throw new UnsupportedError( | 1937 throw new UnsupportedError( |
2507 "Cannot extract a file path from a $scheme URI"); | 1938 "Cannot extract a file path from a $scheme URI"); |
2508 } | 1939 } |
2509 if (query != "") { | 1940 if (query != "") { |
2510 throw new UnsupportedError( | 1941 throw new UnsupportedError( |
2511 "Cannot extract a file path from a URI with a query component"); | 1942 "Cannot extract a file path from a URI with a query component"); |
2512 } | 1943 } |
2513 if (fragment != "") { | 1944 if (fragment != "") { |
2514 throw new UnsupportedError( | 1945 throw new UnsupportedError( |
2515 "Cannot extract a file path from a URI with a fragment component"); | 1946 "Cannot extract a file path from a URI with a fragment component"); |
2516 } | 1947 } |
2517 if (windows == null) windows = _isWindows; | 1948 if (windows == null) windows = _isWindows; |
2518 return windows ? _toWindowsFilePath(this) : _toFilePath(); | 1949 return windows ? _toWindowsFilePath() : _toFilePath(); |
2519 } | 1950 } |
2520 | 1951 |
2521 String _toFilePath() { | 1952 String _toFilePath() { |
2522 if (hasAuthority && host != "") { | 1953 if (host != "") { |
2523 throw new UnsupportedError( | 1954 throw new UnsupportedError( |
2524 "Cannot extract a non-Windows file path from a file URI " | 1955 "Cannot extract a non-Windows file path from a file URI " |
2525 "with an authority"); | 1956 "with an authority"); |
2526 } | 1957 } |
2527 // Use path segments to have any escapes unescaped. | |
2528 var pathSegments = this.pathSegments; | |
2529 _checkNonWindowsPathReservedCharacters(pathSegments, false); | 1958 _checkNonWindowsPathReservedCharacters(pathSegments, false); |
2530 var result = new StringBuffer(); | 1959 var result = new StringBuffer(); |
2531 if (hasAbsolutePath) result.write("/"); | 1960 if (_isPathAbsolute) result.write("/"); |
2532 result.writeAll(pathSegments, "/"); | 1961 result.writeAll(pathSegments, "/"); |
2533 return result.toString(); | 1962 return result.toString(); |
2534 } | 1963 } |
2535 | 1964 |
2536 static String _toWindowsFilePath(Uri uri) { | 1965 String _toWindowsFilePath() { |
2537 bool hasDriveLetter = false; | 1966 bool hasDriveLetter = false; |
2538 var segments = uri.pathSegments; | 1967 var segments = pathSegments; |
2539 if (segments.length > 0 && | 1968 if (segments.length > 0 && |
2540 segments[0].length == 2 && | 1969 segments[0].length == 2 && |
2541 segments[0].codeUnitAt(1) == _COLON) { | 1970 segments[0].codeUnitAt(1) == _COLON) { |
2542 _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); | 1971 _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); |
2543 _checkWindowsPathReservedCharacters(segments, false, 1); | 1972 _checkWindowsPathReservedCharacters(segments, false, 1); |
2544 hasDriveLetter = true; | 1973 hasDriveLetter = true; |
2545 } else { | 1974 } else { |
2546 _checkWindowsPathReservedCharacters(segments, false, 0); | 1975 _checkWindowsPathReservedCharacters(segments, false); |
2547 } | 1976 } |
2548 var result = new StringBuffer(); | 1977 var result = new StringBuffer(); |
2549 if (uri.hasAbsolutePath && !hasDriveLetter) result.write(r"\"); | 1978 if (_isPathAbsolute && !hasDriveLetter) result.write("\\"); |
2550 if (uri.hasAuthority) { | 1979 if (host != "") { |
2551 var host = uri.host; | 1980 result.write("\\"); |
2552 if (host.isNotEmpty) { | 1981 result.write(host); |
2553 result.write(r"\"); | 1982 result.write("\\"); |
2554 result.write(host); | 1983 } |
2555 result.write(r"\"); | 1984 result.writeAll(segments, "\\"); |
2556 } | 1985 if (hasDriveLetter && segments.length == 1) result.write("\\"); |
2557 } | |
2558 result.writeAll(segments, r"\"); | |
2559 if (hasDriveLetter && segments.length == 1) result.write(r"\"); | |
2560 return result.toString(); | 1986 return result.toString(); |
2561 } | 1987 } |
2562 | 1988 |
2563 bool get _isPathAbsolute { | 1989 bool get _isPathAbsolute { |
2564 return _path != null && _path.startsWith('/'); | 1990 if (path == null || path.isEmpty) return false; |
| 1991 return path.startsWith('/'); |
2565 } | 1992 } |
2566 | 1993 |
2567 void _writeAuthority(StringSink ss) { | 1994 void _writeAuthority(StringSink ss) { |
2568 if (_userInfo.isNotEmpty) { | 1995 if (_userInfo.isNotEmpty) { |
2569 ss.write(_userInfo); | 1996 ss.write(_userInfo); |
2570 ss.write("@"); | 1997 ss.write("@"); |
2571 } | 1998 } |
2572 if (_host != null) ss.write(_host); | 1999 if (_host != null) ss.write(_host); |
2573 if (_port != null) { | 2000 if (_port != null) { |
2574 ss.write(":"); | 2001 ss.write(":"); |
2575 ss.write(_port); | 2002 ss.write(_port); |
2576 } | 2003 } |
2577 } | 2004 } |
2578 | 2005 |
2579 /** | 2006 /** |
2580 * Access the structure of a `data:` URI. | 2007 * Access the structure of a `data:` URI. |
2581 * | 2008 * |
2582 * Returns a [UriData] object for `data:` URIs and `null` for all other | 2009 * Returns a [UriData] object for `data:` URIs and `null` for all other |
2583 * URIs. | 2010 * URIs. |
2584 * The [UriData] object can be used to access the media type and data | 2011 * The [UriData] object can be used to access the media type and data |
2585 * of a `data:` URI. | 2012 * of a `data:` URI. |
2586 */ | 2013 */ |
2587 UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; | 2014 UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null; |
2588 | 2015 |
2589 String toString() { | 2016 String toString() { |
2590 return _text ??= _initializeText(); | |
2591 } | |
2592 | |
2593 String _initializeText() { | |
2594 assert(_text == null); | |
2595 StringBuffer sb = new StringBuffer(); | 2017 StringBuffer sb = new StringBuffer(); |
2596 if (scheme.isNotEmpty) sb..write(scheme)..write(":"); | 2018 _addIfNonEmpty(sb, scheme, scheme, ':'); |
2597 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | 2019 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { |
2598 // File URIS always have the authority, even if it is empty. | 2020 // File URIS always have the authority, even if it is empty. |
2599 // The empty URI means "localhost". | 2021 // The empty URI means "localhost". |
2600 sb.write("//"); | 2022 sb.write("//"); |
2601 _writeAuthority(sb); | 2023 _writeAuthority(sb); |
2602 } | 2024 } |
2603 sb.write(path); | 2025 sb.write(path); |
2604 if (_query != null) sb..write("?")..write(_query); | 2026 if (_query != null) { sb..write("?")..write(_query); } |
2605 if (_fragment != null) sb..write("#")..write(_fragment); | 2027 if (_fragment != null) { sb..write("#")..write(_fragment); } |
2606 return sb.toString(); | 2028 return sb.toString(); |
2607 } | 2029 } |
2608 | 2030 |
2609 bool operator==(other) { | 2031 bool operator==(other) { |
2610 if (identical(this, other)) return true; | 2032 if (other is! Uri) return false; |
2611 if (other is Uri) { | 2033 Uri uri = other; |
2612 Uri uri = other; | 2034 return scheme == uri.scheme && |
2613 return scheme == uri.scheme && | 2035 hasAuthority == uri.hasAuthority && |
2614 hasAuthority == uri.hasAuthority && | 2036 userInfo == uri.userInfo && |
2615 userInfo == uri.userInfo && | 2037 host == uri.host && |
2616 host == uri.host && | 2038 port == uri.port && |
2617 port == uri.port && | 2039 path == uri.path && |
2618 path == uri.path && | 2040 hasQuery == uri.hasQuery && |
2619 hasQuery == uri.hasQuery && | 2041 query == uri.query && |
2620 query == uri.query && | 2042 hasFragment == uri.hasFragment && |
2621 hasFragment == uri.hasFragment && | 2043 fragment == uri.fragment; |
2622 fragment == uri.fragment; | |
2623 } | |
2624 return false; | |
2625 } | 2044 } |
2626 | 2045 |
2627 int get hashCode { | 2046 int get hashCode { |
2628 return _hashCodeCache ??= toString().hashCode; | 2047 int combine(part, current) { |
| 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 }); |
2629 } | 2212 } |
2630 | 2213 |
2631 static List _createList() => []; | 2214 static List _createList() => []; |
2632 | 2215 |
2633 static Map _splitQueryStringAll( | 2216 static Map _splitQueryStringAll( |
2634 String query, {Encoding encoding: UTF8}) { | 2217 String query, {Encoding encoding: UTF8}) { |
2635 Map result = {}; | 2218 Map result = {}; |
2636 int i = 0; | 2219 int i = 0; |
2637 int start = 0; | 2220 int start = 0; |
2638 int equalsIndex = -1; | 2221 int equalsIndex = -1; |
(...skipping 22 matching lines...) Expand all Loading... |
2661 parsePair(start, equalsIndex, i); | 2244 parsePair(start, equalsIndex, i); |
2662 start = i + 1; | 2245 start = i + 1; |
2663 equalsIndex = -1; | 2246 equalsIndex = -1; |
2664 } | 2247 } |
2665 i++; | 2248 i++; |
2666 } | 2249 } |
2667 parsePair(start, equalsIndex, i); | 2250 parsePair(start, equalsIndex, i); |
2668 return result; | 2251 return result; |
2669 } | 2252 } |
2670 | 2253 |
| 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 |
2671 external static String _uriEncode(List<int> canonicalTable, | 2423 external static String _uriEncode(List<int> canonicalTable, |
2672 String text, | 2424 String text, |
2673 Encoding encoding, | 2425 Encoding encoding, |
2674 bool spaceToPlus); | 2426 bool spaceToPlus); |
2675 | 2427 |
2676 /** | 2428 /** |
2677 * Convert a byte (2 character hex sequence) in string [s] starting | 2429 * Convert a byte (2 character hex sequence) in string [s] starting |
2678 * at position [pos] to its ordinal value | 2430 * at position [pos] to its ordinal value |
2679 */ | 2431 */ |
2680 static int _hexCharPairToByte(String s, int pos) { | 2432 static int _hexCharPairToByte(String s, int pos) { |
(...skipping 501 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3182 mimeType = ""; | 2934 mimeType = ""; |
3183 } | 2935 } |
3184 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { | 2936 if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) { |
3185 buffer.write(mimeType); // Common cases need no escaping. | 2937 buffer.write(mimeType); // Common cases need no escaping. |
3186 } else { | 2938 } else { |
3187 int slashIndex = _validateMimeType(mimeType); | 2939 int slashIndex = _validateMimeType(mimeType); |
3188 if (slashIndex < 0) { | 2940 if (slashIndex < 0) { |
3189 throw new ArgumentError.value(mimeType, "mimeType", | 2941 throw new ArgumentError.value(mimeType, "mimeType", |
3190 "Invalid MIME type"); | 2942 "Invalid MIME type"); |
3191 } | 2943 } |
3192 buffer.write(_Uri._uriEncode(_tokenCharTable, | 2944 buffer.write(Uri._uriEncode(_tokenCharTable, |
3193 mimeType.substring(0, slashIndex), | 2945 mimeType.substring(0, slashIndex), |
3194 UTF8, false)); | 2946 UTF8, false)); |
3195 buffer.write("/"); | 2947 buffer.write("/"); |
3196 buffer.write(_Uri._uriEncode(_tokenCharTable, | 2948 buffer.write(Uri._uriEncode(_tokenCharTable, |
3197 mimeType.substring(slashIndex + 1), | 2949 mimeType.substring(slashIndex + 1), |
3198 UTF8, false)); | 2950 UTF8, false)); |
3199 } | 2951 } |
3200 if (charsetName != null) { | 2952 if (charsetName != null) { |
3201 if (indices != null) { | 2953 if (indices != null) { |
3202 indices..add(buffer.length) | 2954 indices..add(buffer.length) |
3203 ..add(buffer.length + 8); | 2955 ..add(buffer.length + 8); |
3204 } | 2956 } |
3205 buffer.write(";charset="); | 2957 buffer.write(";charset="); |
3206 buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); | 2958 buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
3207 } | 2959 } |
3208 parameters?.forEach((var key, var value) { | 2960 parameters?.forEach((var key, var value) { |
3209 if (key.isEmpty) { | 2961 if (key.isEmpty) { |
3210 throw new ArgumentError.value("", "Parameter names must not be empty"); | 2962 throw new ArgumentError.value("", "Parameter names must not be empty"); |
3211 } | 2963 } |
3212 if (value.isEmpty) { | 2964 if (value.isEmpty) { |
3213 throw new ArgumentError.value("", "Parameter values must not be empty", | 2965 throw new ArgumentError.value("", "Parameter values must not be empty", |
3214 'parameters["$key"]'); | 2966 'parameters["$key"]'); |
3215 } | 2967 } |
3216 if (indices != null) indices.add(buffer.length); | 2968 if (indices != null) indices.add(buffer.length); |
3217 buffer.write(';'); | 2969 buffer.write(';'); |
3218 // Encode any non-RFC2045-token character and both '%' and '#'. | 2970 // Encode any non-RFC2045-token character and both '%' and '#'. |
3219 buffer.write(_Uri._uriEncode(_tokenCharTable, key, UTF8, false)); | 2971 buffer.write(Uri._uriEncode(_tokenCharTable, key, UTF8, false)); |
3220 if (indices != null) indices.add(buffer.length); | 2972 if (indices != null) indices.add(buffer.length); |
3221 buffer.write('='); | 2973 buffer.write('='); |
3222 buffer.write(_Uri._uriEncode(_tokenCharTable, value, UTF8, false)); | 2974 buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
3223 }); | 2975 }); |
3224 } | 2976 } |
3225 | 2977 |
3226 /** | 2978 /** |
3227 * Checks mimeType is valid-ish (`token '/' token`). | 2979 * Checks mimeType is valid-ish (`token '/' token`). |
3228 * | 2980 * |
3229 * Returns the index of the slash, or -1 if the mime type is not | 2981 * Returns the index of the slash, or -1 if the mime type is not |
3230 * considered valid. | 2982 * considered valid. |
3231 * | 2983 * |
3232 * Currently only looks for slashes, all other characters will be | 2984 * Currently only looks for slashes, all other characters will be |
3233 * percent-encoded as UTF-8 if necessary. | 2985 * percent-encoded as UTF-8 if necessary. |
3234 */ | 2986 */ |
3235 static int _validateMimeType(String mimeType) { | 2987 static int _validateMimeType(String mimeType) { |
3236 int slashIndex = -1; | 2988 int slashIndex = -1; |
3237 for (int i = 0; i < mimeType.length; i++) { | 2989 for (int i = 0; i < mimeType.length; i++) { |
3238 var char = mimeType.codeUnitAt(i); | 2990 var char = mimeType.codeUnitAt(i); |
3239 if (char != _SLASH) continue; | 2991 if (char != Uri._SLASH) continue; |
3240 if (slashIndex < 0) { | 2992 if (slashIndex < 0) { |
3241 slashIndex = i; | 2993 slashIndex = i; |
3242 continue; | 2994 continue; |
3243 } | 2995 } |
3244 return -1; | 2996 return -1; |
3245 } | 2997 } |
3246 return slashIndex; | 2998 return slashIndex; |
3247 } | 2999 } |
3248 | 3000 |
3249 /** | 3001 /** |
3250 * Parses a string as a `data` URI. | 3002 * Parses a string as a `data` URI. |
3251 * | 3003 * |
3252 * The string must have the format: | 3004 * The string must have the format: |
3253 * | 3005 * |
3254 * ``` | 3006 * ``` |
3255 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat
a | 3007 * 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' dat
a |
3256 * ```` | 3008 * ```` |
3257 * | 3009 * |
3258 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, | 3010 * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, |
3259 * and `data` is a sequence of URI-characters (RFC-2396 `uric`). | 3011 * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). |
3260 * | 3012 * |
3261 * This means that all the characters must be ASCII, but the URI may contain | 3013 * This means that all the characters must be ASCII, but the URI may contain |
3262 * percent-escapes for non-ASCII byte values that need an interpretation | 3014 * percent-escapes for non-ASCII byte values that need an interpretation |
3263 * to be converted to the corresponding string. | 3015 * to be converted to the corresponding string. |
3264 * | 3016 * |
3265 * Parsing doesn't check the validity of any part, it just checks that the | 3017 * Parsing doesn't check the validity of any part, it just checks that the |
3266 * input has the correct structure with the correct sequence of `/`, `;`, `=` | 3018 * input has the correct structure with the correct sequence of `/`, `;`, `=` |
3267 * and `,` delimiters. | 3019 * and `,` delimiters. |
3268 * | 3020 * |
3269 * Accessing the individual parts may fail later if they turn out to have | 3021 * Accessing the individual parts may fail later if they turn out to have |
3270 * content that can't be decoded successfully as a string. | 3022 * content that can't be decoded sucessfully as a string. |
3271 */ | 3023 */ |
3272 static UriData parse(String uri) { | 3024 static UriData parse(String uri) { |
3273 if (uri.length >= 5) { | 3025 if (!uri.startsWith("data:")) { |
3274 int dataDelta = _startsWithData(uri, 0); | 3026 throw new FormatException("Does not start with 'data:'", uri, 0); |
3275 if (dataDelta == 0) { | |
3276 // Exact match on "data:". | |
3277 return _parse(uri, 5, null); | |
3278 } | |
3279 if (dataDelta == 0x20) { | |
3280 // Starts with a non-normalized "data" scheme containing upper-case | |
3281 // letters. Parse anyway, but throw away the scheme. | |
3282 return _parse(uri.substring(5), 0, null); | |
3283 } | |
3284 } | 3027 } |
3285 throw new FormatException("Does not start with 'data:'", uri, 0); | 3028 return _parse(uri, 5, null); |
3286 } | 3029 } |
3287 | 3030 |
3288 /** | 3031 /** |
3289 * The [Uri] that this `UriData` is giving access to. | 3032 * The [Uri] that this `UriData` is giving access to. |
3290 * | 3033 * |
3291 * Returns a `Uri` with scheme `data` and the remainder of the data URI | 3034 * Returns a `Uri` with scheme `data` and the remainder of the data URI |
3292 * as path. | 3035 * as path. |
3293 */ | 3036 */ |
3294 Uri get uri { | 3037 Uri get uri { |
3295 if (_uriCache != null) return _uriCache; | 3038 if (_uriCache != null) return _uriCache; |
3296 String path = _text; | 3039 String path = _text; |
3297 String query = null; | 3040 String query = null; |
3298 int colonIndex = _separatorIndices[0]; | 3041 int colonIndex = _separatorIndices[0]; |
3299 int queryIndex = _text.indexOf('?', colonIndex + 1); | 3042 int queryIndex = _text.indexOf('?', colonIndex + 1); |
3300 int end = null; | 3043 int end = null; |
3301 if (queryIndex >= 0) { | 3044 if (queryIndex >= 0) { |
3302 query = _text.substring(queryIndex + 1); | 3045 query = _text.substring(queryIndex + 1); |
3303 end = queryIndex; | 3046 end = queryIndex; |
3304 } | 3047 } |
3305 path = _text.substring(colonIndex + 1, end); | 3048 path = _text.substring(colonIndex + 1, end); |
3306 // TODO(lrn): This can generate a URI that isn't path normalized. | 3049 // TODO(lrn): This can generate a URI that isn't path normalized. |
3307 // That's perfectly reasonable - data URIs are not hierarchical, | 3050 // That's perfectly reasonable - data URIs are not hierarchical, |
3308 // but it may make some consumers stumble. | 3051 // but it may make some consumers stumble. |
3309 // Should we at least do escape normalization? | 3052 // Should we at least do escape normalization? |
3310 _uriCache = new _Uri._internal("data", "", null, null, path, query, null); | 3053 _uriCache = new Uri._internal("data", "", null, null, path, query, null); |
3311 return _uriCache; | 3054 return _uriCache; |
3312 } | 3055 } |
3313 | 3056 |
3314 /** | 3057 /** |
3315 * The MIME type of the data URI. | 3058 * The MIME type of the data URI. |
3316 * | 3059 * |
3317 * A data URI consists of a "media type" followed by data. | 3060 * A data URI consists of a "media type" followed by data. |
3318 * The media type starts with a MIME type and can be followed by | 3061 * The media type starts with a MIME type and can be followed by |
3319 * extra parameters. | 3062 * extra parameters. |
3320 * | 3063 * |
3321 * Example: | 3064 * Example: |
3322 * | 3065 * |
3323 * data:text/plain;charset=utf-8,Hello%20World! | 3066 * data:text/plain;charset=utf-8,Hello%20World! |
3324 * | 3067 * |
3325 * This data URI has the media type `text/plain;charset=utf-8`, which is the | 3068 * This data URI has the media type `text/plain;charset=utf-8`, which is the |
3326 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. | 3069 * MIME type `text/plain` with the parameter `charset` with value `utf-8`. |
3327 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. | 3070 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. |
3328 * | 3071 * |
3329 * If the first part of the data URI is empty, it defaults to `text/plain`. | 3072 * If the first part of the data URI is empty, it defaults to `text/plain`. |
3330 */ | 3073 */ |
3331 String get mimeType { | 3074 String get mimeType { |
3332 int start = _separatorIndices[0] + 1; | 3075 int start = _separatorIndices[0] + 1; |
3333 int end = _separatorIndices[1]; | 3076 int end = _separatorIndices[1]; |
3334 if (start == end) return "text/plain"; | 3077 if (start == end) return "text/plain"; |
3335 return _Uri._uriDecode(_text, start, end, UTF8, false); | 3078 return Uri._uriDecode(_text, start, end, UTF8, false); |
3336 } | 3079 } |
3337 | 3080 |
3338 /** | 3081 /** |
3339 * The charset parameter of the media type. | 3082 * The charset parameter of the media type. |
3340 * | 3083 * |
3341 * If the parameters of the media type contains a `charset` parameter | 3084 * If the parameters of the media type contains a `charset` parameter |
3342 * then this returns its value, otherwise it returns `US-ASCII`, | 3085 * then this returns its value, otherwise it returns `US-ASCII`, |
3343 * which is the default charset for data URIs. | 3086 * which is the default charset for data URIs. |
3344 */ | 3087 */ |
3345 String get charset { | 3088 String get charset { |
3346 int parameterStart = 1; | 3089 int parameterStart = 1; |
3347 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. | 3090 int parameterEnd = _separatorIndices.length - 1; // The ',' before data. |
3348 if (isBase64) { | 3091 if (isBase64) { |
3349 // There is a ";base64" separator, so subtract one for that as well. | 3092 // There is a ";base64" separator, so subtract one for that as well. |
3350 parameterEnd -= 1; | 3093 parameterEnd -= 1; |
3351 } | 3094 } |
3352 for (int i = parameterStart; i < parameterEnd; i += 2) { | 3095 for (int i = parameterStart; i < parameterEnd; i += 2) { |
3353 var keyStart = _separatorIndices[i] + 1; | 3096 var keyStart = _separatorIndices[i] + 1; |
3354 var keyEnd = _separatorIndices[i + 1]; | 3097 var keyEnd = _separatorIndices[i + 1]; |
3355 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { | 3098 if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { |
3356 return _Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], | 3099 return Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], |
3357 UTF8, false); | 3100 UTF8, false); |
3358 } | 3101 } |
3359 } | 3102 } |
3360 return "US-ASCII"; | 3103 return "US-ASCII"; |
3361 } | 3104 } |
3362 | 3105 |
3363 /** | 3106 /** |
3364 * Whether the data is Base64 encoded or not. | 3107 * Whether the data is Base64 encoded or not. |
3365 */ | 3108 */ |
3366 bool get isBase64 => _separatorIndices.length.isOdd; | 3109 bool get isBase64 => _separatorIndices.length.isOdd; |
3367 | 3110 |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3405 result.setRange(0, length, text.codeUnits, start); | 3148 result.setRange(0, length, text.codeUnits, start); |
3406 return result; | 3149 return result; |
3407 } | 3150 } |
3408 int index = 0; | 3151 int index = 0; |
3409 for (int i = start; i < text.length; i++) { | 3152 for (int i = start; i < text.length; i++) { |
3410 var codeUnit = text.codeUnitAt(i); | 3153 var codeUnit = text.codeUnitAt(i); |
3411 if (codeUnit != percent) { | 3154 if (codeUnit != percent) { |
3412 result[index++] = codeUnit; | 3155 result[index++] = codeUnit; |
3413 } else { | 3156 } else { |
3414 if (i + 2 < text.length) { | 3157 if (i + 2 < text.length) { |
3415 var digit1 = _Uri._parseHexDigit(text.codeUnitAt(i + 1)); | 3158 var digit1 = Uri._parseHexDigit(text.codeUnitAt(i + 1)); |
3416 var digit2 = _Uri._parseHexDigit(text.codeUnitAt(i + 2)); | 3159 var digit2 = Uri._parseHexDigit(text.codeUnitAt(i + 2)); |
3417 if (digit1 >= 0 && digit2 >= 0) { | 3160 if (digit1 >= 0 && digit2 >= 0) { |
3418 int byte = digit1 * 16 + digit2; | 3161 int byte = digit1 * 16 + digit2; |
3419 result[index++] = byte; | 3162 result[index++] = byte; |
3420 i += 2; | 3163 i += 2; |
3421 continue; | 3164 continue; |
3422 } | 3165 } |
3423 } | 3166 } |
3424 throw new FormatException("Invalid percent escape", text, i); | 3167 throw new FormatException("Invalid percent escape", text, i); |
3425 } | 3168 } |
3426 } | 3169 } |
3427 assert(index == result.length); | 3170 assert(index == result.length); |
3428 return result; | 3171 return result; |
3429 } | 3172 } |
3430 | 3173 |
3431 /** | 3174 /** |
3432 * Returns a string created from the content of the data URI. | 3175 * Returns a string created from the content of the data URI. |
3433 * | 3176 * |
3434 * If the content is Base64 encoded, it will be decoded to bytes and then | 3177 * If the content is Base64 encoded, it will be decoded to bytes and then |
3435 * decoded to a string using [encoding]. | 3178 * decoded to a string using [encoding]. |
3436 * If encoding is omitted, the value of a `charset` parameter is used | 3179 * If encoding is omitted, the value of a `charset` parameter is used |
3437 * if it is recognized by [Encoding.getByName], otherwise it defaults to | 3180 * if it is recongized by [Encoding.getByName], otherwise it defaults to |
3438 * the [ASCII] encoding, which is the default encoding for data URIs | 3181 * the [ASCII] encoding, which is the default encoding for data URIs |
3439 * that do not specify an encoding. | 3182 * that do not specify an encoding. |
3440 * | 3183 * |
3441 * If the content is not Base64 encoded, it will first have percent-escapes | 3184 * If the content is not Base64 encoded, it will first have percent-escapes |
3442 * converted to bytes and then the character codes and byte values are | 3185 * converted to bytes and then the character codes and byte values are |
3443 * decoded using [encoding]. | 3186 * decoded using [encoding]. |
3444 */ | 3187 */ |
3445 String contentAsString({Encoding encoding}) { | 3188 String contentAsString({Encoding encoding}) { |
3446 if (encoding == null) { | 3189 if (encoding == null) { |
3447 var charset = this.charset; // Returns "US-ASCII" if not present. | 3190 var charset = this.charset; // Returns "US-ASCII" if not present. |
3448 encoding = Encoding.getByName(charset); | 3191 encoding = Encoding.getByName(charset); |
3449 if (encoding == null) { | 3192 if (encoding == null) { |
3450 throw new UnsupportedError("Unknown charset: $charset"); | 3193 throw new UnsupportedError("Unknown charset: $charset"); |
3451 } | 3194 } |
3452 } | 3195 } |
3453 String text = _text; | 3196 String text = _text; |
3454 int start = _separatorIndices.last + 1; | 3197 int start = _separatorIndices.last + 1; |
3455 if (isBase64) { | 3198 if (isBase64) { |
3456 var converter = BASE64.decoder.fuse(encoding.decoder); | 3199 var converter = BASE64.decoder.fuse(encoding.decoder); |
3457 return converter.convert(text.substring(start)); | 3200 return converter.convert(text.substring(start)); |
3458 } | 3201 } |
3459 return _Uri._uriDecode(text, start, text.length, encoding, false); | 3202 return Uri._uriDecode(text, start, text.length, encoding, false); |
3460 } | 3203 } |
3461 | 3204 |
3462 /** | 3205 /** |
3463 * A map representing the parameters of the media type. | 3206 * A map representing the parameters of the media type. |
3464 * | 3207 * |
3465 * A data URI may contain parameters between the MIME type and the | 3208 * A data URI may contain parameters between the MIME type and the |
3466 * data. This converts these parameters to a map from parameter name | 3209 * data. This converts these parameters to a map from parameter name |
3467 * to parameter value. | 3210 * to parameter value. |
3468 * The map only contains parameters that actually occur in the URI. | 3211 * The map only contains parameters that actually occur in the URI. |
3469 * The `charset` parameter has a default value even if it doesn't occur | 3212 * The `charset` parameter has a default value even if it doesn't occur |
3470 * in the URI, which is reflected by the [charset] getter. This means that | 3213 * in the URI, which is reflected by the [charset] getter. This means that |
3471 * [charset] may return a value even if `parameters["charset"]` is `null`. | 3214 * [charset] may return a value even if `parameters["charset"]` is `null`. |
3472 * | 3215 * |
3473 * If the values contain non-ASCII values or percent escapes, they default | 3216 * If the values contain non-ASCII values or percent escapes, they default |
3474 * to being decoded as UTF-8. | 3217 * to being decoded as UTF-8. |
3475 */ | 3218 */ |
3476 Map<String, String> get parameters { | 3219 Map<String, String> get parameters { |
3477 var result = <String, String>{}; | 3220 var result = <String, String>{}; |
3478 for (int i = 3; i < _separatorIndices.length; i += 2) { | 3221 for (int i = 3; i < _separatorIndices.length; i += 2) { |
3479 var start = _separatorIndices[i - 2] + 1; | 3222 var start = _separatorIndices[i - 2] + 1; |
3480 var equals = _separatorIndices[i - 1]; | 3223 var equals = _separatorIndices[i - 1]; |
3481 var end = _separatorIndices[i]; | 3224 var end = _separatorIndices[i]; |
3482 String key = _Uri._uriDecode(_text, start, equals, UTF8, false); | 3225 String key = Uri._uriDecode(_text, start, equals, UTF8, false); |
3483 String value = _Uri._uriDecode(_text,equals + 1, end, UTF8, false); | 3226 String value = Uri._uriDecode(_text,equals + 1, end, UTF8, false); |
3484 result[key] = value; | 3227 result[key] = value; |
3485 } | 3228 } |
3486 return result; | 3229 return result; |
3487 } | 3230 } |
3488 | 3231 |
3489 static UriData _parse(String text, int start, Uri sourceUri) { | 3232 static UriData _parse(String text, int start, Uri sourceUri) { |
3490 assert(start == 0 || start == 5); | 3233 assert(start == 0 || start == 5); |
3491 assert((start == 5) == text.startsWith("data:")); | 3234 assert((start == 5) == text.startsWith("data:")); |
3492 | 3235 |
3493 /// Character codes. | 3236 /// Character codes. |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3556 // Encode the string into bytes then generate an ASCII only string | 3299 // Encode the string into bytes then generate an ASCII only string |
3557 // by percent encoding selected bytes. | 3300 // by percent encoding selected bytes. |
3558 int byteOr = 0; | 3301 int byteOr = 0; |
3559 for (int i = 0; i < bytes.length; i++) { | 3302 for (int i = 0; i < bytes.length; i++) { |
3560 int byte = bytes[i]; | 3303 int byte = bytes[i]; |
3561 byteOr |= byte; | 3304 byteOr |= byte; |
3562 if (byte < 128 && | 3305 if (byte < 128 && |
3563 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | 3306 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
3564 buffer.writeCharCode(byte); | 3307 buffer.writeCharCode(byte); |
3565 } else { | 3308 } else { |
3566 buffer.writeCharCode(_PERCENT); | 3309 buffer.writeCharCode(Uri._PERCENT); |
3567 buffer.writeCharCode(_hexDigits.codeUnitAt(byte >> 4)); | 3310 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte >> 4)); |
3568 buffer.writeCharCode(_hexDigits.codeUnitAt(byte & 0x0f)); | 3311 buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte & 0x0f)); |
3569 } | 3312 } |
3570 } | 3313 } |
3571 if ((byteOr & ~0xFF) != 0) { | 3314 if ((byteOr & ~0xFF) != 0) { |
3572 for (int i = 0; i < bytes.length; i++) { | 3315 for (int i = 0; i < bytes.length; i++) { |
3573 var byte = bytes[i]; | 3316 var byte = bytes[i]; |
3574 if (byte < 0 || byte > 255) { | 3317 if (byte < 0 || byte > 255) { |
3575 throw new ArgumentError.value(byte, "non-byte value"); | 3318 throw new ArgumentError.value(byte, "non-byte value"); |
3576 } | 3319 } |
3577 } | 3320 } |
3578 } | 3321 } |
(...skipping 28 matching lines...) Expand all Loading... |
3607 0x7fff]; // 0x70 - 0x7f 11111111 11111110 | 3350 0x7fff]; // 0x70 - 0x7f 11111111 11111110 |
3608 | 3351 |
3609 // All non-escape RFC-2396 uric characters. | 3352 // All non-escape RFC-2396 uric characters. |
3610 // | 3353 // |
3611 // uric = reserved | unreserved | escaped | 3354 // uric = reserved | unreserved | escaped |
3612 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," | 3355 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," |
3613 // unreserved = alphanum | mark | 3356 // unreserved = alphanum | mark |
3614 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" | 3357 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" |
3615 // | 3358 // |
3616 // This is the same characters as in a URI query (which is URI pchar plus '?') | 3359 // This is the same characters as in a URI query (which is URI pchar plus '?') |
3617 static const _uricTable = _Uri._queryCharTable; | 3360 static const _uricTable = Uri._queryCharTable; |
3618 } | 3361 } |
3619 | |
3620 // -------------------------------------------------------------------- | |
3621 // Constants used to read the scanner result. | |
3622 // The indices points into the table filled by [_scan] which contains | |
3623 // recognized positions in the scanned URI. | |
3624 // The `0` index is only used internally. | |
3625 | |
3626 /// Index of the position of that `:` after a scheme. | |
3627 const int _schemeEndIndex = 1; | |
3628 /// Index of the position of the character just before the host name. | |
3629 const int _hostStartIndex = 2; | |
3630 /// Index of the position of the `:` before a port value. | |
3631 const int _portStartIndex = 3; | |
3632 /// Index of the position of the first character of a path. | |
3633 const int _pathStartIndex = 4; | |
3634 /// Index of the position of the `?` before a query. | |
3635 const int _queryStartIndex = 5; | |
3636 /// Index of the position of the `#` before a fragment. | |
3637 const int _fragmentStartIndex = 6; | |
3638 /// Index of a position where the URI was determined to be "non-simple". | |
3639 const int _notSimpleIndex = 7; | |
3640 | |
3641 // Initial state for scanner. | |
3642 const int _uriStart = 00; | |
3643 | |
3644 // If scanning of a URI terminates in this state or above, | |
3645 // consider the URI non-simple | |
3646 const int _nonSimpleEndStates = 14; | |
3647 | |
3648 // Initial state for scheme validation. | |
3649 const int _schemeStart = 20; | |
3650 | |
3651 /// Transition tables used to scan a URI to determine its structure. | |
3652 /// | |
3653 /// The tables represent a state machine with output. | |
3654 /// | |
3655 /// To scan the URI, start in the [_uriStart] state, then read each character | |
3656 /// of the URI in order, from start to end, and for each character perform a | |
3657 /// transition to a new state while writing the current position into the output | |
3658 /// buffer at a designated index. | |
3659 /// | |
3660 /// Each state, represented by an integer which is an index into | |
3661 /// [_scannerTables], has a set of transitions, one for each character. | |
3662 /// The transitions are encoded as a 5-bit integer representing the next state | |
3663 /// and a 3-bit index into the output table. | |
3664 /// | |
3665 /// For URI scanning, only characters in the range U+0020 through U+007E are | |
3666 /// interesting, all characters outside that range are treated the same. | |
3667 /// The tables only contain 96 entries, representing that characters in the | |
3668 /// interesting range, plus one more to represent all values outside the range. | |
3669 /// The character entries are stored in one `Uint8List` per state, with the | |
3670 /// transition for a character at position `character ^ 0x60`, | |
3671 /// which maps the range U+0020 .. U+007F into positions 0 .. 95. | |
3672 /// All remaining characters are mapped to position 31 (`0x7f ^ 0x60`) which | |
3673 /// represents the transition for all remaining characters. | |
3674 final List<Uint8List> _scannerTables = _createTables(); | |
3675 | |
3676 // ---------------------------------------------------------------------- | |
3677 // Code to create the URI scanner table. | |
3678 | |
3679 /// Creates the tables for [_scannerTables] used by [Uri.parse]. | |
3680 /// | |
3681 /// See [_scannerTables] for the generated format. | |
3682 /// | |
3683 /// The concrete tables are chosen as a trade-off between the number of states | |
3684 /// needed and the precision of the result. | |
3685 /// This allows definitely recognizing the general structure of the URI | |
3686 /// (presence and location of scheme, user-info, host, port, path, query and | |
3687 /// fragment) while at the same time detecting that some components are not | |
3688 /// in canonical form (anything containing a `%`, a host-name containing a | |
3689 /// capital letter). Since the scanner doesn't know whether something is a | |
3690 /// scheme or a path until it sees `:`, or user-info or host until it sees | |
3691 /// a `@`, a second pass is needed to validate the scheme and any user-info | |
3692 /// is considered non-canonical by default. | |
3693 /// | |
3694 /// The states (starting from [_uriStart]) write positions while scanning | |
3695 /// a string from `start` to `end` as follows: | |
3696 /// | |
3697 /// - [_schemeEndIndex]: Should be initialized to `start-1`. | |
3698 /// If the URI has a scheme, it is set to the position of the `:` after | |
3699 /// the scheme. | |
3700 /// - [_hostStartIndex]: Should be initialized to `start - 1`. | |
3701 /// If the URI has an authority, it is set to the character before the | |
3702 /// host name - either the second `/` in the `//` leading the authority, | |
3703 /// or the `@` after a user-info. Comparing this value to the scheme end | |
3704 /// position can be used to detect that there is a user-info component. | |
3705 /// - [_portStartIndex]: Should be initialized to `start`. | |
3706 /// Set to the position of the last `:` in an authority, and unchanged | |
3707 /// if there is no authority or no `:` in an authority. | |
3708 /// If this position is after the host start, there is a port, otherwise it | |
3709 /// is just marking a colon in the user-info component. | |
3710 /// - [_pathStartIndex]: Should be initialized to `start`. | |
3711 /// Is set to the first path character unless the path is empty. | |
3712 /// If the path is empty, the position is either unchanged (`start`) or | |
3713 /// the first slash of an authority. So, if the path start is before a | |
3714 /// host start or scheme end, the path is empty. | |
3715 /// - [_queryStartIndex]: Should be initialized to `end`. | |
3716 /// The position of the `?` leading a query if the URI contains a query. | |
3717 /// - [_fragmentStartIndex]: Should be initialized to `end`. | |
3718 /// The position of the `#` leading a fragment if the URI contains a fragment. | |
3719 /// - [_notSimpleIndex]: Should be initialized to `start - 1`. | |
3720 /// Set to another value if the URI is considered "not simple". | |
3721 /// This is elaborated below. | |
3722 /// | |
3723 /// # Simple URIs | |
3724 /// A URI is considered "simple" if it is in a normalized form containing no | |
3725 /// escapes. This allows us to skip normalization and checking whether escapes | |
3726 /// are valid, and to extract components without worrying about unescaping. | |
3727 /// | |
3728 /// The scanner computes a conservative approximation of being "simple". | |
3729 /// It rejects any URI with an escape, with a user-info component (mainly | |
3730 /// because they are rare and would increase the number of states in the | |
3731 /// scanner significantly), with an IPV6 host or with a capital letter in | |
3732 /// the scheme or host name (the scheme is handled in a second scan using | |
3733 /// a separate two-state table). | |
3734 /// Further, paths containing `..` or `.` path segments are considered | |
3735 /// non-simple except for pure relative paths (no scheme or authority) starting | |
3736 /// with a sequence of "../" segments. | |
3737 /// | |
3738 /// The transition tables cannot detect a trailing ".." in the path, | |
3739 /// followed by a query or fragment, because the segment is not known to be | |
3740 /// complete until we are past it, and we then need to store the query/fragment | |
3741 /// start instead. This cast is checked manually post-scanning (such a path | |
3742 /// needs to be normalized to end in "../", so the URI shouldn't be considered | |
3743 /// simple). | |
3744 List<Uint8List> _createTables() { | |
3745 // TODO(lrn): Use a precomputed table. | |
3746 | |
3747 // Total number of states for the scanner. | |
3748 const int stateCount = 22; | |
3749 | |
3750 // States used to scan a URI from scratch. | |
3751 const int schemeOrPath = 01; | |
3752 const int authOrPath = 02; | |
3753 const int authOrPathSlash = 03; | |
3754 const int uinfoOrHost0 = 04; | |
3755 const int uinfoOrHost = 05; | |
3756 const int uinfoOrPort0 = 06; | |
3757 const int uinfoOrPort = 07; | |
3758 const int ipv6Host = 08; | |
3759 const int relPathSeg = 09; | |
3760 const int pathSeg = 10; | |
3761 const int path = 11; | |
3762 const int query = 12; | |
3763 const int fragment = 13; | |
3764 const int schemeOrPathDot = 14; | |
3765 const int schemeOrPathDot2 = 15; | |
3766 const int relPathSegDot = 16; | |
3767 const int relPathSegDot2 = 17; | |
3768 const int pathSegDot = 18; | |
3769 const int pathSegDot2 = 19; | |
3770 | |
3771 // States used to validate a scheme after its end position has been found. | |
3772 const int scheme0 = _schemeStart; | |
3773 const int scheme = 21; | |
3774 | |
3775 // Constants encoding the write-index for the state transition into the top 5 | |
3776 // bits of a byte. | |
3777 const int schemeEnd = _schemeEndIndex << 5; | |
3778 const int hostStart = _hostStartIndex << 5; | |
3779 const int portStart = _portStartIndex << 5; | |
3780 const int pathStart = _pathStartIndex << 5; | |
3781 const int queryStart = _queryStartIndex << 5; | |
3782 const int fragmentStart = _fragmentStartIndex << 5; | |
3783 const int notSimple = _notSimpleIndex << 5; | |
3784 | |
3785 /// The `unreserved` characters of RFC 3986. | |
3786 const unreserved = | |
3787 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~" ; | |
3788 /// The `sub-delim` characters of RFC 3986. | |
3789 const subDelims = r"!$&'()*+,;="; | |
3790 // The `pchar` characters of RFC 3986: characters that may occur in a path, | |
3791 // excluding escapes. | |
3792 const pchar = "$unreserved$subDelims"; | |
3793 | |
3794 var tables = new List<Uint8List>.generate(stateCount, | |
3795 (_) => new Uint8List(96)); | |
3796 | |
3797 // Helper function which initialize the table for [state] with a default | |
3798 // transition and returns the table. | |
3799 Uint8List build(state, defaultTransition) => | |
3800 tables[state]..fillRange(0, 96, defaultTransition); | |
3801 | |
3802 // Helper function which sets the transition for each character in [chars] | |
3803 // to [transition] in the [target] table. | |
3804 // The [chars] string must contain only characters in the U+0020 .. U+007E | |
3805 // range. | |
3806 void setChars(Uint8List target, String chars, int transition) { | |
3807 for (int i = 0; i < chars.length; i++) { | |
3808 var char = chars.codeUnitAt(i); | |
3809 target[char ^ 0x60] = transition; | |
3810 } | |
3811 } | |
3812 | |
3813 /// Helper function which sets the transition for all characters in the | |
3814 /// range from `range[0]` to `range[1]` to [transition] in the [target] table. | |
3815 /// | |
3816 /// The [range] must be a two-character string where both characters are in | |
3817 /// the U+0020 .. U+007E range and the former character must have a lower | |
3818 /// code point than the latter. | |
3819 void setRange(Uint8List target, String range, int transition) { | |
3820 for (int i = range.codeUnitAt(0), n = range.codeUnitAt(1); i <= n; i++) { | |
3821 target[i ^ 0x60] = transition; | |
3822 } | |
3823 } | |
3824 | |
3825 // Create the transitions for each state. | |
3826 var b; | |
3827 | |
3828 // Validate as path, if it is a scheme, we handle it later. | |
3829 b = build(_uriStart, schemeOrPath | notSimple); | |
3830 setChars(b, pchar, schemeOrPath); | |
3831 setChars(b, ".", schemeOrPathDot); | |
3832 setChars(b, ":", authOrPath | schemeEnd); // Handle later. | |
3833 setChars(b, "/", authOrPathSlash); | |
3834 setChars(b, "?", query | queryStart); | |
3835 setChars(b, "#", fragment | fragmentStart); | |
3836 | |
3837 b = build(schemeOrPathDot, schemeOrPath | notSimple); | |
3838 setChars(b, pchar, schemeOrPath); | |
3839 setChars(b, ".", schemeOrPathDot2); | |
3840 setChars(b, ':', authOrPath | schemeEnd); | |
3841 setChars(b, "/", pathSeg | notSimple); | |
3842 setChars(b, "?", query | queryStart); | |
3843 setChars(b, "#", fragment | fragmentStart); | |
3844 | |
3845 b = build(schemeOrPathDot2, schemeOrPath | notSimple); | |
3846 setChars(b, pchar, schemeOrPath); | |
3847 setChars(b, "%", schemeOrPath | notSimple); | |
3848 setChars(b, ':', authOrPath | schemeEnd); | |
3849 setChars(b, "/", relPathSeg); | |
3850 setChars(b, "?", query | queryStart); | |
3851 setChars(b, "#", fragment | fragmentStart); | |
3852 | |
3853 b = build(schemeOrPath, schemeOrPath | notSimple); | |
3854 setChars(b, pchar, schemeOrPath); | |
3855 setChars(b, ':', authOrPath | schemeEnd); | |
3856 setChars(b, "/", pathSeg); | |
3857 setChars(b, "?", query | queryStart); | |
3858 setChars(b, "#", fragment | fragmentStart); | |
3859 | |
3860 b = build(authOrPath, path | notSimple); | |
3861 setChars(b, pchar, path | pathStart); | |
3862 setChars(b, "/", authOrPathSlash | pathStart); | |
3863 setChars(b, ".", pathSegDot | pathStart); | |
3864 setChars(b, "?", query | queryStart); | |
3865 setChars(b, "#", fragment | fragmentStart); | |
3866 | |
3867 b = build(authOrPathSlash, path | notSimple); | |
3868 setChars(b, pchar, path); | |
3869 setChars(b, "/", uinfoOrHost0 | hostStart); | |
3870 setChars(b, ".", pathSegDot); | |
3871 setChars(b, "?", query | queryStart); | |
3872 setChars(b, "#", fragment | fragmentStart); | |
3873 | |
3874 b = build(uinfoOrHost0, uinfoOrHost | notSimple); | |
3875 setChars(b, pchar, uinfoOrHost); | |
3876 setRange(b, "AZ", uinfoOrHost | notSimple); | |
3877 setChars(b, ":", uinfoOrPort0 | portStart); | |
3878 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3879 setChars(b, "[", ipv6Host | notSimple); | |
3880 setChars(b, "/", pathSeg | pathStart); | |
3881 setChars(b, "?", query | queryStart); | |
3882 setChars(b, "#", fragment | fragmentStart); | |
3883 | |
3884 b = build(uinfoOrHost, uinfoOrHost | notSimple); | |
3885 setChars(b, pchar, uinfoOrHost); | |
3886 setRange(b, "AZ", uinfoOrHost | notSimple); | |
3887 setChars(b, ":", uinfoOrPort0 | portStart); | |
3888 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3889 setChars(b, "/", pathSeg | pathStart); | |
3890 setChars(b, "?", query | queryStart); | |
3891 setChars(b, "#", fragment | fragmentStart); | |
3892 | |
3893 b = build(uinfoOrPort0, uinfoOrPort | notSimple); | |
3894 setRange(b, "19", uinfoOrPort); | |
3895 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3896 setChars(b, "/", pathSeg | pathStart); | |
3897 setChars(b, "?", query | queryStart); | |
3898 setChars(b, "#", fragment | fragmentStart); | |
3899 | |
3900 b = build(uinfoOrPort, uinfoOrPort | notSimple); | |
3901 setRange(b, "09", uinfoOrPort); | |
3902 setChars(b, "@", uinfoOrHost0 | hostStart); | |
3903 setChars(b, "/", pathSeg | pathStart); | |
3904 setChars(b, "?", query | queryStart); | |
3905 setChars(b, "#", fragment | fragmentStart); | |
3906 | |
3907 b = build(ipv6Host, ipv6Host); | |
3908 setChars(b, "]", uinfoOrHost); | |
3909 | |
3910 b = build(relPathSeg, path | notSimple); | |
3911 setChars(b, pchar, path); | |
3912 setChars(b, ".", relPathSegDot); | |
3913 setChars(b, "/", pathSeg | notSimple); | |
3914 setChars(b, "?", query | queryStart); | |
3915 setChars(b, "#", fragment | fragmentStart); | |
3916 | |
3917 b = build(relPathSegDot, path | notSimple); | |
3918 setChars(b, pchar, path); | |
3919 setChars(b, ".", relPathSegDot2); | |
3920 setChars(b, "/", pathSeg | notSimple); | |
3921 setChars(b, "?", query | queryStart); | |
3922 setChars(b, "#", fragment | fragmentStart); | |
3923 | |
3924 b = build(relPathSegDot2, path | notSimple); | |
3925 setChars(b, pchar, path); | |
3926 setChars(b, "/", relPathSeg); | |
3927 setChars(b, "?", query | queryStart); // This should be non-simple. | |
3928 setChars(b, "#", fragment | fragmentStart); // This should be non-simple. | |
3929 | |
3930 b = build(pathSeg, path | notSimple); | |
3931 setChars(b, pchar, path); | |
3932 setChars(b, ".", pathSegDot); | |
3933 setChars(b, "/", pathSeg | notSimple); | |
3934 setChars(b, "?", query | queryStart); | |
3935 setChars(b, "#", fragment | fragmentStart); | |
3936 | |
3937 b = build(pathSegDot, path | notSimple); | |
3938 setChars(b, pchar, path); | |
3939 setChars(b, ".", pathSegDot2); | |
3940 setChars(b, "/", pathSeg | notSimple); | |
3941 setChars(b, "?", query | queryStart); | |
3942 setChars(b, "#", fragment | fragmentStart); | |
3943 | |
3944 b = build(pathSegDot2, path | notSimple); | |
3945 setChars(b, pchar, path); | |
3946 setChars(b, "/", pathSeg | notSimple); | |
3947 setChars(b, "?", query | queryStart); | |
3948 setChars(b, "#", fragment | fragmentStart); | |
3949 | |
3950 b = build(path, path | notSimple); | |
3951 setChars(b, pchar, path); | |
3952 setChars(b, "/", pathSeg); | |
3953 setChars(b, "?", query | queryStart); | |
3954 setChars(b, "#", fragment | fragmentStart); | |
3955 | |
3956 b = build(query, query | notSimple); | |
3957 setChars(b, pchar, query); | |
3958 setChars(b, "?", query); | |
3959 setChars(b, "#", fragment | fragmentStart); | |
3960 | |
3961 b = build(fragment, fragment | notSimple); | |
3962 setChars(b, pchar, fragment); | |
3963 setChars(b, "?", fragment); | |
3964 | |
3965 // A separate two-state validator for lower-case scheme names. | |
3966 // Any non-scheme character or upper-case letter is marked as non-simple. | |
3967 b = build(scheme0, scheme | notSimple); | |
3968 setRange(b, "az", scheme); | |
3969 | |
3970 b = build(scheme, scheme | notSimple); | |
3971 setRange(b, "az", scheme); | |
3972 setRange(b, "09", scheme); | |
3973 setChars(b, "+-.", scheme); | |
3974 | |
3975 return tables; | |
3976 } | |
3977 | |
3978 // -------------------------------------------------------------------- | |
3979 // Code that uses the URI scanner table. | |
3980 | |
3981 /// Scan a string using the [_scannerTables] state machine. | |
3982 /// | |
3983 /// Scans [uri] from [start] to [end], startig in state [state] and | |
3984 /// writing output into [indices]. | |
3985 /// | |
3986 /// Returns the final state. | |
3987 int _scan(String uri, int start, int end, int state, List<int> indices) { | |
3988 var tables = _scannerTables; | |
3989 assert(end <= uri.length); | |
3990 for (int i = start; i < end; i++) { | |
3991 var table = tables[state]; | |
3992 // Xor with 0x60 to move range 0x20-0x7f into 0x00-0x5f | |
3993 int char = uri.codeUnitAt(i) ^ 0x60; | |
3994 // Use 0x1f (nee 0x7f) to represent all unhandled characters. | |
3995 if (char > 0x5f) char = 0x1f; | |
3996 int transition = table[char]; | |
3997 state = transition & 0x1f; | |
3998 indices[transition >> 5] = i; | |
3999 } | |
4000 return state; | |
4001 } | |
4002 | |
4003 class _SimpleUri implements Uri { | |
4004 final String _uri; | |
4005 final int _schemeEnd; | |
4006 final int _hostStart; | |
4007 final int _portStart; | |
4008 final int _pathStart; | |
4009 final int _queryStart; | |
4010 final int _fragmentStart; | |
4011 /// The scheme is often used to distinguish URIs. | |
4012 /// To make comparisons more efficient, we cache the value, and | |
4013 /// canonicalize a few known types. | |
4014 String _schemeCache; | |
4015 int _hashCodeCache; | |
4016 | |
4017 _SimpleUri( | |
4018 this._uri, | |
4019 this._schemeEnd, | |
4020 this._hostStart, | |
4021 this._portStart, | |
4022 this._pathStart, | |
4023 this._queryStart, | |
4024 this._fragmentStart, | |
4025 this._schemeCache); | |
4026 | |
4027 bool get hasScheme => _schemeEnd > 0; | |
4028 bool get hasAuthority => _hostStart > 0; | |
4029 bool get hasUserInfo => _hostStart > _schemeEnd + 4; | |
4030 bool get hasPort => _hostStart > 0 && _portStart + 1 < _pathStart; | |
4031 bool get hasQuery => _queryStart < _fragmentStart; | |
4032 bool get hasFragment => _fragmentStart < _uri.length; | |
4033 | |
4034 bool get _isFile => _schemeEnd == 4 && _uri.startsWith("file"); | |
4035 bool get _isHttp => _schemeEnd == 4 && _uri.startsWith("http"); | |
4036 bool get _isHttps => _schemeEnd == 5 && _uri.startsWith("https"); | |
4037 bool get _isPackage => _schemeEnd == 7 && _uri.startsWith("package"); | |
4038 bool _isScheme(String scheme) => | |
4039 _schemeEnd == scheme.length && _uri.startsWith(scheme); | |
4040 | |
4041 bool get hasAbsolutePath => _uri.startsWith("/", _pathStart); | |
4042 bool get hasEmptyPath => _pathStart == _queryStart; | |
4043 | |
4044 bool get isAbsolute => hasScheme && !hasFragment; | |
4045 | |
4046 String get scheme { | |
4047 if (_schemeEnd <= 0) return ""; | |
4048 if (_schemeCache != null) return _schemeCache; | |
4049 if (_isHttp) { | |
4050 _schemeCache = "http"; | |
4051 } else if (_isHttps) { | |
4052 _schemeCache = "https"; | |
4053 } else if (_isFile) { | |
4054 _schemeCache = "file"; | |
4055 } else if (_isPackage) { | |
4056 _schemeCache = "package"; | |
4057 } else { | |
4058 _schemeCache = _uri.substring(0, _schemeEnd); | |
4059 } | |
4060 return _schemeCache; | |
4061 } | |
4062 String get authority => _hostStart > 0 ? | |
4063 _uri.substring(_schemeEnd + 3, _pathStart) : ""; | |
4064 String get userInfo => (_hostStart > _schemeEnd + 3) ? | |
4065 _uri.substring(_schemeEnd + 3, _hostStart - 1) : ""; | |
4066 String get host => | |
4067 _hostStart > 0 ? _uri.substring(_hostStart, _portStart) : ""; | |
4068 int get port { | |
4069 if (hasPort) return int.parse(_uri.substring(_portStart + 1, _pathStart)); | |
4070 if (_isHttp) return 80; | |
4071 if (_isHttps) return 443; | |
4072 return 0; | |
4073 } | |
4074 String get path =>_uri.substring(_pathStart, _queryStart); | |
4075 String get query => (_queryStart < _fragmentStart) ? | |
4076 _uri.substring(_queryStart + 1, _fragmentStart) : ""; | |
4077 String get fragment => (_fragmentStart < _uri.length) ? | |
4078 _uri.substring(_fragmentStart + 1) : ""; | |
4079 | |
4080 String get origin { | |
4081 // Check original behavior - W3C spec is wonky! | |
4082 bool isHttp = _isHttp; | |
4083 if (_schemeEnd < 0 || _hostStart == _portStart) { | |
4084 throw new StateError("Cannot use origin without a scheme: $this"); | |
4085 } | |
4086 if (!isHttp && !_isHttps) { | |
4087 throw new StateError( | |
4088 "Origin is only applicable schemes http and https: $this"); | |
4089 } | |
4090 if (_hostStart == _schemeEnd + 3) { | |
4091 return _uri.substring(0, _pathStart); | |
4092 } | |
4093 // Need to drop anon-empty userInfo. | |
4094 return _uri.substring(0, _schemeEnd + 3) + | |
4095 _uri.substring(_hostStart, _pathStart); | |
4096 } | |
4097 | |
4098 List<String> get pathSegments { | |
4099 int start = _pathStart; | |
4100 int end = _queryStart; | |
4101 if (_uri.startsWith("/", start)) start++; | |
4102 if (start == end) return const <String>[]; | |
4103 List<String> parts = []; | |
4104 for (int i = start; i < end; i++) { | |
4105 var char = _uri.codeUnitAt(i); | |
4106 if (char == _SLASH) { | |
4107 parts.add(_uri.substring(start, i)); | |
4108 start = i + 1; | |
4109 } | |
4110 } | |
4111 parts.add(_uri.substring(start, end)); | |
4112 return new List<String>.unmodifiable(parts); | |
4113 } | |
4114 | |
4115 Map<String, String> get queryParameters { | |
4116 if (!hasQuery) return const <String, String>{}; | |
4117 return new UnmodifiableMapView<String, String>( | |
4118 Uri.splitQueryString(query)); | |
4119 } | |
4120 | |
4121 Map<String, List<String>> get queryParametersAll { | |
4122 if (!hasQuery) return const <String, List<String>>{}; | |
4123 Map queryParameterLists = _Uri._splitQueryStringAll(query); | |
4124 for (var key in queryParameterLists.keys) { | |
4125 queryParameterLists[key] = | |
4126 new List<String>.unmodifiable(queryParameterLists[key]); | |
4127 } | |
4128 return new Map<String, List<String>>.unmodifiable(queryParameterLists); | |
4129 } | |
4130 | |
4131 bool _isPort(String port) { | |
4132 int portDigitStart = _portStart + 1; | |
4133 return portDigitStart + port.length == _pathStart && | |
4134 _uri.startsWith(port, portDigitStart); | |
4135 } | |
4136 | |
4137 Uri normalizePath() => this; | |
4138 | |
4139 Uri removeFragment() { | |
4140 if (!hasFragment) return this; | |
4141 return new _SimpleUri( | |
4142 _uri.substring(0, _fragmentStart), | |
4143 _schemeEnd, _hostStart, _portStart, | |
4144 _pathStart, _queryStart, _fragmentStart, _schemeCache); | |
4145 } | |
4146 | |
4147 Uri replace({String scheme, | |
4148 String userInfo, | |
4149 String host, | |
4150 int port, | |
4151 String path, | |
4152 Iterable<String> pathSegments, | |
4153 String query, | |
4154 Map<String, dynamic/*String|Iterable<String>*/> queryParameters, | |
4155 String fragment}) { | |
4156 bool schemeChanged = false; | |
4157 if (scheme != null) { | |
4158 scheme = _Uri._makeScheme(scheme, 0, scheme.length); | |
4159 schemeChanged = !_isScheme(scheme); | |
4160 } else { | |
4161 scheme = this.scheme; | |
4162 } | |
4163 bool isFile = (scheme == "file"); | |
4164 if (userInfo != null) { | |
4165 userInfo = _Uri._makeUserInfo(userInfo, 0, userInfo.length); | |
4166 } else if (_hostStart > 0) { | |
4167 userInfo = _uri.substring(_schemeEnd + 3, _hostStart); | |
4168 } else { | |
4169 userInfo = ""; | |
4170 } | |
4171 if (port != null) { | |
4172 port = _Uri._makePort(port, scheme); | |
4173 } else { | |
4174 port = this.hasPort ? this.port : null; | |
4175 if (schemeChanged) { | |
4176 // The default port might have changed. | |
4177 port = _Uri._makePort(port, scheme); | |
4178 } | |
4179 } | |
4180 if (host != null) { | |
4181 host = _Uri._makeHost(host, 0, host.length, false); | |
4182 } else if (_hostStart > 0) { | |
4183 host = _uri.substring(_hostStart, _portStart); | |
4184 } else if (userInfo.isNotEmpty || port != null || isFile) { | |
4185 host = ""; | |
4186 } | |
4187 | |
4188 bool hasAuthority = host != null; | |
4189 if (path != null || pathSegments != null) { | |
4190 path = _Uri._makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
4191 scheme, hasAuthority); | |
4192 } else { | |
4193 path = _uri.substring(_pathStart, _queryStart); | |
4194 if ((isFile || (hasAuthority && !path.isEmpty)) && | |
4195 !path.startsWith('/')) { | |
4196 path = "/" + path; | |
4197 } | |
4198 } | |
4199 | |
4200 if (query != null || queryParameters != null) { | |
4201 query = _Uri._makeQuery( | |
4202 query, 0, _stringOrNullLength(query), queryParameters); | |
4203 } else if (_queryStart < _fragmentStart) { | |
4204 query = _uri.substring(_queryStart, _fragmentStart); | |
4205 } | |
4206 | |
4207 if (fragment != null) { | |
4208 fragment = _Uri._makeFragment(fragment, 0, fragment.length); | |
4209 } else if (_fragmentStart < _uri.length) { | |
4210 fragment = _uri.substring(_fragmentStart + 1); | |
4211 } | |
4212 | |
4213 return new _Uri._internal( | |
4214 scheme, userInfo, host, port, path, query, fragment); | |
4215 } | |
4216 | |
4217 Uri resolve(String reference) { | |
4218 return resolveUri(Uri.parse(reference)); | |
4219 } | |
4220 | |
4221 Uri resolveUri(Uri reference) { | |
4222 if (reference is _SimpleUri) { | |
4223 return _simpleMerge(this, reference); | |
4224 } | |
4225 return _toNonSimple().resolveUri(reference); | |
4226 } | |
4227 | |
4228 // Merge two simple URIs. This should always result in a prefix of | |
4229 // one concatentated with a suffix of the other, possibly with a `/` in | |
4230 // the middle of two merged paths, which is again simple. | |
4231 // In a few cases, there might be a need for extra normalization, when | |
4232 // resolving on top of a known scheme. | |
4233 Uri _simpleMerge(_SimpleUri base, _SimpleUri ref) { | |
4234 if (ref.hasScheme) return ref; | |
4235 if (ref.hasAuthority) { | |
4236 if (!base.hasScheme) return ref; | |
4237 bool isSimple = true; | |
4238 if (base._isFile) { | |
4239 isSimple = !ref.hasEmptyPath; | |
4240 } else if (base._isHttp) { | |
4241 isSimple = !ref._isPort("80"); | |
4242 } else if (base._isHttps) { | |
4243 isSimple = !ref._isPort("443"); | |
4244 } | |
4245 if (isSimple) { | |
4246 var delta = base._schemeEnd + 1; | |
4247 var newUri = base._uri.substring(0, base._schemeEnd + 1) + | |
4248 ref._uri.substring(ref._schemeEnd + 1); | |
4249 return new _SimpleUri(newUri, | |
4250 base._schemeEnd, | |
4251 ref._hostStart + delta, | |
4252 ref._portStart + delta, | |
4253 ref._pathStart + delta, | |
4254 ref._queryStart + delta, | |
4255 ref._fragmentStart + delta, | |
4256 base._schemeCache); | |
4257 } else { | |
4258 // This will require normalization, so use the _Uri implementation. | |
4259 return _toNonSimple().resolveUri(ref); | |
4260 } | |
4261 } | |
4262 if (ref.hasEmptyPath) { | |
4263 if (ref.hasQuery) { | |
4264 int delta = base._queryStart - ref._queryStart; | |
4265 var newUri = base._uri.substring(0, base._queryStart) + | |
4266 ref._uri.substring(ref._queryStart); | |
4267 return new _SimpleUri(newUri, | |
4268 base._schemeEnd, | |
4269 base._hostStart, | |
4270 base._portStart, | |
4271 base._pathStart, | |
4272 ref._queryStart + delta, | |
4273 ref._fragmentStart + delta, | |
4274 base._schemeCache); | |
4275 } | |
4276 if (ref.hasFragment) { | |
4277 int delta = base._fragmentStart - ref._fragmentStart; | |
4278 var newUri = base._uri.substring(0, base._fragmentStart) + | |
4279 ref._uri.substring(ref._fragmentStart); | |
4280 return new _SimpleUri(newUri, | |
4281 base._schemeEnd, | |
4282 base._hostStart, | |
4283 base._portStart, | |
4284 base._pathStart, | |
4285 base._queryStart, | |
4286 ref._fragmentStart + delta, | |
4287 base._schemeCache); | |
4288 } | |
4289 return base.removeFragment(); | |
4290 } | |
4291 if (ref.hasAbsolutePath) { | |
4292 var delta = base._pathStart - ref._pathStart; | |
4293 var newUri = base._uri.substring(0, base._pathStart) + | |
4294 ref._uri.substring(ref._pathStart); | |
4295 return new _SimpleUri(newUri, | |
4296 base._schemeEnd, | |
4297 base._hostStart, | |
4298 base._portStart, | |
4299 base._pathStart, | |
4300 ref._queryStart + delta, | |
4301 ref._fragmentStart + delta, | |
4302 base._schemeCache); | |
4303 } | |
4304 if (base.hasEmptyPath && base.hasAuthority) { | |
4305 // ref has relative non-empty path. | |
4306 // Add a "/" in front, then leading "/../" segments are folded to "/". | |
4307 int refStart = ref._pathStart; | |
4308 while (ref._uri.startsWith("../", refStart)) { | |
4309 refStart += 3; | |
4310 } | |
4311 var delta = base._pathStart - refStart + 1; | |
4312 var newUri = "${base._uri.substring(0, base._pathStart)}/" | |
4313 "${ref._uri.substring(refStart)}"; | |
4314 return new _SimpleUri(newUri, | |
4315 base._schemeEnd, | |
4316 base._hostStart, | |
4317 base._portStart, | |
4318 base._pathStart, | |
4319 ref._queryStart + delta, | |
4320 ref._fragmentStart + delta, | |
4321 base._schemeCache); | |
4322 } | |
4323 // Merge paths. | |
4324 if (base._uri.startsWith("../", base._pathStart)) { | |
4325 // Complex rare case, go slow. | |
4326 return _toNonSimple().resolveUri(ref); | |
4327 } | |
4328 | |
4329 // The RFC 3986 algorithm merges the base path without its final segment | |
4330 // (anything after the final "/", or everything if the base path doesn't | |
4331 // contain any "/"), and the reference path. | |
4332 // Then it removes "." and ".." segments using the remove-dot-segment | |
4333 // algorithm. | |
4334 // This code combines the two steps. It is simplified by knowing that | |
4335 // the base path contains no "." or ".." segments, and the reference | |
4336 // path can only contain leading ".." segments. | |
4337 | |
4338 String baseUri = base._uri; | |
4339 String refUri = ref._uri; | |
4340 int baseStart = base._pathStart; | |
4341 int baseEnd = base._queryStart; | |
4342 int refStart = ref._pathStart; | |
4343 int refEnd = ref._queryStart; | |
4344 int backCount = 1; | |
4345 | |
4346 int slashCount = 0; | |
4347 | |
4348 // Count leading ".." segments in reference path. | |
4349 while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) { | |
4350 refStart += 3; | |
4351 backCount += 1; | |
4352 } | |
4353 | |
4354 // Extra slash inserted between base and reference path parts if | |
4355 // the base path contains any slashes. | |
4356 // (We could use a slash from the base path in most cases, but not if | |
4357 // we remove the entire base path). | |
4358 String insert = ""; | |
4359 while (baseEnd > baseStart) { | |
4360 baseEnd--; | |
4361 int char = baseUri.codeUnitAt(baseEnd); | |
4362 if (char == _SLASH) { | |
4363 insert = "/"; | |
4364 backCount--; | |
4365 if (backCount == 0) break; | |
4366 } | |
4367 } | |
4368 // If the base URI has no scheme or authority (`_pathStart == 0`) | |
4369 // and a relative path, and we reached the beginning of the path, | |
4370 // we have a special case. | |
4371 if (baseEnd == 0 && !base.hasAbsolutePath) { | |
4372 // Non-RFC 3986 behavior when resolving a purely relative path on top of | |
4373 // another relative path: Don't make the result absolute. | |
4374 insert = ""; | |
4375 } | |
4376 | |
4377 var delta = baseEnd - refStart + insert.length; | |
4378 var newUri = "${base._uri.substring(0, baseEnd)}$insert" | |
4379 "${ref._uri.substring(refStart)}"; | |
4380 | |
4381 return new _SimpleUri(newUri, | |
4382 base._schemeEnd, | |
4383 base._hostStart, | |
4384 base._portStart, | |
4385 base._pathStart, | |
4386 ref._queryStart + delta, | |
4387 ref._fragmentStart + delta, | |
4388 base._schemeCache); | |
4389 } | |
4390 | |
4391 String toFilePath({bool windows}) { | |
4392 if (_schemeEnd >= 0 && !_isFile) { | |
4393 throw new UnsupportedError( | |
4394 "Cannot extract a file path from a $scheme URI"); | |
4395 } | |
4396 if (_queryStart < _uri.length) { | |
4397 if (_queryStart < _fragmentStart) { | |
4398 throw new UnsupportedError( | |
4399 "Cannot extract a file path from a URI with a query component"); | |
4400 } | |
4401 throw new UnsupportedError( | |
4402 "Cannot extract a file path from a URI with a fragment component"); | |
4403 } | |
4404 if (windows == null) windows = _Uri._isWindows; | |
4405 return windows ? _Uri._toWindowsFilePath(this) : _toFilePath(); | |
4406 } | |
4407 | |
4408 String _toFilePath() { | |
4409 if (_hostStart < _portStart) { | |
4410 // Has authority and non-empty host. | |
4411 throw new UnsupportedError( | |
4412 "Cannot extract a non-Windows file path from a file URI " | |
4413 "with an authority"); | |
4414 } | |
4415 return this.path; | |
4416 } | |
4417 | |
4418 UriData get data { | |
4419 assert(scheme != "data"); | |
4420 return null; | |
4421 } | |
4422 | |
4423 int get hashCode => _hashCodeCache ??= _uri.hashCode; | |
4424 | |
4425 bool operator==(Object other) { | |
4426 if (identical(this, other)) return true; | |
4427 if (other is Uri) return _uri == other.toString(); | |
4428 return false; | |
4429 } | |
4430 | |
4431 Uri _toNonSimple() { | |
4432 return new _Uri._internal( | |
4433 this.scheme, | |
4434 this.userInfo, | |
4435 this.hasAuthority ? this.host: null, | |
4436 this.hasPort ? this.port : null, | |
4437 this.path, | |
4438 this.hasQuery ? this.query : null, | |
4439 this.hasFragment ? this.fragment : null | |
4440 ); | |
4441 } | |
4442 | |
4443 String toString() => _uri; | |
4444 } | |
4445 | |
4446 /// Checks whether [text] starts with "data:" at position [start]. | |
4447 /// | |
4448 /// The text must be long enough to allow reading five characters | |
4449 /// from the [start] position. | |
4450 /// | |
4451 /// Returns an integer value which is zero if text starts with all-lowercase | |
4452 /// "data:" and 0x20 if the text starts with "data:" that isn't all lower-case. | |
4453 /// All other values means the text starts with some other character. | |
4454 int _startsWithData(String text, int start) { | |
4455 // Multiply by 3 to avoid a non-colon character making delta be 0x20. | |
4456 int delta = (text.codeUnitAt(start + 4) ^ _COLON) * 3; | |
4457 delta |= text.codeUnitAt(start) ^ 0x64 /*d*/; | |
4458 delta |= text.codeUnitAt(start + 1) ^ 0x61 /*a*/; | |
4459 delta |= text.codeUnitAt(start + 2) ^ 0x74 /*t*/; | |
4460 delta |= text.codeUnitAt(start + 3) ^ 0x61 /*a*/; | |
4461 return delta; | |
4462 } | |
4463 | |
4464 /// Helper function returning the length of a string, or `0` for `null`. | |
4465 int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | |
OLD | NEW |