OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of dart.core; | |
6 | |
7 /** | |
8 * A parsed URI, such as a URL. | |
9 * | |
10 * **See also:** | |
11 * | |
12 * * [URIs][uris] in the [library tour][libtour] | |
13 * * [RFC-3986](http://tools.ietf.org/html/rfc3986) | |
14 * | |
15 * [uris]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html#c
h03-uri | |
16 * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.htm
l | |
17 */ | |
18 class Uri { | |
19 /** | |
20 * The scheme component of the URI. | |
21 * | |
22 * Returns the empty string if there is no scheme component. | |
23 * | |
24 * A URI scheme is case insensitive. | |
25 * The returned scheme is canonicalized to lowercase letters. | |
26 */ | |
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 | |
80 /// Internal non-verifying constructor. Only call with validated arguments. | |
81 Uri._internal(this.scheme, | |
82 this._userInfo, | |
83 this._host, | |
84 this._port, | |
85 this._path, | |
86 this._query, | |
87 this._fragment); | |
88 | |
89 /** | |
90 * Creates a new URI from its components. | |
91 * | |
92 * Each component is set through a named argument. Any number of | |
93 * components can be provided. The [path] and [query] components can be set | |
94 * using either of two different named arguments. | |
95 * | |
96 * The scheme component is set through [scheme]. The scheme is | |
97 * normalized to all lowercase letters. If the scheme is omitted or empty, | |
98 * the URI will not have a scheme part. | |
99 * | |
100 * The user info part of the authority component is set through | |
101 * [userInfo]. It defaults to the empty string, which will be omitted | |
102 * from the string representation of the URI. | |
103 * | |
104 * The host part of the authority component is set through | |
105 * [host]. The host can either be a hostname, an IPv4 address or an | |
106 * IPv6 address, contained in '[' and ']'. If the host contains a | |
107 * ':' character, the '[' and ']' are added if not already provided. | |
108 * The host is normalized to all lowercase letters. | |
109 * | |
110 * The port part of the authority component is set through | |
111 * [port]. | |
112 * If [port] is omitted or `null`, it implies the default port for | |
113 * the URI's scheme, and is equivalent to passing that port explicitly. | |
114 * The recognized schemes, and their default ports, are "http" (80) and | |
115 * "https" (443). All other schemes are considered as having zero as the | |
116 * default port. | |
117 * | |
118 * If any of `userInfo`, `host` or `port` are provided, | |
119 * the URI will have an autority according to [hasAuthority]. | |
120 * | |
121 * The path component is set through either [path] or | |
122 * [pathSegments]. When [path] is used, it should be a valid URI path, | |
123 * but invalid characters, except the general delimiters ':/@[]?#', | |
124 * will be escaped if necessary. | |
125 * When [pathSegments] is used, each of the provided segments | |
126 * is first percent-encoded and then joined using the forward slash | |
127 * separator. The percent-encoding of the path segments encodes all | |
128 * characters except for the unreserved characters and the following | |
129 * list of characters: `!$&'()*+,;=:@`. If the other components | |
130 * calls for an absolute path a leading slash `/` is prepended if | |
131 * not already there. | |
132 * | |
133 * The query component is set through either [query] or | |
134 * [queryParameters]. When [query] is used the provided string should | |
135 * be a valid URI query, but invalid characters other than general delimiters, | |
136 * will be escaped if necessary. | |
137 * When [queryParameters] is used the query is built from the | |
138 * provided map. Each key and value in the map is percent-encoded | |
139 * and joined using equal and ampersand characters. The | |
140 * percent-encoding of the keys and values encodes all characters | |
141 * except for the unreserved characters. | |
142 * If `query` is the empty string, it is equivalent to omitting it. | |
143 * To have an actual empty query part, | |
144 * use an empty list for `queryParameters`. | |
145 * If both `query` and `queryParameters` are omitted or `null`, the | |
146 * URI will have no query part. | |
147 * | |
148 * The fragment component is set through [fragment]. | |
149 * It should be a valid URI fragment, but invalid characters other than | |
150 * general delimiters, will be escaped if necessary. | |
151 * If `fragment` is omitted or `null`, the URI will have no fragment part. | |
152 */ | |
153 factory Uri({String scheme : "", | |
154 String userInfo : "", | |
155 String host, | |
156 int port, | |
157 String path, | |
158 Iterable<String> pathSegments, | |
159 String query, | |
160 Map<String, String> queryParameters, | |
161 String fragment}) { | |
162 scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); | |
163 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); | |
164 host = _makeHost(host, 0, _stringOrNullLength(host), false); | |
165 // Special case this constructor for backwards compatibility. | |
166 if (query == "") query = null; | |
167 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
168 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); | |
169 port = _makePort(port, scheme); | |
170 bool isFile = (scheme == "file"); | |
171 if (host == null && | |
172 (userInfo.isNotEmpty || port != null || isFile)) { | |
173 host = ""; | |
174 } | |
175 bool hasAuthority = (host != null); | |
176 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
177 scheme, hasAuthority); | |
178 if (scheme.isEmpty && host == null && !path.startsWith('/')) { | |
179 path = _normalizeRelativePath(path); | |
180 } else { | |
181 path = _removeDotSegments(path); | |
182 } | |
183 return new Uri._internal(scheme, userInfo, host, port, | |
184 path, query, fragment); | |
185 } | |
186 | |
187 /** | |
188 * Creates a new `http` URI from authority, path and query. | |
189 * | |
190 * Examples: | |
191 * | |
192 * ``` | |
193 * // http://example.org/path?q=dart. | |
194 * new Uri.http("google.com", "/search", { "q" : "dart" }); | |
195 * | |
196 * // http://user:pass@localhost:8080 | |
197 * new Uri.http("user:pass@localhost:8080", ""); | |
198 * | |
199 * // http://example.org/a%20b | |
200 * new Uri.http("example.org", "a b"); | |
201 * | |
202 * // http://example.org/a%252F | |
203 * new Uri.http("example.org", "/a%2F"); | |
204 * ``` | |
205 * | |
206 * The `scheme` is always set to `http`. | |
207 * | |
208 * The `userInfo`, `host` and `port` components are set from the | |
209 * [authority] argument. If `authority` is `null` or empty, | |
210 * the created `Uri` will have no authority, and will not be directly usable | |
211 * as an HTTP URL, which must have a non-empty host. | |
212 * | |
213 * The `path` component is set from the [unencodedPath] | |
214 * argument. The path passed must not be encoded as this constructor | |
215 * encodes the path. | |
216 * | |
217 * The `query` component is set from the optional [queryParameters] | |
218 * argument. | |
219 */ | |
220 factory Uri.http(String authority, | |
221 String unencodedPath, | |
222 [Map<String, String> queryParameters]) { | |
223 return _makeHttpUri("http", authority, unencodedPath, queryParameters); | |
224 } | |
225 | |
226 /** | |
227 * Creates a new `https` URI from authority, path and query. | |
228 * | |
229 * This constructor is the same as [Uri.http] except for the scheme | |
230 * which is set to `https`. | |
231 */ | |
232 factory Uri.https(String authority, | |
233 String unencodedPath, | |
234 [Map<String, String> queryParameters]) { | |
235 return _makeHttpUri("https", authority, unencodedPath, queryParameters); | |
236 } | |
237 | |
238 /** | |
239 * Returns the authority component. | |
240 * | |
241 * The authority is formatted from the [userInfo], [host] and [port] | |
242 * parts. | |
243 * | |
244 * Returns the empty string if there is no authority component. | |
245 */ | |
246 String get authority { | |
247 if (!hasAuthority) return ""; | |
248 var sb = new StringBuffer(); | |
249 _writeAuthority(sb); | |
250 return sb.toString(); | |
251 } | |
252 | |
253 /** | |
254 * Returns the user info part of the authority component. | |
255 * | |
256 * Returns the empty string if there is no user info in the | |
257 * authority component. | |
258 */ | |
259 String get userInfo => _userInfo; | |
260 | |
261 /** | |
262 * Returns the host part of the authority component. | |
263 * | |
264 * Returns the empty string if there is no authority component and | |
265 * hence no host. | |
266 * | |
267 * If the host is an IP version 6 address, the surrounding `[` and `]` is | |
268 * removed. | |
269 * | |
270 * The host string is case-insensitive. | |
271 * The returned host name is canonicalized to lower-case | |
272 * with upper-case percent-escapes. | |
273 */ | |
274 String get host { | |
275 if (_host == null) return ""; | |
276 if (_host.startsWith('[')) { | |
277 return _host.substring(1, _host.length - 1); | |
278 } | |
279 return _host; | |
280 } | |
281 | |
282 /** | |
283 * Returns the port part of the authority component. | |
284 * | |
285 * Returns the defualt port if there is no port number in the authority | |
286 * component. That's 80 for http, 443 for https, and 0 for everything else. | |
287 */ | |
288 int get port { | |
289 if (_port == null) return _defaultPort(scheme); | |
290 return _port; | |
291 } | |
292 | |
293 // The default port for the scheme of this Uri.. | |
294 static int _defaultPort(String scheme) { | |
295 if (scheme == "http") return 80; | |
296 if (scheme == "https") return 443; | |
297 return 0; | |
298 } | |
299 | |
300 /** | |
301 * Returns the path component. | |
302 * | |
303 * The returned path is encoded. To get direct access to the decoded | |
304 * path use [pathSegments]. | |
305 * | |
306 * Returns the empty string if there is no path component. | |
307 */ | |
308 String get path => _path; | |
309 | |
310 /** | |
311 * Returns the query component. The returned query is encoded. To get | |
312 * direct access to the decoded query use [queryParameters]. | |
313 * | |
314 * Returns the empty string if there is no query component. | |
315 */ | |
316 String get query => (_query == null) ? "" : _query; | |
317 | |
318 /** | |
319 * Returns the fragment identifier component. | |
320 * | |
321 * Returns the empty string if there is no fragment identifier | |
322 * component. | |
323 */ | |
324 String get fragment => (_fragment == null) ? "" : _fragment; | |
325 | |
326 /** | |
327 * Creates a new `Uri` object by parsing a URI string. | |
328 * | |
329 * If [start] and [end] are provided, only the substring from `start` | |
330 * to `end` is parsed as a URI. | |
331 * | |
332 * If the string is not valid as a URI or URI reference, | |
333 * a [FormatException] is thrown. | |
334 */ | |
335 static Uri parse(String uri, [int start = 0, int end]) { | |
336 // This parsing will not validate percent-encoding, IPv6, etc. | |
337 // When done splitting into parts, it will call, e.g., [_makeFragment] | |
338 // to do the final parsing. | |
339 // | |
340 // Important parts of the RFC 3986 used here: | |
341 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] | |
342 // | |
343 // hier-part = "//" authority path-abempty | |
344 // / path-absolute | |
345 // / path-rootless | |
346 // / path-empty | |
347 // | |
348 // URI-reference = URI / relative-ref | |
349 // | |
350 // absolute-URI = scheme ":" hier-part [ "?" query ] | |
351 // | |
352 // relative-ref = relative-part [ "?" query ] [ "#" fragment ] | |
353 // | |
354 // relative-part = "//" authority path-abempty | |
355 // / path-absolute | |
356 // / path-noscheme | |
357 // / path-empty | |
358 // | |
359 // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) | |
360 // | |
361 // authority = [ userinfo "@" ] host [ ":" port ] | |
362 // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) | |
363 // host = IP-literal / IPv4address / reg-name | |
364 // port = *DIGIT | |
365 // reg-name = *( unreserved / pct-encoded / sub-delims ) | |
366 // | |
367 // path = path-abempty ; begins with "/" or is empty | |
368 // / path-absolute ; begins with "/" but not "//" | |
369 // / path-noscheme ; begins with a non-colon segment | |
370 // / path-rootless ; begins with a segment | |
371 // / path-empty ; zero characters | |
372 // | |
373 // path-abempty = *( "/" segment ) | |
374 // path-absolute = "/" [ segment-nz *( "/" segment ) ] | |
375 // path-noscheme = segment-nz-nc *( "/" segment ) | |
376 // path-rootless = segment-nz *( "/" segment ) | |
377 // path-empty = 0<pchar> | |
378 // | |
379 // segment = *pchar | |
380 // segment-nz = 1*pchar | |
381 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | |
382 // ; non-zero-length segment without any colon ":" | |
383 // | |
384 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | |
385 // | |
386 // query = *( pchar / "/" / "?" ) | |
387 // | |
388 // fragment = *( pchar / "/" / "?" ) | |
389 const int EOI = -1; | |
390 | |
391 String scheme = ""; | |
392 String userinfo = ""; | |
393 String host = null; | |
394 int port = null; | |
395 String path = null; | |
396 String query = null; | |
397 String fragment = null; | |
398 if (end == null) end = uri.length; | |
399 | |
400 int index = start; | |
401 int pathStart = start; | |
402 // End of input-marker. | |
403 int char = EOI; | |
404 | |
405 void parseAuth() { | |
406 if (index == end) { | |
407 char = EOI; | |
408 return; | |
409 } | |
410 int authStart = index; | |
411 int lastColon = -1; | |
412 int lastAt = -1; | |
413 char = uri.codeUnitAt(index); | |
414 while (index < end) { | |
415 char = uri.codeUnitAt(index); | |
416 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { | |
417 break; | |
418 } | |
419 if (char == _AT_SIGN) { | |
420 lastAt = index; | |
421 lastColon = -1; | |
422 } else if (char == _COLON) { | |
423 lastColon = index; | |
424 } else if (char == _LEFT_BRACKET) { | |
425 lastColon = -1; | |
426 int endBracket = uri.indexOf(']', index + 1); | |
427 if (endBracket == -1) { | |
428 index = end; | |
429 char = EOI; | |
430 break; | |
431 } else { | |
432 index = endBracket; | |
433 } | |
434 } | |
435 index++; | |
436 char = EOI; | |
437 } | |
438 int hostStart = authStart; | |
439 int hostEnd = index; | |
440 if (lastAt >= 0) { | |
441 userinfo = _makeUserInfo(uri, authStart, lastAt); | |
442 hostStart = lastAt + 1; | |
443 } | |
444 if (lastColon >= 0) { | |
445 int portNumber; | |
446 if (lastColon + 1 < index) { | |
447 portNumber = 0; | |
448 for (int i = lastColon + 1; i < index; i++) { | |
449 int digit = uri.codeUnitAt(i); | |
450 if (_ZERO > digit || _NINE < digit) { | |
451 _fail(uri, i, "Invalid port number"); | |
452 } | |
453 portNumber = portNumber * 10 + (digit - _ZERO); | |
454 } | |
455 } | |
456 port = _makePort(portNumber, scheme); | |
457 hostEnd = lastColon; | |
458 } | |
459 host = _makeHost(uri, hostStart, hostEnd, true); | |
460 if (index < end) { | |
461 char = uri.codeUnitAt(index); | |
462 } | |
463 } | |
464 | |
465 // When reaching path parsing, the current character is known to not | |
466 // be part of the path. | |
467 const int NOT_IN_PATH = 0; | |
468 // When reaching path parsing, the current character is part | |
469 // of the a non-empty path. | |
470 const int IN_PATH = 1; | |
471 // When reaching authority parsing, authority is possible. | |
472 // This is only true at start or right after scheme. | |
473 const int ALLOW_AUTH = 2; | |
474 | |
475 // Current state. | |
476 // Initialized to the default value that is used when exiting the | |
477 // scheme loop by reaching the end of input. | |
478 // All other breaks set their own state. | |
479 int state = NOT_IN_PATH; | |
480 int i = index; // Temporary alias for index to avoid bug 19550 in dart2js. | |
481 while (i < end) { | |
482 char = uri.codeUnitAt(i); | |
483 if (char == _QUESTION || char == _NUMBER_SIGN) { | |
484 state = NOT_IN_PATH; | |
485 break; | |
486 } | |
487 if (char == _SLASH) { | |
488 state = (i == start) ? ALLOW_AUTH : IN_PATH; | |
489 break; | |
490 } | |
491 if (char == _COLON) { | |
492 if (i == start) _fail(uri, start, "Invalid empty scheme"); | |
493 scheme = _makeScheme(uri, start, i); | |
494 i++; | |
495 pathStart = i; | |
496 if (i == end) { | |
497 char = EOI; | |
498 state = NOT_IN_PATH; | |
499 } else { | |
500 char = uri.codeUnitAt(i); | |
501 if (char == _QUESTION || char == _NUMBER_SIGN) { | |
502 state = NOT_IN_PATH; | |
503 } else if (char == _SLASH) { | |
504 state = ALLOW_AUTH; | |
505 } else { | |
506 state = IN_PATH; | |
507 } | |
508 } | |
509 break; | |
510 } | |
511 i++; | |
512 char = EOI; | |
513 } | |
514 index = i; // Remove alias when bug is fixed. | |
515 | |
516 if (state == ALLOW_AUTH) { | |
517 assert(char == _SLASH); | |
518 // Have seen one slash either at start or right after scheme. | |
519 // If two slashes, it's an authority, otherwise it's just the path. | |
520 index++; | |
521 if (index == end) { | |
522 char = EOI; | |
523 state = NOT_IN_PATH; | |
524 } else { | |
525 char = uri.codeUnitAt(index); | |
526 if (char == _SLASH) { | |
527 index++; | |
528 parseAuth(); | |
529 pathStart = index; | |
530 } | |
531 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { | |
532 state = NOT_IN_PATH; | |
533 } else { | |
534 state = IN_PATH; | |
535 } | |
536 } | |
537 } | |
538 | |
539 assert(state == IN_PATH || state == NOT_IN_PATH); | |
540 if (state == IN_PATH) { | |
541 // Characters from pathStart to index (inclusive) are known | |
542 // to be part of the path. | |
543 while (++index < end) { | |
544 char = uri.codeUnitAt(index); | |
545 if (char == _QUESTION || char == _NUMBER_SIGN) { | |
546 break; | |
547 } | |
548 char = EOI; | |
549 } | |
550 state = NOT_IN_PATH; | |
551 } | |
552 | |
553 assert(state == NOT_IN_PATH); | |
554 bool hasAuthority = (host != null); | |
555 path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); | |
556 | |
557 if (char == _QUESTION) { | |
558 int numberSignIndex = -1; | |
559 for (int i = index + 1; i < end; i++) { | |
560 if (uri.codeUnitAt(i) == _NUMBER_SIGN) { | |
561 numberSignIndex = i; | |
562 break; | |
563 } | |
564 } | |
565 if (numberSignIndex < 0) { | |
566 query = _makeQuery(uri, index + 1, end, null); | |
567 } else { | |
568 query = _makeQuery(uri, index + 1, numberSignIndex, null); | |
569 fragment = _makeFragment(uri, numberSignIndex + 1, end); | |
570 } | |
571 } else if (char == _NUMBER_SIGN) { | |
572 fragment = _makeFragment(uri, index + 1, end); | |
573 } | |
574 return new Uri._internal(scheme, | |
575 userinfo, | |
576 host, | |
577 port, | |
578 path, | |
579 query, | |
580 fragment); | |
581 } | |
582 | |
583 // Report a parse failure. | |
584 static void _fail(String uri, int index, String message) { | |
585 throw new FormatException(message, uri, index); | |
586 } | |
587 | |
588 static Uri _makeHttpUri(String scheme, | |
589 String authority, | |
590 String unencodedPath, | |
591 Map<String, String> queryParameters) { | |
592 var userInfo = ""; | |
593 var host = null; | |
594 var port = null; | |
595 | |
596 if (authority != null && authority.isNotEmpty) { | |
597 var hostStart = 0; | |
598 // Split off the user info. | |
599 bool hasUserInfo = false; | |
600 for (int i = 0; i < authority.length; i++) { | |
601 if (authority.codeUnitAt(i) == _AT_SIGN) { | |
602 hasUserInfo = true; | |
603 userInfo = authority.substring(0, i); | |
604 hostStart = i + 1; | |
605 break; | |
606 } | |
607 } | |
608 var hostEnd = hostStart; | |
609 if (hostStart < authority.length && | |
610 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | |
611 // IPv6 host. | |
612 for (; hostEnd < authority.length; hostEnd++) { | |
613 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | |
614 } | |
615 if (hostEnd == authority.length) { | |
616 throw new FormatException("Invalid IPv6 host entry.", | |
617 authority, hostStart); | |
618 } | |
619 parseIPv6Address(authority, hostStart + 1, hostEnd); | |
620 hostEnd++; // Skip the closing bracket. | |
621 if (hostEnd != authority.length && | |
622 authority.codeUnitAt(hostEnd) != _COLON) { | |
623 throw new FormatException("Invalid end of authority", | |
624 authority, hostEnd); | |
625 } | |
626 } | |
627 // Split host and port. | |
628 bool hasPort = false; | |
629 for (; hostEnd < authority.length; hostEnd++) { | |
630 if (authority.codeUnitAt(hostEnd) == _COLON) { | |
631 var portString = authority.substring(hostEnd + 1); | |
632 // We allow the empty port - falling back to initial value. | |
633 if (portString.isNotEmpty) port = int.parse(portString); | |
634 break; | |
635 } | |
636 } | |
637 host = authority.substring(hostStart, hostEnd); | |
638 } | |
639 return new Uri(scheme: scheme, | |
640 userInfo: userInfo, | |
641 host: host, | |
642 port: port, | |
643 pathSegments: unencodedPath.split("/"), | |
644 queryParameters: queryParameters); | |
645 } | |
646 | |
647 /** | |
648 * Creates a new file URI from an absolute or relative file path. | |
649 * | |
650 * The file path is passed in [path]. | |
651 * | |
652 * This path is interpreted using either Windows or non-Windows | |
653 * semantics. | |
654 * | |
655 * With non-Windows semantics the slash ("/") is used to separate | |
656 * path segments. | |
657 * | |
658 * With Windows semantics, backslash ("\") and forward-slash ("/") | |
659 * are used to separate path segments, except if the path starts | |
660 * with "\\?\" in which case, only backslash ("\") separates path | |
661 * segments. | |
662 * | |
663 * If the path starts with a path separator an absolute URI is | |
664 * created. Otherwise a relative URI is created. One exception from | |
665 * this rule is that when Windows semantics is used and the path | |
666 * starts with a drive letter followed by a colon (":") and a | |
667 * path separator then an absolute URI is created. | |
668 * | |
669 * The default for whether to use Windows or non-Windows semantics | |
670 * determined from the platform Dart is running on. When running in | |
671 * the standalone VM this is detected by the VM based on the | |
672 * operating system. When running in a browser non-Windows semantics | |
673 * is always used. | |
674 * | |
675 * To override the automatic detection of which semantics to use pass | |
676 * a value for [windows]. Passing `true` will use Windows | |
677 * semantics and passing `false` will use non-Windows semantics. | |
678 * | |
679 * Examples using non-Windows semantics: | |
680 * | |
681 * ``` | |
682 * // xxx/yyy | |
683 * new Uri.file("xxx/yyy", windows: false); | |
684 * | |
685 * // xxx/yyy/ | |
686 * new Uri.file("xxx/yyy/", windows: false); | |
687 * | |
688 * // file:///xxx/yyy | |
689 * new Uri.file("/xxx/yyy", windows: false); | |
690 * | |
691 * // file:///xxx/yyy/ | |
692 * new Uri.file("/xxx/yyy/", windows: false); | |
693 * | |
694 * // C: | |
695 * new Uri.file("C:", windows: false); | |
696 * ``` | |
697 * | |
698 * Examples using Windows semantics: | |
699 * | |
700 * ``` | |
701 * // xxx/yyy | |
702 * new Uri.file(r"xxx\yyy", windows: true); | |
703 * | |
704 * // xxx/yyy/ | |
705 * new Uri.file(r"xxx\yyy\", windows: true); | |
706 * | |
707 * file:///xxx/yyy | |
708 * new Uri.file(r"\xxx\yyy", windows: true); | |
709 * | |
710 * file:///xxx/yyy/ | |
711 * new Uri.file(r"\xxx\yyy/", windows: true); | |
712 * | |
713 * // file:///C:/xxx/yyy | |
714 * new Uri.file(r"C:\xxx\yyy", windows: true); | |
715 * | |
716 * // This throws an error. A path with a drive letter is not absolute. | |
717 * new Uri.file(r"C:", windows: true); | |
718 * | |
719 * // This throws an error. A path with a drive letter is not absolute. | |
720 * new Uri.file(r"C:xxx\yyy", windows: true); | |
721 * | |
722 * // file://server/share/file | |
723 * new Uri.file(r"\\server\share\file", windows: true); | |
724 * ``` | |
725 * | |
726 * If the path passed is not a legal file path [ArgumentError] is thrown. | |
727 */ | |
728 factory Uri.file(String path, {bool windows}) { | |
729 windows = (windows == null) ? Uri._isWindows : windows; | |
730 return windows ? _makeWindowsFileUrl(path, false) | |
731 : _makeFileUri(path, false); | |
732 } | |
733 | |
734 /** | |
735 * Like [Uri.file] except that a non-empty URI path ends in a slash. | |
736 * | |
737 * If [path] is not empty, and it doesn't end in a directory separator, | |
738 * then a slash is added to the returned URI's path. | |
739 * In all other cases, the result is the same as returned by `Uri.file`. | |
740 */ | |
741 factory Uri.directory(String path, {bool windows}) { | |
742 windows = (windows == null) ? Uri._isWindows : windows; | |
743 return windows ? _makeWindowsFileUrl(path, true) | |
744 : _makeFileUri(path, true); | |
745 } | |
746 | |
747 /** | |
748 * Returns the natural base URI for the current platform. | |
749 * | |
750 * When running in a browser this is the current URL (from | |
751 * `window.location.href`). | |
752 * | |
753 * When not running in a browser this is the file URI referencing | |
754 * the current working directory. | |
755 */ | |
756 external static Uri get base; | |
757 | |
758 external static bool get _isWindows; | |
759 | |
760 static _checkNonWindowsPathReservedCharacters(List<String> segments, | |
761 bool argumentError) { | |
762 segments.forEach((segment) { | |
763 if (segment.contains("/")) { | |
764 if (argumentError) { | |
765 throw new ArgumentError("Illegal path character $segment"); | |
766 } else { | |
767 throw new UnsupportedError("Illegal path character $segment"); | |
768 } | |
769 } | |
770 }); | |
771 } | |
772 | |
773 static _checkWindowsPathReservedCharacters(List<String> segments, | |
774 bool argumentError, | |
775 [int firstSegment = 0]) { | |
776 for (var segment in segments.skip(firstSegment)) { | |
777 if (segment.contains(new RegExp(r'["*/:<>?\\|]'))) { | |
778 if (argumentError) { | |
779 throw new ArgumentError("Illegal character in path"); | |
780 } else { | |
781 throw new UnsupportedError("Illegal character in path"); | |
782 } | |
783 } | |
784 } | |
785 } | |
786 | |
787 static _checkWindowsDriveLetter(int charCode, bool argumentError) { | |
788 if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) || | |
789 (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) { | |
790 return; | |
791 } | |
792 if (argumentError) { | |
793 throw new ArgumentError("Illegal drive letter " + | |
794 new String.fromCharCode(charCode)); | |
795 } else { | |
796 throw new UnsupportedError("Illegal drive letter " + | |
797 new String.fromCharCode(charCode)); | |
798 } | |
799 } | |
800 | |
801 static _makeFileUri(String path, bool slashTerminated) { | |
802 const String sep = "/"; | |
803 var segments = path.split(sep); | |
804 if (slashTerminated && | |
805 segments.isNotEmpty && | |
806 segments.last.isNotEmpty) { | |
807 segments.add(""); // Extra separator at end. | |
808 } | |
809 if (path.startsWith(sep)) { | |
810 // Absolute file:// URI. | |
811 return new Uri(scheme: "file", pathSegments: segments); | |
812 } else { | |
813 // Relative URI. | |
814 return new Uri(pathSegments: segments); | |
815 } | |
816 } | |
817 | |
818 static _makeWindowsFileUrl(String path, bool slashTerminated) { | |
819 if (path.startsWith(r"\\?\")) { | |
820 if (path.startsWith(r"UNC\", 4)) { | |
821 path = path.replaceRange(0, 7, r'\'); | |
822 } else { | |
823 path = path.substring(4); | |
824 if (path.length < 3 || | |
825 path.codeUnitAt(1) != _COLON || | |
826 path.codeUnitAt(2) != _BACKSLASH) { | |
827 throw new ArgumentError( | |
828 r"Windows paths with \\?\ prefix must be absolute"); | |
829 } | |
830 } | |
831 } else { | |
832 path = path.replaceAll("/", r'\'); | |
833 } | |
834 const String sep = r'\'; | |
835 if (path.length > 1 && path.codeUnitAt(1) == _COLON) { | |
836 _checkWindowsDriveLetter(path.codeUnitAt(0), true); | |
837 if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) { | |
838 throw new ArgumentError( | |
839 "Windows paths with drive letter must be absolute"); | |
840 } | |
841 // Absolute file://C:/ URI. | |
842 var pathSegments = path.split(sep); | |
843 if (slashTerminated && | |
844 pathSegments.last.isNotEmpty) { | |
845 pathSegments.add(""); // Extra separator at end. | |
846 } | |
847 _checkWindowsPathReservedCharacters(pathSegments, true, 1); | |
848 return new Uri(scheme: "file", pathSegments: pathSegments); | |
849 } | |
850 | |
851 if (path.startsWith(sep)) { | |
852 if (path.startsWith(sep, 1)) { | |
853 // Absolute file:// URI with host. | |
854 int pathStart = path.indexOf(r'\', 2); | |
855 String hostPart = | |
856 (pathStart < 0) ? path.substring(2) : path.substring(2, pathStart); | |
857 String pathPart = | |
858 (pathStart < 0) ? "" : path.substring(pathStart + 1); | |
859 var pathSegments = pathPart.split(sep); | |
860 _checkWindowsPathReservedCharacters(pathSegments, true); | |
861 if (slashTerminated && | |
862 pathSegments.last.isNotEmpty) { | |
863 pathSegments.add(""); // Extra separator at end. | |
864 } | |
865 return new Uri( | |
866 scheme: "file", host: hostPart, pathSegments: pathSegments); | |
867 } else { | |
868 // Absolute file:// URI. | |
869 var pathSegments = path.split(sep); | |
870 if (slashTerminated && | |
871 pathSegments.last.isNotEmpty) { | |
872 pathSegments.add(""); // Extra separator at end. | |
873 } | |
874 _checkWindowsPathReservedCharacters(pathSegments, true); | |
875 return new Uri(scheme: "file", pathSegments: pathSegments); | |
876 } | |
877 } else { | |
878 // Relative URI. | |
879 var pathSegments = path.split(sep); | |
880 _checkWindowsPathReservedCharacters(pathSegments, true); | |
881 if (slashTerminated && | |
882 pathSegments.isNotEmpty && | |
883 pathSegments.last.isNotEmpty) { | |
884 pathSegments.add(""); // Extra separator at end. | |
885 } | |
886 return new Uri(pathSegments: pathSegments); | |
887 } | |
888 } | |
889 | |
890 /** | |
891 * Returns a new `Uri` based on this one, but with some parts replaced. | |
892 * | |
893 * This method takes the same parameters as the [new Uri] constructor, | |
894 * and they have the same meaning. | |
895 * | |
896 * At most one of [path] and [pathSegments] must be provided. | |
897 * Likewise, at most one of [query] and [queryParameters] must be provided. | |
898 * | |
899 * Each part that is not provided will default to the corresponding | |
900 * value from this `Uri` instead. | |
901 * | |
902 * This method is different from [Uri.resolve] which overrides in a | |
903 * hierarchial manner, | |
904 * and can instead replace each part of a `Uri` individually. | |
905 * | |
906 * Example: | |
907 * | |
908 * Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); | |
909 * Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); | |
910 * print(uri2); // prints "A://b@c:4/D/E/E/?f#G" | |
911 * | |
912 * This method acts similarly to using the `new Uri` constructor with | |
913 * some of the arguments taken from this `Uri` . Example: | |
914 * | |
915 * Uri uri3 = new Uri( | |
916 * scheme: "A", | |
917 * userInfo: uri1.userInfo, | |
918 * host: uri1.host, | |
919 * port: uri1.port, | |
920 * path: "D/E/E", | |
921 * query: uri1.query, | |
922 * fragment: "G"); | |
923 * print(uri3); // prints "A://b@c:4/D/E/E/?f#G" | |
924 * print(uri2 == uri3); // prints true. | |
925 * | |
926 * Using this method can be seen as a shorthand for the `Uri` constructor | |
927 * call above, but may also be slightly faster because the parts taken | |
928 * from this `Uri` need not be checked for validity again. | |
929 */ | |
930 Uri replace({String scheme, | |
931 String userInfo, | |
932 String host, | |
933 int port, | |
934 String path, | |
935 Iterable<String> pathSegments, | |
936 String query, | |
937 Map<String, String> queryParameters, | |
938 String fragment}) { | |
939 // Set to true if the scheme has (potentially) changed. | |
940 // In that case, the default port may also have changed and we need | |
941 // to check even the existing port. | |
942 bool schemeChanged = false; | |
943 if (scheme != null) { | |
944 scheme = _makeScheme(scheme, 0, scheme.length); | |
945 schemeChanged = true; | |
946 } else { | |
947 scheme = this.scheme; | |
948 } | |
949 bool isFile = (scheme == "file"); | |
950 if (userInfo != null) { | |
951 userInfo = _makeUserInfo(userInfo, 0, userInfo.length); | |
952 } else { | |
953 userInfo = this._userInfo; | |
954 } | |
955 if (port != null) { | |
956 port = _makePort(port, scheme); | |
957 } else { | |
958 port = this._port; | |
959 if (schemeChanged) { | |
960 // The default port might have changed. | |
961 port = _makePort(port, scheme); | |
962 } | |
963 } | |
964 if (host != null) { | |
965 host = _makeHost(host, 0, host.length, false); | |
966 } else if (this.hasAuthority) { | |
967 host = this._host; | |
968 } else if (userInfo.isNotEmpty || port != null || isFile) { | |
969 host = ""; | |
970 } | |
971 | |
972 bool hasAuthority = host != null; | |
973 if (path != null || pathSegments != null) { | |
974 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, | |
975 scheme, hasAuthority); | |
976 } else { | |
977 path = this._path; | |
978 if ((isFile || (hasAuthority && !path.isEmpty)) && | |
979 !path.startsWith('/')) { | |
980 path = "/" + path; | |
981 } | |
982 } | |
983 | |
984 if (query != null || queryParameters != null) { | |
985 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); | |
986 } else { | |
987 query = this._query; | |
988 } | |
989 | |
990 if (fragment != null) { | |
991 fragment = _makeFragment(fragment, 0, fragment.length); | |
992 } else { | |
993 fragment = this._fragment; | |
994 } | |
995 | |
996 return new Uri._internal( | |
997 scheme, userInfo, host, port, path, query, fragment); | |
998 } | |
999 | |
1000 /** | |
1001 * Returns a `Uri` that differs from this only in not having a fragment. | |
1002 * | |
1003 * If this `Uri` does not have a fragment, it is itself returned. | |
1004 */ | |
1005 Uri removeFragment() { | |
1006 if (!this.hasFragment) return this; | |
1007 return new Uri._internal(scheme, _userInfo, _host, _port, | |
1008 _path, _query, null); | |
1009 } | |
1010 | |
1011 /** | |
1012 * Returns the URI path split into its segments. Each of the | |
1013 * segments in the returned list have been decoded. If the path is | |
1014 * empty the empty list will be returned. A leading slash `/` does | |
1015 * not affect the segments returned. | |
1016 * | |
1017 * The returned list is unmodifiable and will throw [UnsupportedError] on any | |
1018 * calls that would mutate it. | |
1019 */ | |
1020 List<String> get pathSegments { | |
1021 if (_pathSegments == null) { | |
1022 var pathToSplit = !path.isEmpty && path.codeUnitAt(0) == _SLASH | |
1023 ? path.substring(1) | |
1024 : path; | |
1025 _pathSegments = new UnmodifiableListView( | |
1026 pathToSplit == "" ? const<String>[] | |
1027 : pathToSplit.split("/") | |
1028 .map(Uri.decodeComponent) | |
1029 .toList(growable: false)); | |
1030 } | |
1031 return _pathSegments; | |
1032 } | |
1033 | |
1034 /** | |
1035 * Returns the URI query split into a map according to the rules | |
1036 * specified for FORM post in the [HTML 4.01 specification section 17.13.4] | |
1037 * (http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 | |
1038 * "HTML 4.01 section 17.13.4"). Each key and value in the returned map | |
1039 * has been decoded. If there is no query the empty map is returned. | |
1040 * | |
1041 * Keys in the query string that have no value are mapped to the | |
1042 * empty string. | |
1043 * | |
1044 * The returned map is unmodifiable and will throw [UnsupportedError] on any | |
1045 * calls that would mutate it. | |
1046 */ | |
1047 Map<String, String> get queryParameters { | |
1048 if (_queryParameters == null) { | |
1049 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); | |
1050 } | |
1051 return _queryParameters; | |
1052 } | |
1053 | |
1054 /** | |
1055 * Returns a URI where the path has been normalized. | |
1056 * | |
1057 * A normalized path does not contain `.` segments or non-leading `..` | |
1058 * segments. | |
1059 * Only a relative path with no scheme or authority may contain | |
1060 * leading `..` segments, | |
1061 * a path that starts with `/` will also drop any leading `..` segments. | |
1062 * | |
1063 * This uses the same normalization strategy as `new Uri().resolve(this)`. | |
1064 * | |
1065 * Does not change any part of the URI except the path. | |
1066 * | |
1067 * The default implementation of `Uri` always normalizes paths, so calling | |
1068 * this function has no effect. | |
1069 */ | |
1070 Uri normalizePath() { | |
1071 String path = _normalizePath(_path, scheme, hasAuthority); | |
1072 if (identical(path, _path)) return this; | |
1073 return this.replace(path: path); | |
1074 } | |
1075 | |
1076 static int _makePort(int port, String scheme) { | |
1077 // Perform scheme specific normalization. | |
1078 if (port != null && port == _defaultPort(scheme)) return null; | |
1079 return port; | |
1080 } | |
1081 | |
1082 /** | |
1083 * Check and normalize a host name. | |
1084 * | |
1085 * If the host name starts and ends with '[' and ']', it is considered an | |
1086 * IPv6 address. If [strictIPv6] is false, the address is also considered | |
1087 * an IPv6 address if it contains any ':' character. | |
1088 * | |
1089 * If it is not an IPv6 address, it is case- and escape-normalized. | |
1090 * This escapes all characters not valid in a reg-name, | |
1091 * and converts all non-escape upper-case letters to lower-case. | |
1092 */ | |
1093 static String _makeHost(String host, int start, int end, bool strictIPv6) { | |
1094 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? | |
1095 if (host == null) return null; | |
1096 if (start == end) return ""; | |
1097 // Host is an IPv6 address if it starts with '[' or contains a colon. | |
1098 if (host.codeUnitAt(start) == _LEFT_BRACKET) { | |
1099 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { | |
1100 _fail(host, start, 'Missing end `]` to match `[` in host'); | |
1101 } | |
1102 parseIPv6Address(host, start + 1, end - 1); | |
1103 // RFC 5952 requires hex digits to be lower case. | |
1104 return host.substring(start, end).toLowerCase(); | |
1105 } | |
1106 if (!strictIPv6) { | |
1107 // TODO(lrn): skip if too short to be a valid IPv6 address? | |
1108 for (int i = start; i < end; i++) { | |
1109 if (host.codeUnitAt(i) == _COLON) { | |
1110 parseIPv6Address(host, start, end); | |
1111 return '[$host]'; | |
1112 } | |
1113 } | |
1114 } | |
1115 return _normalizeRegName(host, start, end); | |
1116 } | |
1117 | |
1118 static bool _isRegNameChar(int char) { | |
1119 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | |
1120 } | |
1121 | |
1122 /** | |
1123 * Validates and does case- and percent-encoding normalization. | |
1124 * | |
1125 * The [host] must be an RFC3986 "reg-name". It is converted | |
1126 * to lower case, and percent escapes are converted to either | |
1127 * lower case unreserved characters or upper case escapes. | |
1128 */ | |
1129 static String _normalizeRegName(String host, int start, int end) { | |
1130 StringBuffer buffer; | |
1131 int sectionStart = start; | |
1132 int index = start; | |
1133 // Whether all characters between sectionStart and index are normalized, | |
1134 bool isNormalized = true; | |
1135 | |
1136 while (index < end) { | |
1137 int char = host.codeUnitAt(index); | |
1138 if (char == _PERCENT) { | |
1139 // The _regNameTable contains "%", so we check that first. | |
1140 String replacement = _normalizeEscape(host, index, true); | |
1141 if (replacement == null && isNormalized) { | |
1142 index += 3; | |
1143 continue; | |
1144 } | |
1145 if (buffer == null) buffer = new StringBuffer(); | |
1146 String slice = host.substring(sectionStart, index); | |
1147 if (!isNormalized) slice = slice.toLowerCase(); | |
1148 buffer.write(slice); | |
1149 int sourceLength = 3; | |
1150 if (replacement == null) { | |
1151 replacement = host.substring(index, index + 3); | |
1152 } else if (replacement == "%") { | |
1153 replacement = "%25"; | |
1154 sourceLength = 1; | |
1155 } | |
1156 buffer.write(replacement); | |
1157 index += sourceLength; | |
1158 sectionStart = index; | |
1159 isNormalized = true; | |
1160 } else if (_isRegNameChar(char)) { | |
1161 if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) { | |
1162 // Put initial slice in buffer and continue in non-normalized mode | |
1163 if (buffer == null) buffer = new StringBuffer(); | |
1164 if (sectionStart < index) { | |
1165 buffer.write(host.substring(sectionStart, index)); | |
1166 sectionStart = index; | |
1167 } | |
1168 isNormalized = false; | |
1169 } | |
1170 index++; | |
1171 } else if (_isGeneralDelimiter(char)) { | |
1172 _fail(host, index, "Invalid character"); | |
1173 } else { | |
1174 int sourceLength = 1; | |
1175 if ((char & 0xFC00) == 0xD800 && (index + 1) < end) { | |
1176 int tail = host.codeUnitAt(index + 1); | |
1177 if ((tail & 0xFC00) == 0xDC00) { | |
1178 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | |
1179 sourceLength = 2; | |
1180 } | |
1181 } | |
1182 if (buffer == null) buffer = new StringBuffer(); | |
1183 String slice = host.substring(sectionStart, index); | |
1184 if (!isNormalized) slice = slice.toLowerCase(); | |
1185 buffer.write(slice); | |
1186 buffer.write(_escapeChar(char)); | |
1187 index += sourceLength; | |
1188 sectionStart = index; | |
1189 } | |
1190 } | |
1191 if (buffer == null) return host.substring(start, end); | |
1192 if (sectionStart < end) { | |
1193 String slice = host.substring(sectionStart, end); | |
1194 if (!isNormalized) slice = slice.toLowerCase(); | |
1195 buffer.write(slice); | |
1196 } | |
1197 return buffer.toString(); | |
1198 } | |
1199 | |
1200 /** | |
1201 * Validates scheme characters and does case-normalization. | |
1202 * | |
1203 * Schemes are converted to lower case. They cannot contain escapes. | |
1204 */ | |
1205 static String _makeScheme(String scheme, int start, int end) { | |
1206 if (start == end) return ""; | |
1207 final int firstCodeUnit = scheme.codeUnitAt(start); | |
1208 if (!_isAlphabeticCharacter(firstCodeUnit)) { | |
1209 _fail(scheme, start, "Scheme not starting with alphabetic character"); | |
1210 } | |
1211 bool containsUpperCase = false; | |
1212 for (int i = start; i < end; i++) { | |
1213 final int codeUnit = scheme.codeUnitAt(i); | |
1214 if (!_isSchemeCharacter(codeUnit)) { | |
1215 _fail(scheme, i, "Illegal scheme character"); | |
1216 } | |
1217 if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { | |
1218 containsUpperCase = true; | |
1219 } | |
1220 } | |
1221 scheme = scheme.substring(start, end); | |
1222 if (containsUpperCase) scheme = scheme.toLowerCase(); | |
1223 return scheme; | |
1224 } | |
1225 | |
1226 static String _makeUserInfo(String userInfo, int start, int end) { | |
1227 if (userInfo == null) return ""; | |
1228 return _normalize(userInfo, start, end, _userinfoTable); | |
1229 } | |
1230 | |
1231 static String _makePath(String path, int start, int end, | |
1232 Iterable<String> pathSegments, | |
1233 String scheme, | |
1234 bool hasAuthority) { | |
1235 bool isFile = (scheme == "file"); | |
1236 bool ensureLeadingSlash = isFile || hasAuthority; | |
1237 if (path == null && pathSegments == null) return isFile ? "/" : ""; | |
1238 if (path != null && pathSegments != null) { | |
1239 throw new ArgumentError('Both path and pathSegments specified'); | |
1240 } | |
1241 var result; | |
1242 if (path != null) { | |
1243 result = _normalize(path, start, end, _pathCharOrSlashTable); | |
1244 } else { | |
1245 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); | |
1246 } | |
1247 if (result.isEmpty) { | |
1248 if (isFile) return "/"; | |
1249 } else if (ensureLeadingSlash && !result.startsWith('/')) { | |
1250 result = "/" + result; | |
1251 } | |
1252 result = _normalizePath(result, scheme, hasAuthority); | |
1253 return result; | |
1254 } | |
1255 | |
1256 /// Performs path normalization (remove dot segments) on a path. | |
1257 /// | |
1258 /// If the URI has neither scheme nor authority, it's considered a | |
1259 /// "pure path" and normalization won't remove leading ".." segments. | |
1260 /// Otherwise it follows the RFC 3986 "remove dot segments" algorithm. | |
1261 static String _normalizePath(String path, String scheme, bool hasAuthority) { | |
1262 if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) { | |
1263 return _normalizeRelativePath(path); | |
1264 } | |
1265 return _removeDotSegments(path); | |
1266 } | |
1267 | |
1268 static String _makeQuery(String query, int start, int end, | |
1269 Map<String, String> queryParameters) { | |
1270 if (query == null && queryParameters == null) return null; | |
1271 if (query != null && queryParameters != null) { | |
1272 throw new ArgumentError('Both query and queryParameters specified'); | |
1273 } | |
1274 if (query != null) return _normalize(query, start, end, _queryCharTable); | |
1275 | |
1276 var result = new StringBuffer(); | |
1277 var first = true; | |
1278 queryParameters.forEach((key, value) { | |
1279 if (!first) { | |
1280 result.write("&"); | |
1281 } | |
1282 first = false; | |
1283 result.write(Uri.encodeQueryComponent(key)); | |
1284 if (value != null && !value.isEmpty) { | |
1285 result.write("="); | |
1286 result.write(Uri.encodeQueryComponent(value)); | |
1287 } | |
1288 }); | |
1289 return result.toString(); | |
1290 } | |
1291 | |
1292 static String _makeFragment(String fragment, int start, int end) { | |
1293 if (fragment == null) return null; | |
1294 return _normalize(fragment, start, end, _queryCharTable); | |
1295 } | |
1296 | |
1297 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; | |
1298 | |
1299 static bool _isHexDigit(int char) { | |
1300 if (_NINE >= char) return _ZERO <= char; | |
1301 char |= 0x20; | |
1302 return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char; | |
1303 } | |
1304 | |
1305 static int _hexValue(int char) { | |
1306 assert(_isHexDigit(char)); | |
1307 if (_NINE >= char) return char - _ZERO; | |
1308 char |= 0x20; | |
1309 return char - (_LOWER_CASE_A - 10); | |
1310 } | |
1311 | |
1312 /** | |
1313 * Performs RFC 3986 Percent-Encoding Normalization. | |
1314 * | |
1315 * Returns a replacement string that should be replace the original escape. | |
1316 * Returns null if no replacement is necessary because the escape is | |
1317 * not for an unreserved character and is already non-lower-case. | |
1318 * | |
1319 * Returns "%" if the escape is invalid (not two valid hex digits following | |
1320 * the percent sign). The calling code should replace the percent | |
1321 * sign with "%25", but leave the following two characters unmodified. | |
1322 * | |
1323 * If [lowerCase] is true, a single character returned is always lower case, | |
1324 */ | |
1325 static String _normalizeEscape(String source, int index, bool lowerCase) { | |
1326 assert(source.codeUnitAt(index) == _PERCENT); | |
1327 if (index + 2 >= source.length) { | |
1328 return "%"; // Marks the escape as invalid. | |
1329 } | |
1330 int firstDigit = source.codeUnitAt(index + 1); | |
1331 int secondDigit = source.codeUnitAt(index + 2); | |
1332 if (!_isHexDigit(firstDigit) || !_isHexDigit(secondDigit)) { | |
1333 return "%"; // Marks the escape as invalid. | |
1334 } | |
1335 int value = _hexValue(firstDigit) * 16 + _hexValue(secondDigit); | |
1336 if (_isUnreservedChar(value)) { | |
1337 if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { | |
1338 value |= 0x20; | |
1339 } | |
1340 return new String.fromCharCode(value); | |
1341 } | |
1342 if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { | |
1343 // Either digit is lower case. | |
1344 return source.substring(index, index + 3).toUpperCase(); | |
1345 } | |
1346 // Escape is retained, and is already non-lower case, so return null to | |
1347 // represent "no replacement necessary". | |
1348 return null; | |
1349 } | |
1350 | |
1351 static bool _isUnreservedChar(int ch) { | |
1352 return ch < 127 && | |
1353 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | |
1354 } | |
1355 | |
1356 static String _escapeChar(char) { | |
1357 assert(char <= 0x10ffff); // It's a valid unicode code point. | |
1358 const hexDigits = "0123456789ABCDEF"; | |
1359 List codeUnits; | |
1360 if (char < 0x80) { | |
1361 // ASCII, a single percent encoded sequence. | |
1362 codeUnits = new List(3); | |
1363 codeUnits[0] = _PERCENT; | |
1364 codeUnits[1] = hexDigits.codeUnitAt(char >> 4); | |
1365 codeUnits[2] = hexDigits.codeUnitAt(char & 0xf); | |
1366 } else { | |
1367 // Do UTF-8 encoding of character, then percent encode bytes. | |
1368 int flag = 0xc0; // The high-bit markers on the first byte of UTF-8. | |
1369 int encodedBytes = 2; | |
1370 if (char > 0x7ff) { | |
1371 flag = 0xe0; | |
1372 encodedBytes = 3; | |
1373 if (char > 0xffff) { | |
1374 encodedBytes = 4; | |
1375 flag = 0xf0; | |
1376 } | |
1377 } | |
1378 codeUnits = new List(3 * encodedBytes); | |
1379 int index = 0; | |
1380 while (--encodedBytes >= 0) { | |
1381 int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; | |
1382 codeUnits[index] = _PERCENT; | |
1383 codeUnits[index + 1] = hexDigits.codeUnitAt(byte >> 4); | |
1384 codeUnits[index + 2] = hexDigits.codeUnitAt(byte & 0xf); | |
1385 index += 3; | |
1386 flag = 0x80; // Following bytes have only high bit set. | |
1387 } | |
1388 } | |
1389 return new String.fromCharCodes(codeUnits); | |
1390 } | |
1391 | |
1392 /** | |
1393 * Runs through component checking that each character is valid and | |
1394 * normalize percent escapes. | |
1395 * | |
1396 * Uses [charTable] to check if a non-`%` character is allowed. | |
1397 * Each `%` character must be followed by two hex digits. | |
1398 * If the hex-digits are lower case letters, they are converted to | |
1399 * upper case. | |
1400 */ | |
1401 static String _normalize(String component, int start, int end, | |
1402 List<int> charTable) { | |
1403 StringBuffer buffer; | |
1404 int sectionStart = start; | |
1405 int index = start; | |
1406 // Loop while characters are valid and escapes correct and upper-case. | |
1407 while (index < end) { | |
1408 int char = component.codeUnitAt(index); | |
1409 if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) { | |
1410 index++; | |
1411 } else { | |
1412 String replacement; | |
1413 int sourceLength; | |
1414 if (char == _PERCENT) { | |
1415 replacement = _normalizeEscape(component, index, false); | |
1416 // Returns null if we should keep the existing escape. | |
1417 if (replacement == null) { | |
1418 index += 3; | |
1419 continue; | |
1420 } | |
1421 // Returns "%" if we should escape the existing percent. | |
1422 if ("%" == replacement) { | |
1423 replacement = "%25"; | |
1424 sourceLength = 1; | |
1425 } else { | |
1426 sourceLength = 3; | |
1427 } | |
1428 } else if (_isGeneralDelimiter(char)) { | |
1429 _fail(component, index, "Invalid character"); | |
1430 } else { | |
1431 sourceLength = 1; | |
1432 if ((char & 0xFC00) == 0xD800) { | |
1433 // Possible lead surrogate. | |
1434 if (index + 1 < end) { | |
1435 int tail = component.codeUnitAt(index + 1); | |
1436 if ((tail & 0xFC00) == 0xDC00) { | |
1437 // Tail surrogat. | |
1438 sourceLength = 2; | |
1439 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); | |
1440 } | |
1441 } | |
1442 } | |
1443 replacement = _escapeChar(char); | |
1444 } | |
1445 if (buffer == null) buffer = new StringBuffer(); | |
1446 buffer.write(component.substring(sectionStart, index)); | |
1447 buffer.write(replacement); | |
1448 index += sourceLength; | |
1449 sectionStart = index; | |
1450 } | |
1451 } | |
1452 if (buffer == null) { | |
1453 // Makes no copy if start == 0 and end == component.length. | |
1454 return component.substring(start, end); | |
1455 } | |
1456 if (sectionStart < end) { | |
1457 buffer.write(component.substring(sectionStart, end)); | |
1458 } | |
1459 return buffer.toString(); | |
1460 } | |
1461 | |
1462 static bool _isSchemeCharacter(int ch) { | |
1463 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | |
1464 } | |
1465 | |
1466 static bool _isGeneralDelimiter(int ch) { | |
1467 return ch <= _RIGHT_BRACKET && | |
1468 ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | |
1469 } | |
1470 | |
1471 /** | |
1472 * Returns whether the URI is absolute. | |
1473 */ | |
1474 bool get isAbsolute => scheme != "" && fragment == ""; | |
1475 | |
1476 String _mergePaths(String base, String reference) { | |
1477 // Optimize for the case: absolute base, reference beginning with "../". | |
1478 int backCount = 0; | |
1479 int refStart = 0; | |
1480 // Count number of "../" at beginning of reference. | |
1481 while (reference.startsWith("../", refStart)) { | |
1482 refStart += 3; | |
1483 backCount++; | |
1484 } | |
1485 | |
1486 // Drop last segment - everything after last '/' of base. | |
1487 int baseEnd = base.lastIndexOf('/'); | |
1488 // Drop extra segments for each leading "../" of reference. | |
1489 while (baseEnd > 0 && backCount > 0) { | |
1490 int newEnd = base.lastIndexOf('/', baseEnd - 1); | |
1491 if (newEnd < 0) { | |
1492 break; | |
1493 } | |
1494 int delta = baseEnd - newEnd; | |
1495 // If we see a "." or ".." segment in base, stop here and let | |
1496 // _removeDotSegments handle it. | |
1497 if ((delta == 2 || delta == 3) && | |
1498 base.codeUnitAt(newEnd + 1) == _DOT && | |
1499 (delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) { | |
1500 break; | |
1501 } | |
1502 baseEnd = newEnd; | |
1503 backCount--; | |
1504 } | |
1505 return base.replaceRange(baseEnd + 1, null, | |
1506 reference.substring(refStart - 3 * backCount)); | |
1507 } | |
1508 | |
1509 /// Make a guess at whether a path contains a `..` or `.` segment. | |
1510 /// | |
1511 /// This is a primitive test that can cause false positives. | |
1512 /// It's only used to avoid a more expensive operation in the case where | |
1513 /// it's not necessary. | |
1514 static bool _mayContainDotSegments(String path) { | |
1515 if (path.startsWith('.')) return true; | |
1516 int index = path.indexOf("/."); | |
1517 return index != -1; | |
1518 } | |
1519 | |
1520 /// Removes '.' and '..' segments from a path. | |
1521 /// | |
1522 /// Follows the RFC 2986 "remove dot segments" algorithm. | |
1523 /// This algorithm is only used on paths of URIs with a scheme, | |
1524 /// and it treats the path as if it is absolute (leading '..' are removed). | |
1525 static String _removeDotSegments(String path) { | |
1526 if (!_mayContainDotSegments(path)) return path; | |
1527 assert(path.isNotEmpty); // An empty path would not have dot segments. | |
1528 List<String> output = []; | |
1529 bool appendSlash = false; | |
1530 for (String segment in path.split("/")) { | |
1531 appendSlash = false; | |
1532 if (segment == "..") { | |
1533 if (output.isNotEmpty) { | |
1534 output.removeLast(); | |
1535 if (output.isEmpty) { | |
1536 output.add(""); | |
1537 } | |
1538 } | |
1539 appendSlash = true; | |
1540 } else if ("." == segment) { | |
1541 appendSlash = true; | |
1542 } else { | |
1543 output.add(segment); | |
1544 } | |
1545 } | |
1546 if (appendSlash) output.add(""); | |
1547 return output.join("/"); | |
1548 } | |
1549 | |
1550 /// Removes all `.` segments and any non-leading `..` segments. | |
1551 /// | |
1552 /// Removing the ".." from a "bar/foo/.." sequence results in "bar/" | |
1553 /// (trailing "/"). If the entire path is removed (because it contains as | |
1554 /// many ".." segments as real segments), the result is "./". | |
1555 /// This is different from an empty string, which represents "no path", | |
1556 /// when you resolve it against a base URI with a path with a non-empty | |
1557 /// final segment. | |
1558 static String _normalizeRelativePath(String path) { | |
1559 assert(!path.startsWith('/')); // Only get called for relative paths. | |
1560 if (!_mayContainDotSegments(path)) return path; | |
1561 assert(path.isNotEmpty); // An empty path would not have dot segments. | |
1562 List<String> output = []; | |
1563 bool appendSlash = false; | |
1564 for (String segment in path.split("/")) { | |
1565 appendSlash = false; | |
1566 if (".." == segment) { | |
1567 if (!output.isEmpty && output.last != "..") { | |
1568 output.removeLast(); | |
1569 appendSlash = true; | |
1570 } else { | |
1571 output.add(".."); | |
1572 } | |
1573 } else if ("." == segment) { | |
1574 appendSlash = true; | |
1575 } else { | |
1576 output.add(segment); | |
1577 } | |
1578 } | |
1579 if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { | |
1580 return "./"; | |
1581 } | |
1582 if (appendSlash || output.last == '..') output.add(""); | |
1583 return output.join("/"); | |
1584 } | |
1585 | |
1586 /** | |
1587 * Resolve [reference] as an URI relative to `this`. | |
1588 * | |
1589 * First turn [reference] into a URI using [Uri.parse]. Then resolve the | |
1590 * resulting URI relative to `this`. | |
1591 * | |
1592 * Returns the resolved URI. | |
1593 * | |
1594 * See [resolveUri] for details. | |
1595 */ | |
1596 Uri resolve(String reference) { | |
1597 return resolveUri(Uri.parse(reference)); | |
1598 } | |
1599 | |
1600 /** | |
1601 * Resolve [reference] as an URI relative to `this`. | |
1602 * | |
1603 * Returns the resolved URI. | |
1604 * | |
1605 * The algorithm "Transform Reference" for resolving a reference is | |
1606 * described in [RFC-3986 Section 5] | |
1607 * (http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). | |
1608 * | |
1609 * Updated to handle the case where the base URI is just a relative path - | |
1610 * that is: when it has no scheme or authority and the path does not start | |
1611 * with a slash. | |
1612 * In that case, the paths are combined without removing leading "..", and | |
1613 * an empty path is not converted to "/". | |
1614 */ | |
1615 Uri resolveUri(Uri reference) { | |
1616 // From RFC 3986. | |
1617 String targetScheme; | |
1618 String targetUserInfo = ""; | |
1619 String targetHost; | |
1620 int targetPort; | |
1621 String targetPath; | |
1622 String targetQuery; | |
1623 if (reference.scheme.isNotEmpty) { | |
1624 targetScheme = reference.scheme; | |
1625 if (reference.hasAuthority) { | |
1626 targetUserInfo = reference.userInfo; | |
1627 targetHost = reference.host; | |
1628 targetPort = reference.hasPort ? reference.port : null; | |
1629 } | |
1630 targetPath = _removeDotSegments(reference.path); | |
1631 if (reference.hasQuery) { | |
1632 targetQuery = reference.query; | |
1633 } | |
1634 } else { | |
1635 targetScheme = this.scheme; | |
1636 if (reference.hasAuthority) { | |
1637 targetUserInfo = reference.userInfo; | |
1638 targetHost = reference.host; | |
1639 targetPort = _makePort(reference.hasPort ? reference.port : null, | |
1640 targetScheme); | |
1641 targetPath = _removeDotSegments(reference.path); | |
1642 if (reference.hasQuery) targetQuery = reference.query; | |
1643 } else { | |
1644 targetUserInfo = this._userInfo; | |
1645 targetHost = this._host; | |
1646 targetPort = this._port; | |
1647 if (reference.path == "") { | |
1648 targetPath = this._path; | |
1649 if (reference.hasQuery) { | |
1650 targetQuery = reference.query; | |
1651 } else { | |
1652 targetQuery = this._query; | |
1653 } | |
1654 } else { | |
1655 if (reference.hasAbsolutePath) { | |
1656 targetPath = _removeDotSegments(reference.path); | |
1657 } else { | |
1658 // This is the RFC 3986 behavior for merging. | |
1659 if (this.hasEmptyPath) { | |
1660 if (!this.hasScheme && !this.hasAuthority) { | |
1661 // Keep the path relative if no scheme or authority. | |
1662 targetPath = reference.path; | |
1663 } else { | |
1664 // Add path normalization on top of RFC algorithm. | |
1665 targetPath = _removeDotSegments("/" + reference.path); | |
1666 } | |
1667 } else { | |
1668 var mergedPath = _mergePaths(this._path, reference.path); | |
1669 if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { | |
1670 targetPath = _removeDotSegments(mergedPath); | |
1671 } else { | |
1672 // Non-RFC 3986 beavior. If both base and reference are relative | |
1673 // path, allow the merged path to start with "..". | |
1674 // The RFC only specifies the case where the base has a scheme. | |
1675 targetPath = _normalizeRelativePath(mergedPath); | |
1676 } | |
1677 } | |
1678 } | |
1679 if (reference.hasQuery) targetQuery = reference.query; | |
1680 } | |
1681 } | |
1682 } | |
1683 String fragment = reference.hasFragment ? reference.fragment : null; | |
1684 return new Uri._internal(targetScheme, | |
1685 targetUserInfo, | |
1686 targetHost, | |
1687 targetPort, | |
1688 targetPath, | |
1689 targetQuery, | |
1690 fragment); | |
1691 } | |
1692 | |
1693 /** | |
1694 * Returns whether the URI has a [scheme] component. | |
1695 */ | |
1696 bool get hasScheme => scheme.isNotEmpty; | |
1697 | |
1698 /** | |
1699 * Returns whether the URI has an [authority] component. | |
1700 */ | |
1701 bool get hasAuthority => _host != null; | |
1702 | |
1703 /** | |
1704 * Returns whether the URI has an explicit port. | |
1705 * | |
1706 * If the port number is the default port number | |
1707 * (zero for unrecognized schemes, with http (80) and https (443) being | |
1708 * recognized), | |
1709 * then the port is made implicit and omitted from the URI. | |
1710 */ | |
1711 bool get hasPort => _port != null; | |
1712 | |
1713 /** | |
1714 * Returns whether the URI has a query part. | |
1715 */ | |
1716 bool get hasQuery => _query != null; | |
1717 | |
1718 /** | |
1719 * Returns whether the URI has a fragment part. | |
1720 */ | |
1721 bool get hasFragment => _fragment != null; | |
1722 | |
1723 /** | |
1724 * Returns whether the URI has an empty path. | |
1725 */ | |
1726 bool get hasEmptyPath => _path.isEmpty; | |
1727 | |
1728 /** | |
1729 * Returns whether the URI has an absolute path (starting with '/'). | |
1730 */ | |
1731 bool get hasAbsolutePath => _path.startsWith('/'); | |
1732 | |
1733 /** | |
1734 * Returns the origin of the URI in the form scheme://host:port for the | |
1735 * schemes http and https. | |
1736 * | |
1737 * It is an error if the scheme is not "http" or "https". | |
1738 * | |
1739 * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin | |
1740 */ | |
1741 String get origin { | |
1742 if (scheme == "" || _host == null || _host == "") { | |
1743 throw new StateError("Cannot use origin without a scheme: $this"); | |
1744 } | |
1745 if (scheme != "http" && scheme != "https") { | |
1746 throw new StateError( | |
1747 "Origin is only applicable schemes http and https: $this"); | |
1748 } | |
1749 if (_port == null) return "$scheme://$_host"; | |
1750 return "$scheme://$_host:$_port"; | |
1751 } | |
1752 | |
1753 /** | |
1754 * Returns the file path from a file URI. | |
1755 * | |
1756 * The returned path has either Windows or non-Windows | |
1757 * semantics. | |
1758 * | |
1759 * For non-Windows semantics the slash ("/") is used to separate | |
1760 * path segments. | |
1761 * | |
1762 * For Windows semantics the backslash ("\") separator is used to | |
1763 * separate path segments. | |
1764 * | |
1765 * If the URI is absolute the path starts with a path separator | |
1766 * unless Windows semantics is used and the first path segment is a | |
1767 * drive letter. When Windows semantics is used a host component in | |
1768 * the uri in interpreted as a file server and a UNC path is | |
1769 * returned. | |
1770 * | |
1771 * The default for whether to use Windows or non-Windows semantics | |
1772 * determined from the platform Dart is running on. When running in | |
1773 * the standalone VM this is detected by the VM based on the | |
1774 * operating system. When running in a browser non-Windows semantics | |
1775 * is always used. | |
1776 * | |
1777 * To override the automatic detection of which semantics to use pass | |
1778 * a value for [windows]. Passing `true` will use Windows | |
1779 * semantics and passing `false` will use non-Windows semantics. | |
1780 * | |
1781 * If the URI ends with a slash (i.e. the last path component is | |
1782 * empty) the returned file path will also end with a slash. | |
1783 * | |
1784 * With Windows semantics URIs starting with a drive letter cannot | |
1785 * be relative to the current drive on the designated drive. That is | |
1786 * for the URI `file:///c:abc` calling `toFilePath` will throw as a | |
1787 * path segment cannot contain colon on Windows. | |
1788 * | |
1789 * Examples using non-Windows semantics (resulting of calling | |
1790 * toFilePath in comment): | |
1791 * | |
1792 * Uri.parse("xxx/yyy"); // xxx/yyy | |
1793 * Uri.parse("xxx/yyy/"); // xxx/yyy/ | |
1794 * Uri.parse("file:///xxx/yyy"); // /xxx/yyy | |
1795 * Uri.parse("file:///xxx/yyy/"); // /xxx/yyy/ | |
1796 * Uri.parse("file:///C:"); // /C: | |
1797 * Uri.parse("file:///C:a"); // /C:a | |
1798 * | |
1799 * Examples using Windows semantics (resulting URI in comment): | |
1800 * | |
1801 * Uri.parse("xxx/yyy"); // xxx\yyy | |
1802 * Uri.parse("xxx/yyy/"); // xxx\yyy\ | |
1803 * Uri.parse("file:///xxx/yyy"); // \xxx\yyy | |
1804 * Uri.parse("file:///xxx/yyy/"); // \xxx\yyy/ | |
1805 * Uri.parse("file:///C:/xxx/yyy"); // C:\xxx\yyy | |
1806 * Uri.parse("file:C:xxx/yyy"); // Throws as a path segment | |
1807 * // cannot contain colon on Windows. | |
1808 * Uri.parse("file://server/share/file"); // \\server\share\file | |
1809 * | |
1810 * If the URI is not a file URI calling this throws | |
1811 * [UnsupportedError]. | |
1812 * | |
1813 * If the URI cannot be converted to a file path calling this throws | |
1814 * [UnsupportedError]. | |
1815 */ | |
1816 String toFilePath({bool windows}) { | |
1817 if (scheme != "" && scheme != "file") { | |
1818 throw new UnsupportedError( | |
1819 "Cannot extract a file path from a $scheme URI"); | |
1820 } | |
1821 if (query != "") { | |
1822 throw new UnsupportedError( | |
1823 "Cannot extract a file path from a URI with a query component"); | |
1824 } | |
1825 if (fragment != "") { | |
1826 throw new UnsupportedError( | |
1827 "Cannot extract a file path from a URI with a fragment component"); | |
1828 } | |
1829 if (windows == null) windows = _isWindows; | |
1830 return windows ? _toWindowsFilePath() : _toFilePath(); | |
1831 } | |
1832 | |
1833 String _toFilePath() { | |
1834 if (host != "") { | |
1835 throw new UnsupportedError( | |
1836 "Cannot extract a non-Windows file path from a file URI " | |
1837 "with an authority"); | |
1838 } | |
1839 _checkNonWindowsPathReservedCharacters(pathSegments, false); | |
1840 var result = new StringBuffer(); | |
1841 if (_isPathAbsolute) result.write("/"); | |
1842 result.writeAll(pathSegments, "/"); | |
1843 return result.toString(); | |
1844 } | |
1845 | |
1846 String _toWindowsFilePath() { | |
1847 bool hasDriveLetter = false; | |
1848 var segments = pathSegments; | |
1849 if (segments.length > 0 && | |
1850 segments[0].length == 2 && | |
1851 segments[0].codeUnitAt(1) == _COLON) { | |
1852 _checkWindowsDriveLetter(segments[0].codeUnitAt(0), false); | |
1853 _checkWindowsPathReservedCharacters(segments, false, 1); | |
1854 hasDriveLetter = true; | |
1855 } else { | |
1856 _checkWindowsPathReservedCharacters(segments, false); | |
1857 } | |
1858 var result = new StringBuffer(); | |
1859 if (_isPathAbsolute && !hasDriveLetter) result.write("\\"); | |
1860 if (host != "") { | |
1861 result.write("\\"); | |
1862 result.write(host); | |
1863 result.write("\\"); | |
1864 } | |
1865 result.writeAll(segments, "\\"); | |
1866 if (hasDriveLetter && segments.length == 1) result.write("\\"); | |
1867 return result.toString(); | |
1868 } | |
1869 | |
1870 bool get _isPathAbsolute { | |
1871 if (path == null || path.isEmpty) return false; | |
1872 return path.startsWith('/'); | |
1873 } | |
1874 | |
1875 void _writeAuthority(StringSink ss) { | |
1876 if (_userInfo.isNotEmpty) { | |
1877 ss.write(_userInfo); | |
1878 ss.write("@"); | |
1879 } | |
1880 if (_host != null) ss.write(_host); | |
1881 if (_port != null) { | |
1882 ss.write(":"); | |
1883 ss.write(_port); | |
1884 } | |
1885 } | |
1886 | |
1887 String toString() { | |
1888 StringBuffer sb = new StringBuffer(); | |
1889 _addIfNonEmpty(sb, scheme, scheme, ':'); | |
1890 if (hasAuthority || path.startsWith("//") || (scheme == "file")) { | |
1891 // File URIS always have the authority, even if it is empty. | |
1892 // The empty URI means "localhost". | |
1893 sb.write("//"); | |
1894 _writeAuthority(sb); | |
1895 } | |
1896 sb.write(path); | |
1897 if (_query != null) { sb..write("?")..write(_query); } | |
1898 if (_fragment != null) { sb..write("#")..write(_fragment); } | |
1899 return sb.toString(); | |
1900 } | |
1901 | |
1902 bool operator==(other) { | |
1903 if (other is! Uri) return false; | |
1904 Uri uri = other; | |
1905 return scheme == uri.scheme && | |
1906 hasAuthority == uri.hasAuthority && | |
1907 userInfo == uri.userInfo && | |
1908 host == uri.host && | |
1909 port == uri.port && | |
1910 path == uri.path && | |
1911 hasQuery == uri.hasQuery && | |
1912 query == uri.query && | |
1913 hasFragment == uri.hasFragment && | |
1914 fragment == uri.fragment; | |
1915 } | |
1916 | |
1917 int get hashCode { | |
1918 int combine(part, current) { | |
1919 // The sum is truncated to 30 bits to make sure it fits into a Smi. | |
1920 return (current * 31 + part.hashCode) & 0x3FFFFFFF; | |
1921 } | |
1922 return combine(scheme, combine(userInfo, combine(host, combine(port, | |
1923 combine(path, combine(query, combine(fragment, 1))))))); | |
1924 } | |
1925 | |
1926 static void _addIfNonEmpty(StringBuffer sb, String test, | |
1927 String first, String second) { | |
1928 if ("" != test) { | |
1929 sb.write(first); | |
1930 sb.write(second); | |
1931 } | |
1932 } | |
1933 | |
1934 /** | |
1935 * Encode the string [component] using percent-encoding to make it | |
1936 * safe for literal use as a URI component. | |
1937 * | |
1938 * All characters except uppercase and lowercase letters, digits and | |
1939 * the characters `-_.!~*'()` are percent-encoded. This is the | |
1940 * set of characters specified in RFC 2396 and the which is | |
1941 * specified for the encodeUriComponent in ECMA-262 version 5.1. | |
1942 * | |
1943 * When manually encoding path segments or query components remember | |
1944 * to encode each part separately before building the path or query | |
1945 * string. | |
1946 * | |
1947 * For encoding the query part consider using | |
1948 * [encodeQueryComponent]. | |
1949 * | |
1950 * To avoid the need for explicitly encoding use the [pathSegments] | |
1951 * and [queryParameters] optional named arguments when constructing | |
1952 * a [Uri]. | |
1953 */ | |
1954 static String encodeComponent(String component) { | |
1955 return _uriEncode(_unreserved2396Table, component); | |
1956 } | |
1957 | |
1958 /** | |
1959 * Encode the string [component] according to the HTML 4.01 rules | |
1960 * for encoding the posting of a HTML form as a query string | |
1961 * component. | |
1962 * | |
1963 * Encode the string [component] according to the HTML 4.01 rules | |
1964 * for encoding the posting of a HTML form as a query string | |
1965 * component. | |
1966 | |
1967 * The component is first encoded to bytes using [encoding]. | |
1968 * The default is to use [UTF8] encoding, which preserves all | |
1969 * the characters that don't need encoding. | |
1970 | |
1971 * Then the resulting bytes are "percent-encoded". This transforms | |
1972 * spaces (U+0020) to a plus sign ('+') and all bytes that are not | |
1973 * the ASCII decimal digits, letters or one of '-._~' are written as | |
1974 * a percent sign '%' followed by the two-digit hexadecimal | |
1975 * representation of the byte. | |
1976 | |
1977 * Note that the set of characters which are percent-encoded is a | |
1978 * superset of what HTML 4.01 requires, since it refers to RFC 1738 | |
1979 * for reserved characters. | |
1980 * | |
1981 * When manually encoding query components remember to encode each | |
1982 * part separately before building the query string. | |
1983 * | |
1984 * To avoid the need for explicitly encoding the query use the | |
1985 * [queryParameters] optional named arguments when constructing a | |
1986 * [Uri]. | |
1987 * | |
1988 * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more | |
1989 * details. | |
1990 */ | |
1991 static String encodeQueryComponent(String component, | |
1992 {Encoding encoding: UTF8}) { | |
1993 return _uriEncode( | |
1994 _unreservedTable, component, encoding: encoding, spaceToPlus: true); | |
1995 } | |
1996 | |
1997 /** | |
1998 * Decodes the percent-encoding in [encodedComponent]. | |
1999 * | |
2000 * Note that decoding a URI component might change its meaning as | |
2001 * some of the decoded characters could be characters with are | |
2002 * delimiters for a given URI componene type. Always split a URI | |
2003 * component using the delimiters for the component before decoding | |
2004 * the individual parts. | |
2005 * | |
2006 * For handling the [path] and [query] components consider using | |
2007 * [pathSegments] and [queryParameters] to get the separated and | |
2008 * decoded component. | |
2009 */ | |
2010 static String decodeComponent(String encodedComponent) { | |
2011 return _uriDecode(encodedComponent); | |
2012 } | |
2013 | |
2014 /** | |
2015 * Decodes the percent-encoding in [encodedComponent], converting | |
2016 * pluses to spaces. | |
2017 * | |
2018 * It will create a byte-list of the decoded characters, and then use | |
2019 * [encoding] to decode the byte-list to a String. The default encoding is | |
2020 * UTF-8. | |
2021 */ | |
2022 static String decodeQueryComponent( | |
2023 String encodedComponent, | |
2024 {Encoding encoding: UTF8}) { | |
2025 return _uriDecode(encodedComponent, plusToSpace: true, encoding: encoding); | |
2026 } | |
2027 | |
2028 /** | |
2029 * Encode the string [uri] using percent-encoding to make it | |
2030 * safe for literal use as a full URI. | |
2031 * | |
2032 * All characters except uppercase and lowercase letters, digits and | |
2033 * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This | |
2034 * is the set of characters specified in in ECMA-262 version 5.1 for | |
2035 * the encodeURI function . | |
2036 */ | |
2037 static String encodeFull(String uri) { | |
2038 return _uriEncode(_encodeFullTable, uri); | |
2039 } | |
2040 | |
2041 /** | |
2042 * Decodes the percent-encoding in [uri]. | |
2043 * | |
2044 * Note that decoding a full URI might change its meaning as some of | |
2045 * the decoded characters could be reserved characters. In most | |
2046 * cases an encoded URI should be parsed into components using | |
2047 * [Uri.parse] before decoding the separate components. | |
2048 */ | |
2049 static String decodeFull(String uri) { | |
2050 return _uriDecode(uri); | |
2051 } | |
2052 | |
2053 /** | |
2054 * Returns the [query] split into a map according to the rules | |
2055 * specified for FORM post in the | |
2056 * [HTML 4.01 specification section 17.13.4] | |
2057 * (http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 | |
2058 * "HTML 4.01 section 17.13.4"). Each key and value in the returned | |
2059 * map has been decoded. If the [query] | |
2060 * is the empty string an empty map is returned. | |
2061 * | |
2062 * Keys in the query string that have no value are mapped to the | |
2063 * empty string. | |
2064 * | |
2065 * Each query component will be decoded using [encoding]. The default encoding | |
2066 * is UTF-8. | |
2067 */ | |
2068 static Map<String, String> splitQueryString(String query, | |
2069 {Encoding encoding: UTF8}) { | |
2070 return query.split("&").fold({}, (map, element) { | |
2071 int index = element.indexOf("="); | |
2072 if (index == -1) { | |
2073 if (element != "") { | |
2074 map[decodeQueryComponent(element, encoding: encoding)] = ""; | |
2075 } | |
2076 } else if (index != 0) { | |
2077 var key = element.substring(0, index); | |
2078 var value = element.substring(index + 1); | |
2079 map[Uri.decodeQueryComponent(key, encoding: encoding)] = | |
2080 decodeQueryComponent(value, encoding: encoding); | |
2081 } | |
2082 return map; | |
2083 }); | |
2084 } | |
2085 | |
2086 /** | |
2087 * Parse the [host] as an IP version 4 (IPv4) address, returning the address | |
2088 * as a list of 4 bytes in network byte order (big endian). | |
2089 * | |
2090 * Throws a [FormatException] if [host] is not a valid IPv4 address | |
2091 * representation. | |
2092 */ | |
2093 static List<int> parseIPv4Address(String host) { | |
2094 void error(String msg) { | |
2095 throw new FormatException('Illegal IPv4 address, $msg'); | |
2096 } | |
2097 var bytes = host.split('.'); | |
2098 if (bytes.length != 4) { | |
2099 error('IPv4 address should contain exactly 4 parts'); | |
2100 } | |
2101 // TODO(ajohnsen): Consider using Uint8List. | |
2102 return bytes | |
2103 .map((byteString) { | |
2104 int byte = int.parse(byteString); | |
2105 if (byte < 0 || byte > 255) { | |
2106 error('each part must be in the range of `0..255`'); | |
2107 } | |
2108 return byte; | |
2109 }) | |
2110 .toList(); | |
2111 } | |
2112 | |
2113 /** | |
2114 * Parse the [host] as an IP version 6 (IPv6) address, returning the address | |
2115 * as a list of 16 bytes in network byte order (big endian). | |
2116 * | |
2117 * Throws a [FormatException] if [host] is not a valid IPv6 address | |
2118 * representation. | |
2119 * | |
2120 * Acts on the substring from [start] to [end]. If [end] is omitted, it | |
2121 * defaults ot the end of the string. | |
2122 * | |
2123 * Some examples of IPv6 addresses: | |
2124 * * ::1 | |
2125 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | |
2126 * * 3ffe:2a00:100:7031::1 | |
2127 * * ::FFFF:129.144.52.38 | |
2128 * * 2010:836B:4179::836B:4179 | |
2129 */ | |
2130 static List<int> parseIPv6Address(String host, [int start = 0, int end]) { | |
2131 if (end == null) end = host.length; | |
2132 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated | |
2133 // by `:`'s, with the following exceptions: | |
2134 // | |
2135 // - One (and only one) wildcard (`::`) may be present, representing a fill | |
2136 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. | |
2137 // - The last two parts may be replaced by an IPv4 address. | |
2138 void error(String msg, [position]) { | |
2139 throw new FormatException('Illegal IPv6 address, $msg', host, position); | |
2140 } | |
2141 int parseHex(int start, int end) { | |
2142 if (end - start > 4) { | |
2143 error('an IPv6 part can only contain a maximum of 4 hex digits', start); | |
2144 } | |
2145 int value = int.parse(host.substring(start, end), radix: 16); | |
2146 if (value < 0 || value > (1 << 16) - 1) { | |
2147 error('each part must be in the range of `0x0..0xFFFF`', start); | |
2148 } | |
2149 return value; | |
2150 } | |
2151 if (host.length < 2) error('address is too short'); | |
2152 List<int> parts = []; | |
2153 bool wildcardSeen = false; | |
2154 int partStart = start; | |
2155 // Parse all parts, except a potential last one. | |
2156 for (int i = start; i < end; i++) { | |
2157 if (host.codeUnitAt(i) == _COLON) { | |
2158 if (i == start) { | |
2159 // If we see a `:` in the beginning, expect wildcard. | |
2160 i++; | |
2161 if (host.codeUnitAt(i) != _COLON) { | |
2162 error('invalid start colon.', i); | |
2163 } | |
2164 partStart = i; | |
2165 } | |
2166 if (i == partStart) { | |
2167 // Wildcard. We only allow one. | |
2168 if (wildcardSeen) { | |
2169 error('only one wildcard `::` is allowed', i); | |
2170 } | |
2171 wildcardSeen = true; | |
2172 parts.add(-1); | |
2173 } else { | |
2174 // Found a single colon. Parse [partStart..i] as a hex entry. | |
2175 parts.add(parseHex(partStart, i)); | |
2176 } | |
2177 partStart = i + 1; | |
2178 } | |
2179 } | |
2180 if (parts.length == 0) error('too few parts'); | |
2181 bool atEnd = (partStart == end); | |
2182 bool isLastWildcard = (parts.last == -1); | |
2183 if (atEnd && !isLastWildcard) { | |
2184 error('expected a part after last `:`', end); | |
2185 } | |
2186 if (!atEnd) { | |
2187 try { | |
2188 parts.add(parseHex(partStart, end)); | |
2189 } catch (e) { | |
2190 // Failed to parse the last chunk as hex. Try IPv4. | |
2191 try { | |
2192 List<int> last = parseIPv4Address(host.substring(partStart, end)); | |
2193 parts.add(last[0] << 8 | last[1]); | |
2194 parts.add(last[2] << 8 | last[3]); | |
2195 } catch (e) { | |
2196 error('invalid end of IPv6 address.', partStart); | |
2197 } | |
2198 } | |
2199 } | |
2200 if (wildcardSeen) { | |
2201 if (parts.length > 7) { | |
2202 error('an address with a wildcard must have less than 7 parts'); | |
2203 } | |
2204 } else if (parts.length != 8) { | |
2205 error('an address without a wildcard must contain exactly 8 parts'); | |
2206 } | |
2207 // TODO(ajohnsen): Consider using Uint8List. | |
2208 List bytes = new List<int>(16); | |
2209 for (int i = 0, index = 0; i < parts.length; i++) { | |
2210 int value = parts[i]; | |
2211 if (value == -1) { | |
2212 int wildCardLength = 9 - parts.length; | |
2213 for (int j = 0; j < wildCardLength; j++) { | |
2214 bytes[index] = 0; | |
2215 bytes[index + 1] = 0; | |
2216 index += 2; | |
2217 } | |
2218 } else { | |
2219 bytes[index] = value >> 8; | |
2220 bytes[index + 1] = value & 0xff; | |
2221 index += 2; | |
2222 } | |
2223 } | |
2224 return bytes; | |
2225 } | |
2226 | |
2227 // Frequently used character codes. | |
2228 static const int _SPACE = 0x20; | |
2229 static const int _DOUBLE_QUOTE = 0x22; | |
2230 static const int _NUMBER_SIGN = 0x23; | |
2231 static const int _PERCENT = 0x25; | |
2232 static const int _ASTERISK = 0x2A; | |
2233 static const int _PLUS = 0x2B; | |
2234 static const int _DOT = 0x2E; | |
2235 static const int _SLASH = 0x2F; | |
2236 static const int _ZERO = 0x30; | |
2237 static const int _NINE = 0x39; | |
2238 static const int _COLON = 0x3A; | |
2239 static const int _LESS = 0x3C; | |
2240 static const int _GREATER = 0x3E; | |
2241 static const int _QUESTION = 0x3F; | |
2242 static const int _AT_SIGN = 0x40; | |
2243 static const int _UPPER_CASE_A = 0x41; | |
2244 static const int _UPPER_CASE_F = 0x46; | |
2245 static const int _UPPER_CASE_Z = 0x5A; | |
2246 static const int _LEFT_BRACKET = 0x5B; | |
2247 static const int _BACKSLASH = 0x5C; | |
2248 static const int _RIGHT_BRACKET = 0x5D; | |
2249 static const int _LOWER_CASE_A = 0x61; | |
2250 static const int _LOWER_CASE_F = 0x66; | |
2251 static const int _LOWER_CASE_Z = 0x7A; | |
2252 static const int _BAR = 0x7C; | |
2253 | |
2254 /** | |
2255 * This is the internal implementation of JavaScript's encodeURI function. | |
2256 * It encodes all characters in the string [text] except for those | |
2257 * that appear in [canonicalTable], and returns the escaped string. | |
2258 */ | |
2259 static String _uriEncode(List<int> canonicalTable, | |
2260 String text, | |
2261 {Encoding encoding: UTF8, | |
2262 bool spaceToPlus: false}) { | |
2263 byteToHex(byte, buffer) { | |
2264 const String hex = '0123456789ABCDEF'; | |
2265 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); | |
2266 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); | |
2267 } | |
2268 | |
2269 // Encode the string into bytes then generate an ASCII only string | |
2270 // by percent encoding selected bytes. | |
2271 StringBuffer result = new StringBuffer(); | |
2272 var bytes = encoding.encode(text); | |
2273 for (int i = 0; i < bytes.length; i++) { | |
2274 int byte = bytes[i]; | |
2275 if (byte < 128 && | |
2276 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { | |
2277 result.writeCharCode(byte); | |
2278 } else if (spaceToPlus && byte == _SPACE) { | |
2279 result.writeCharCode(_PLUS); | |
2280 } else { | |
2281 result.writeCharCode(_PERCENT); | |
2282 byteToHex(byte, result); | |
2283 } | |
2284 } | |
2285 return result.toString(); | |
2286 } | |
2287 | |
2288 /** | |
2289 * Convert a byte (2 character hex sequence) in string [s] starting | |
2290 * at position [pos] to its ordinal value | |
2291 */ | |
2292 static int _hexCharPairToByte(String s, int pos) { | |
2293 int byte = 0; | |
2294 for (int i = 0; i < 2; i++) { | |
2295 var charCode = s.codeUnitAt(pos + i); | |
2296 if (0x30 <= charCode && charCode <= 0x39) { | |
2297 byte = byte * 16 + charCode - 0x30; | |
2298 } else { | |
2299 // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). | |
2300 charCode |= 0x20; | |
2301 if (0x61 <= charCode && charCode <= 0x66) { | |
2302 byte = byte * 16 + charCode - 0x57; | |
2303 } else { | |
2304 throw new ArgumentError("Invalid URL encoding"); | |
2305 } | |
2306 } | |
2307 } | |
2308 return byte; | |
2309 } | |
2310 | |
2311 /** | |
2312 * Uri-decode a percent-encoded string. | |
2313 * | |
2314 * It unescapes the string [text] and returns the unescaped string. | |
2315 * | |
2316 * This function is similar to the JavaScript-function `decodeURI`. | |
2317 * | |
2318 * If [plusToSpace] is `true`, plus characters will be converted to spaces. | |
2319 * | |
2320 * The decoder will create a byte-list of the percent-encoded parts, and then | |
2321 * decode the byte-list using [encoding]. The default encodingis UTF-8. | |
2322 */ | |
2323 static String _uriDecode(String text, | |
2324 {bool plusToSpace: false, | |
2325 Encoding encoding: UTF8}) { | |
2326 // First check whether there is any characters which need special handling. | |
2327 bool simple = true; | |
2328 for (int i = 0; i < text.length && simple; i++) { | |
2329 var codeUnit = text.codeUnitAt(i); | |
2330 simple = codeUnit != _PERCENT && codeUnit != _PLUS; | |
2331 } | |
2332 List<int> bytes; | |
2333 if (simple) { | |
2334 if (encoding == UTF8 || encoding == LATIN1) { | |
2335 return text; | |
2336 } else { | |
2337 bytes = text.codeUnits; | |
2338 } | |
2339 } else { | |
2340 bytes = new List(); | |
2341 for (int i = 0; i < text.length; i++) { | |
2342 var codeUnit = text.codeUnitAt(i); | |
2343 if (codeUnit > 127) { | |
2344 throw new ArgumentError("Illegal percent encoding in URI"); | |
2345 } | |
2346 if (codeUnit == _PERCENT) { | |
2347 if (i + 3 > text.length) { | |
2348 throw new ArgumentError('Truncated URI'); | |
2349 } | |
2350 bytes.add(_hexCharPairToByte(text, i + 1)); | |
2351 i += 2; | |
2352 } else if (plusToSpace && codeUnit == _PLUS) { | |
2353 bytes.add(_SPACE); | |
2354 } else { | |
2355 bytes.add(codeUnit); | |
2356 } | |
2357 } | |
2358 } | |
2359 return encoding.decode(bytes); | |
2360 } | |
2361 | |
2362 static bool _isAlphabeticCharacter(int codeUnit) | |
2363 => (codeUnit >= _LOWER_CASE_A && codeUnit <= _LOWER_CASE_Z) || | |
2364 (codeUnit >= _UPPER_CASE_A && codeUnit <= _UPPER_CASE_Z); | |
2365 | |
2366 // Tables of char-codes organized as a bit vector of 128 bits where | |
2367 // each bit indicate whether a character code on the 0-127 needs to | |
2368 // be escaped or not. | |
2369 | |
2370 // The unreserved characters of RFC 3986. | |
2371 static const _unreservedTable = const [ | |
2372 // LSB MSB | |
2373 // | | | |
2374 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2375 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2376 // -. | |
2377 0x6000, // 0x20 - 0x2f 0000000000000110 | |
2378 // 0123456789 | |
2379 0x03ff, // 0x30 - 0x3f 1111111111000000 | |
2380 // ABCDEFGHIJKLMNO | |
2381 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2382 // PQRSTUVWXYZ _ | |
2383 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2384 // abcdefghijklmno | |
2385 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2386 // pqrstuvwxyz ~ | |
2387 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2388 | |
2389 // The unreserved characters of RFC 2396. | |
2390 static const _unreserved2396Table = const [ | |
2391 // LSB MSB | |
2392 // | | | |
2393 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2394 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2395 // ! '()* -. | |
2396 0x6782, // 0x20 - 0x2f 0100000111100110 | |
2397 // 0123456789 | |
2398 0x03ff, // 0x30 - 0x3f 1111111111000000 | |
2399 // ABCDEFGHIJKLMNO | |
2400 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2401 // PQRSTUVWXYZ _ | |
2402 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2403 // abcdefghijklmno | |
2404 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2405 // pqrstuvwxyz ~ | |
2406 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2407 | |
2408 // Table of reserved characters specified by ECMAScript 5. | |
2409 static const _encodeFullTable = const [ | |
2410 // LSB MSB | |
2411 // | | | |
2412 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2413 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2414 // ! #$ &'()*+,-./ | |
2415 0xffda, // 0x20 - 0x2f 0101101111111111 | |
2416 // 0123456789:; = ? | |
2417 0xafff, // 0x30 - 0x3f 1111111111110101 | |
2418 // @ABCDEFGHIJKLMNO | |
2419 0xffff, // 0x40 - 0x4f 1111111111111111 | |
2420 // PQRSTUVWXYZ _ | |
2421 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2422 // abcdefghijklmno | |
2423 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2424 // pqrstuvwxyz ~ | |
2425 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2426 | |
2427 // Characters allowed in the scheme. | |
2428 static const _schemeTable = const [ | |
2429 // LSB MSB | |
2430 // | | | |
2431 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2432 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2433 // + -. | |
2434 0x6800, // 0x20 - 0x2f 0000000000010110 | |
2435 // 0123456789 | |
2436 0x03ff, // 0x30 - 0x3f 1111111111000000 | |
2437 // ABCDEFGHIJKLMNO | |
2438 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2439 // PQRSTUVWXYZ | |
2440 0x07ff, // 0x50 - 0x5f 1111111111100001 | |
2441 // abcdefghijklmno | |
2442 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2443 // pqrstuvwxyz | |
2444 0x07ff]; // 0x70 - 0x7f 1111111111100010 | |
2445 | |
2446 // Characters allowed in scheme except for upper case letters. | |
2447 static const _schemeLowerTable = const [ | |
2448 // LSB MSB | |
2449 // | | | |
2450 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2451 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2452 // + -. | |
2453 0x6800, // 0x20 - 0x2f 0000000000010110 | |
2454 // 0123456789 | |
2455 0x03ff, // 0x30 - 0x3f 1111111111000000 | |
2456 // | |
2457 0x0000, // 0x40 - 0x4f 0111111111111111 | |
2458 // | |
2459 0x0000, // 0x50 - 0x5f 1111111111100001 | |
2460 // abcdefghijklmno | |
2461 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2462 // pqrstuvwxyz | |
2463 0x07ff]; // 0x70 - 0x7f 1111111111100010 | |
2464 | |
2465 // Sub delimiter characters combined with unreserved as of 3986. | |
2466 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" | |
2467 // / "*" / "+" / "," / ";" / "=" | |
2468 // RFC 3986 section 2.3. | |
2469 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" | |
2470 static const _subDelimitersTable = const [ | |
2471 // LSB MSB | |
2472 // | | | |
2473 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2474 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2475 // ! $ &'()*+,-. | |
2476 0x7fd2, // 0x20 - 0x2f 0100101111111110 | |
2477 // 0123456789 ; = | |
2478 0x2bff, // 0x30 - 0x3f 1111111111010100 | |
2479 // ABCDEFGHIJKLMNO | |
2480 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2481 // PQRSTUVWXYZ _ | |
2482 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2483 // abcdefghijklmno | |
2484 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2485 // pqrstuvwxyz ~ | |
2486 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2487 | |
2488 // General delimiter characters, RFC 3986 section 2.2. | |
2489 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" | |
2490 // | |
2491 static const _genDelimitersTable = const [ | |
2492 // LSB MSB | |
2493 // | | | |
2494 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2495 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2496 // # / | |
2497 0x8008, // 0x20 - 0x2f 0001000000000001 | |
2498 // : ? | |
2499 0x8400, // 0x30 - 0x3f 0000000000100001 | |
2500 // @ | |
2501 0x0001, // 0x40 - 0x4f 1000000000000000 | |
2502 // [ ] | |
2503 0x2800, // 0x50 - 0x5f 0000000000010100 | |
2504 // | |
2505 0x0000, // 0x60 - 0x6f 0000000000000000 | |
2506 // | |
2507 0x0000]; // 0x70 - 0x7f 0000000000000000 | |
2508 | |
2509 // Characters allowed in the userinfo as of RFC 3986. | |
2510 // RFC 3986 Apendix A | |
2511 // userinfo = *( unreserved / pct-encoded / sub-delims / ':') | |
2512 static const _userinfoTable = const [ | |
2513 // LSB MSB | |
2514 // | | | |
2515 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2516 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2517 // ! $ &'()*+,-. | |
2518 0x7fd2, // 0x20 - 0x2f 0100101111111110 | |
2519 // 0123456789:; = | |
2520 0x2fff, // 0x30 - 0x3f 1111111111110100 | |
2521 // ABCDEFGHIJKLMNO | |
2522 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2523 // PQRSTUVWXYZ _ | |
2524 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2525 // abcdefghijklmno | |
2526 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2527 // pqrstuvwxyz ~ | |
2528 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2529 | |
2530 // Characters allowed in the reg-name as of RFC 3986. | |
2531 // RFC 3986 Apendix A | |
2532 // reg-name = *( unreserved / pct-encoded / sub-delims ) | |
2533 static const _regNameTable = const [ | |
2534 // LSB MSB | |
2535 // | | | |
2536 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2537 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2538 // ! $%&'()*+,-. | |
2539 0x7ff2, // 0x20 - 0x2f 0100111111111110 | |
2540 // 0123456789 ; = | |
2541 0x2bff, // 0x30 - 0x3f 1111111111010100 | |
2542 // ABCDEFGHIJKLMNO | |
2543 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
2544 // PQRSTUVWXYZ _ | |
2545 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2546 // abcdefghijklmno | |
2547 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2548 // pqrstuvwxyz ~ | |
2549 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2550 | |
2551 // Characters allowed in the path as of RFC 3986. | |
2552 // RFC 3986 section 3.3. | |
2553 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | |
2554 static const _pathCharTable = const [ | |
2555 // LSB MSB | |
2556 // | | | |
2557 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2558 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2559 // ! $ &'()*+,-. | |
2560 0x7fd2, // 0x20 - 0x2f 0100101111111110 | |
2561 // 0123456789:; = | |
2562 0x2fff, // 0x30 - 0x3f 1111111111110100 | |
2563 // @ABCDEFGHIJKLMNO | |
2564 0xffff, // 0x40 - 0x4f 1111111111111111 | |
2565 // PQRSTUVWXYZ _ | |
2566 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2567 // abcdefghijklmno | |
2568 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2569 // pqrstuvwxyz ~ | |
2570 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2571 | |
2572 // Characters allowed in the path as of RFC 3986. | |
2573 // RFC 3986 section 3.3 *and* slash. | |
2574 static const _pathCharOrSlashTable = const [ | |
2575 // LSB MSB | |
2576 // | | | |
2577 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2578 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2579 // ! $ &'()*+,-./ | |
2580 0xffd2, // 0x20 - 0x2f 0100101111111111 | |
2581 // 0123456789:; = | |
2582 0x2fff, // 0x30 - 0x3f 1111111111110100 | |
2583 // @ABCDEFGHIJKLMNO | |
2584 0xffff, // 0x40 - 0x4f 1111111111111111 | |
2585 | |
2586 // PQRSTUVWXYZ _ | |
2587 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2588 // abcdefghijklmno | |
2589 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2590 // pqrstuvwxyz ~ | |
2591 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2592 | |
2593 // Characters allowed in the query as of RFC 3986. | |
2594 // RFC 3986 section 3.4. | |
2595 // query = *( pchar / "/" / "?" ) | |
2596 static const _queryCharTable = const [ | |
2597 // LSB MSB | |
2598 // | | | |
2599 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2600 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2601 // ! $ &'()*+,-./ | |
2602 0xffd2, // 0x20 - 0x2f 0100101111111111 | |
2603 // 0123456789:; = ? | |
2604 0xafff, // 0x30 - 0x3f 1111111111110101 | |
2605 // @ABCDEFGHIJKLMNO | |
2606 0xffff, // 0x40 - 0x4f 1111111111111111 | |
2607 // PQRSTUVWXYZ _ | |
2608 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2609 // abcdefghijklmno | |
2610 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2611 // pqrstuvwxyz ~ | |
2612 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2613 } | |
OLD | NEW |