Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(641)

Side by Side Diff: tool/input_sdk/lib/core/uri.dart

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

Powered by Google App Engine
This is Rietveld 408576698