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