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