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