OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of dart.core; | 5 part of dart.core; |
6 | 6 |
7 /** | 7 /** |
8 * A parsed URI, such as a URL. | 8 * A parsed URI, such as a URL. |
9 * | 9 * |
10 * **See also:** | 10 * **See also:** |
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
109 * Cache the computed return value of [pathSegements]. | 109 * Cache the computed return value of [pathSegements]. |
110 */ | 110 */ |
111 List<String> _pathSegments; | 111 List<String> _pathSegments; |
112 | 112 |
113 /** | 113 /** |
114 * Cache the computed return value of [queryParameters]. | 114 * Cache the computed return value of [queryParameters]. |
115 */ | 115 */ |
116 Map<String, String> _queryParameters; | 116 Map<String, String> _queryParameters; |
117 | 117 |
118 /** | 118 /** |
119 * Creates a new URI object by parsing a URI string. | 119 * Creates a new `Uri` object by parsing a URI string. |
| 120 * |
| 121 * If the string is not valid as a URI or URI reference, |
| 122 * invalid characters will be percent escaped where possible. |
| 123 * The resulting `Uri` will represent a valid URI or URI reference. |
120 */ | 124 */ |
121 static Uri parse(String uri) { | 125 static Uri parse(String uri) { |
122 // This parsing will not validate percent-encoding, IPv6, etc. When done | 126 // This parsing will not validate percent-encoding, IPv6, etc. When done |
123 // it will call `new Uri(...)` which will perform these validations. | 127 // it will call `new Uri(...)` which will perform these validations. |
124 // This is purely splitting up the URI string into components. | 128 // This is purely splitting up the URI string into components. |
125 // | 129 // |
126 // Important parts of the RFC 3986 used here: | 130 // Important parts of the RFC 3986 used here: |
127 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] | 131 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] |
128 // | 132 // |
129 // hier-part = "//" authority path-abempty | 133 // hier-part = "//" authority path-abempty |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
168 // ; non-zero-length segment without any colon ":" | 172 // ; non-zero-length segment without any colon ":" |
169 // | 173 // |
170 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 174 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
171 // | 175 // |
172 // query = *( pchar / "/" / "?" ) | 176 // query = *( pchar / "/" / "?" ) |
173 // | 177 // |
174 // fragment = *( pchar / "/" / "?" ) | 178 // fragment = *( pchar / "/" / "?" ) |
175 bool isRegName(int ch) { | 179 bool isRegName(int ch) { |
176 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 180 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
177 } | 181 } |
178 | 182 const int EOI = -1; |
179 int ipV6Address(int index) { | 183 |
180 // IPv6. Skip to ']'. | 184 String scheme = ""; |
181 index = uri.indexOf(']', index); | 185 String path; |
182 if (index == -1) { | 186 String userinfo = ""; |
183 throw new FormatException("Bad end of IPv6 host"); | 187 String host = ""; |
184 } | 188 int port = 0; |
185 return index + 1; | 189 String query = ""; |
186 } | 190 String fragment = ""; |
187 | 191 |
188 int length = uri.length; | |
189 int index = 0; | 192 int index = 0; |
190 | 193 int pathStart = 0; |
191 int schemeEndIndex = 0; | 194 // End of input-marker. |
192 | 195 int char = EOI; |
193 if (length == 0) { | 196 |
194 return new Uri(); | 197 void parseAuth() { |
195 } | 198 if (index == uri.length) { |
196 | 199 char = EOI; |
197 if (uri.codeUnitAt(0) != _SLASH) { | 200 return; |
198 // Can be scheme. | 201 } |
199 while (index < length) { | 202 int authStart = index; |
200 // Look for ':'. If found, continue from the post of ':'. If not (end | 203 int lastColon = -1; |
201 // reached or invalid scheme char found) back up one char, and continue | 204 int lastAt = -1; |
202 // to path. | 205 char = uri.codeUnitAt(index); |
203 // Note that scheme-chars is contained in path-chars. | 206 while (index < uri.length) { |
204 int codeUnit = uri.codeUnitAt(index++); | 207 char = uri.codeUnitAt(index); |
205 if (!_isSchemeCharacter(codeUnit)) { | 208 if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) { |
206 if (codeUnit == _COLON) { | |
207 schemeEndIndex = index; | |
208 } else { | |
209 // Back up one char, since we met an invalid scheme char. | |
210 index--; | |
211 } | |
212 break; | 209 break; |
213 } | 210 } |
214 } | 211 if (char == _AT_SIGN) { |
215 } | 212 lastAt = index; |
216 | 213 lastColon = -1; |
217 int userInfoEndIndex = -1; | 214 } else if (char == _COLON) { |
218 int portIndex = -1; | 215 lastColon = index; |
219 int authorityEndIndex = schemeEndIndex; | 216 } else if (char == _LEFT_BRACKET) { |
220 // If we see '//', there must be an authority. | 217 lastColon = -1; |
221 if (authorityEndIndex == index && | 218 int endBracket = uri.indexOf(']', index + 1); |
222 authorityEndIndex + 1 < length && | 219 if (endBracket == -1) { |
223 uri.codeUnitAt(authorityEndIndex) == _SLASH && | 220 index = uri.length; |
224 uri.codeUnitAt(authorityEndIndex + 1) == _SLASH) { | 221 char = EOI; |
225 // Skip '//'. | |
226 authorityEndIndex += 2; | |
227 // It can both be host and userInfo. | |
228 while (authorityEndIndex < length) { | |
229 int codeUnit = uri.codeUnitAt(authorityEndIndex++); | |
230 if (!isRegName(codeUnit)) { | |
231 if (codeUnit == _LEFT_BRACKET) { | |
232 authorityEndIndex = ipV6Address(authorityEndIndex); | |
233 } else if (portIndex == -1 && codeUnit == _COLON) { | |
234 // First time ':'. | |
235 portIndex = authorityEndIndex; | |
236 } else if (codeUnit == _AT_SIGN || codeUnit == _COLON) { | |
237 // Second time ':' or first '@'. Must be userInfo. | |
238 userInfoEndIndex = uri.indexOf('@', authorityEndIndex - 1); | |
239 // Not found. Must be path then. | |
240 if (userInfoEndIndex == -1) { | |
241 authorityEndIndex = index; | |
242 break; | |
243 } | |
244 portIndex = -1; | |
245 authorityEndIndex = userInfoEndIndex + 1; | |
246 // Now it can only be host:port. | |
247 while (authorityEndIndex < length) { | |
248 int codeUnit = uri.codeUnitAt(authorityEndIndex++); | |
249 if (!isRegName(codeUnit)) { | |
250 if (codeUnit == _LEFT_BRACKET) { | |
251 authorityEndIndex = ipV6Address(authorityEndIndex); | |
252 } else if (codeUnit == _COLON) { | |
253 if (portIndex != -1) { | |
254 throw new FormatException("Double port in host"); | |
255 } | |
256 portIndex = authorityEndIndex; | |
257 } else { | |
258 authorityEndIndex--; | |
259 break; | |
260 } | |
261 } | |
262 } | |
263 break; | 222 break; |
264 } else { | 223 } else { |
265 authorityEndIndex--; | 224 index = endBracket; |
266 break; | |
267 } | 225 } |
268 } | 226 } |
269 } | 227 index++; |
| 228 char = EOI; |
| 229 } |
| 230 int hostStart = authStart; |
| 231 int hostEnd = index; |
| 232 if (lastAt >= 0) { |
| 233 userinfo = _makeUserInfo(uri, authStart, lastAt); |
| 234 hostStart = lastAt + 1; |
| 235 } |
| 236 if (lastColon >= 0) { |
| 237 if (lastColon + 1 == index) { |
| 238 _fail(uri, index, "Invalid port number"); |
| 239 } |
| 240 int portNumber = 0; |
| 241 for (int i = lastColon + 1; i < index; i++) { |
| 242 int digit = uri.codeUnitAt(i); |
| 243 if (_ZERO > digit || _NINE < digit) { |
| 244 _fail(uri, i, "Invalid port number"); |
| 245 } |
| 246 portNumber = portNumber * 10 + (digit - _ZERO); |
| 247 } |
| 248 port = _makePort(portNumber, scheme); |
| 249 hostEnd = lastColon; |
| 250 } |
| 251 host = _makeHost(uri, hostStart, hostEnd, true); |
| 252 if (index < uri.length) { |
| 253 char = uri.codeUnitAt(index); |
| 254 } |
| 255 } |
| 256 |
| 257 // When reaching path parsing, the current character is known to not |
| 258 // be part of the path. |
| 259 const int NOT_IN_PATH = 0; |
| 260 // When reaching path parsing, the current character is part |
| 261 // of the a non-empty path. |
| 262 const int IN_PATH = 1; |
| 263 // When reaching authority parsing, authority is possible. |
| 264 // This is only true at start or right after scheme. |
| 265 const int ALLOW_AUTH = 2; |
| 266 |
| 267 // Current state. |
| 268 // Initialized to the default value that is used when exiting the |
| 269 // scheme loop by reaching the end of input. |
| 270 // All other breaks set their own state. |
| 271 int state = NOT_IN_PATH; |
| 272 while (index < uri.length) { |
| 273 char = uri.codeUnitAt(index); |
| 274 if (char == _QUESTION || char == _NUMBER_SIGN) { |
| 275 state = NOT_IN_PATH; |
| 276 break; |
| 277 } |
| 278 if (char == _SLASH) { |
| 279 state = (index == 0) ? ALLOW_AUTH : IN_PATH; |
| 280 break; |
| 281 } |
| 282 if (char == _COLON) { |
| 283 if (index == 0) _fail(uri, 0, "Invalid empty scheme"); |
| 284 scheme = _makeScheme(uri, index); |
| 285 index++; |
| 286 pathStart = index; |
| 287 if (index == uri.length) { |
| 288 char = EOI; |
| 289 state = NOT_IN_PATH; |
| 290 } else { |
| 291 char = uri.codeUnitAt(index); |
| 292 if (char == _QUESTION || char == _NUMBER_SIGN) { |
| 293 state = NOT_IN_PATH; |
| 294 } else if (char == _SLASH) { |
| 295 state = ALLOW_AUTH; |
| 296 } else { |
| 297 state = IN_PATH; |
| 298 } |
| 299 } |
| 300 break; |
| 301 } |
| 302 index++; |
| 303 char = EOI; |
| 304 } |
| 305 |
| 306 if (state == ALLOW_AUTH) { |
| 307 assert(char == _SLASH); |
| 308 // Have seen one slash either at start or right after scheme. |
| 309 // If two slashes, it's an authority, otherwise it's just the path. |
| 310 index++; |
| 311 if (index == uri.length) { |
| 312 char = EOI; |
| 313 state = NOT_IN_PATH; |
| 314 } else { |
| 315 char = uri.codeUnitAt(index); |
| 316 if (char == _SLASH) { |
| 317 index++; |
| 318 parseAuth(); |
| 319 pathStart = index; |
| 320 } |
| 321 if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { |
| 322 state = NOT_IN_PATH; |
| 323 } else { |
| 324 state = IN_PATH; |
| 325 } |
| 326 } |
| 327 } |
| 328 |
| 329 assert(state == IN_PATH || state == NOT_IN_PATH); |
| 330 if (state == IN_PATH) { |
| 331 // Characters from pathStart to index (inclusive) are known |
| 332 // to be part of the path. |
| 333 while (++index < uri.length) { |
| 334 char = uri.codeUnitAt(index); |
| 335 if (char == _QUESTION || char == _NUMBER_SIGN) { |
| 336 break; |
| 337 } |
| 338 char = EOI; |
| 339 } |
| 340 state = NOT_IN_PATH; |
| 341 } |
| 342 |
| 343 assert(state == NOT_IN_PATH); |
| 344 bool ensureLeadingSlash = (host != "" || scheme == "file"); |
| 345 path = _makePath(uri, pathStart, index, null, ensureLeadingSlash); |
| 346 |
| 347 if (char == _QUESTION) { |
| 348 int numberSignIndex = uri.indexOf('#', index + 1); |
| 349 if (numberSignIndex < 0) { |
| 350 query = _makeQuery(uri, index + 1, uri.length, null); |
| 351 } else { |
| 352 query = _makeQuery(uri, index + 1, numberSignIndex, null); |
| 353 fragment = _makeFragment(uri, numberSignIndex + 1, uri.length); |
| 354 } |
| 355 } else if (char == _NUMBER_SIGN) { |
| 356 fragment = _makeFragment(uri, index + 1, uri.length); |
| 357 } |
| 358 return new Uri._internal(scheme, |
| 359 userinfo, |
| 360 host, |
| 361 port, |
| 362 path, |
| 363 query, |
| 364 fragment); |
| 365 } |
| 366 |
| 367 // Report a parse failure. |
| 368 static void _fail(String uri, int index, String message) { |
| 369 // TODO(lrn): Consider adding this to FormatException. |
| 370 if (index == uri.length) { |
| 371 message += " at end of input."; |
270 } else { | 372 } else { |
271 authorityEndIndex = schemeEndIndex; | 373 message += " at position $index.\n"; |
272 } | 374 // Pick a slice of uri containing index and, if |
273 | 375 // necessary, truncate the ends to ensure the entire |
274 // At path now. | 376 // slice fits on one line. |
275 int pathEndIndex = authorityEndIndex; | 377 int min = 0; |
276 while (pathEndIndex < length) { | 378 int max = uri.length; |
277 int codeUnit = uri.codeUnitAt(pathEndIndex++); | 379 String pre = ""; |
278 if (codeUnit == _QUESTION || codeUnit == _NUMBER_SIGN) { | 380 String post = ""; |
279 pathEndIndex--; | 381 if (uri.length > 78) { |
280 break; | 382 min = index - 10; |
281 } | 383 if (min < 0) min = 0; |
282 } | 384 int max = min + 72; |
283 | 385 if (max > uri.length) { |
284 // Maybe query. | 386 max = uri.length; |
285 int queryEndIndex = pathEndIndex; | 387 min = max - 72; |
286 if (queryEndIndex < length && uri.codeUnitAt(queryEndIndex) == _QUESTION) { | 388 } |
287 while (queryEndIndex < length) { | 389 if (min != 0) pre = "..."; |
288 int codeUnit = uri.codeUnitAt(queryEndIndex++); | 390 if (max != uri.length) post = "..."; |
289 if (codeUnit == _NUMBER_SIGN) { | 391 } |
290 queryEndIndex--; | 392 // Combine message, slice and a caret pointing to the error index. |
291 break; | 393 message = "$message$pre${uri.substring(min, max)}$post\n" |
292 } | 394 "${' ' * (pre.length + index - min)}^"; |
293 } | 395 } |
294 } | 396 throw new FormatException(message); |
295 | |
296 var scheme = null; | |
297 if (schemeEndIndex > 0) { | |
298 scheme = uri.substring(0, schemeEndIndex - 1); | |
299 } | |
300 | |
301 var host = ""; | |
302 var userInfo = ""; | |
303 var port = 0; | |
304 if (schemeEndIndex != authorityEndIndex) { | |
305 int startIndex = schemeEndIndex + 2; | |
306 if (userInfoEndIndex > 0) { | |
307 userInfo = uri.substring(startIndex, userInfoEndIndex); | |
308 startIndex = userInfoEndIndex + 1; | |
309 } | |
310 if (portIndex > 0) { | |
311 var portStr = uri.substring(portIndex, authorityEndIndex); | |
312 try { | |
313 port = int.parse(portStr); | |
314 } catch (_) { | |
315 throw new FormatException("Invalid port: '$portStr'"); | |
316 } | |
317 host = uri.substring(startIndex, portIndex - 1); | |
318 } else { | |
319 host = uri.substring(startIndex, authorityEndIndex); | |
320 } | |
321 } | |
322 | |
323 var path = uri.substring(authorityEndIndex, pathEndIndex); | |
324 var query = ""; | |
325 if (pathEndIndex < queryEndIndex) { | |
326 query = uri.substring(pathEndIndex + 1, queryEndIndex); | |
327 } | |
328 var fragment = ""; | |
329 // If queryEndIndex is not at end (length), there is a fragment. | |
330 if (queryEndIndex < length) { | |
331 fragment = uri.substring(queryEndIndex + 1, length); | |
332 } | |
333 | |
334 return new Uri(scheme: scheme, | |
335 userInfo: userInfo, | |
336 host: host, | |
337 port: port, | |
338 path: path, | |
339 query: query, | |
340 fragment: fragment); | |
341 } | 397 } |
342 | 398 |
| 399 /// Internal non-verifying constructor. Only call with validated arguments. |
| 400 Uri._internal(this.scheme, |
| 401 this.userInfo, |
| 402 this._host, |
| 403 this._port, |
| 404 this._path, |
| 405 this.query, |
| 406 this.fragment); |
| 407 |
343 /** | 408 /** |
344 * Creates a new URI from its components. | 409 * Creates a new URI from its components. |
345 * | 410 * |
346 * Each component is set through a named argument. Any number of | 411 * Each component is set through a named argument. Any number of |
347 * components can be provided. The default value for the components | 412 * components can be provided. The default value for the components |
348 * not provided is the empry string, except for [port] which has a | 413 * not provided is the empry string, except for [port] which has a |
349 * default value of 0. The [path] and [query] components can be set | 414 * default value of 0. The [path] and [query] components can be set |
350 * using two different named arguments. | 415 * using two different named arguments. |
351 * | 416 * |
352 * The scheme component is set through [scheme]. The scheme is | 417 * The scheme component is set through [scheme]. The scheme is |
353 * normalized to all lowercase letters. | 418 * normalized to all lowercase letters. |
354 * | 419 * |
355 * The user info part of the authority component is set through | 420 * The user info part of the authority component is set through |
356 * [userInfo]. | 421 * [userInfo]. |
357 * | 422 * |
358 * The host part of the authority component is set through | 423 * The host part of the authority component is set through |
359 * [host]. The host can either be a hostname, an IPv4 address or an | 424 * [host]. The host can either be a hostname, an IPv4 address or an |
360 * IPv6 address, contained in '[' and ']'. If the host contains a | 425 * IPv6 address, contained in '[' and ']'. If the host contains a |
361 * ':' character, the '[' and ']' are added if not already provided. | 426 * ':' character, the '[' and ']' are added if not already provided. |
| 427 * The host is normalized to all lowercase letters. |
362 * | 428 * |
363 * The port part of the authority component is set through | 429 * The port part of the authority component is set through |
364 * [port]. The port is normalized for scheme http and https where | 430 * [port]. The port is normalized for scheme http and https where |
365 * port 80 and port 443 respectively is set. | 431 * port 80 and port 443 respectively is set. |
366 * | 432 * |
367 * The path component is set through either [path] or | 433 * The path component is set through either [path] or |
368 * [pathSegments]. When [path] is used, the provided string is | 434 * [pathSegments]. When [path] is used, the provided string is |
369 * expected to be fully percent-encoded, and is used in its literal | 435 * expected to be fully percent-encoded, and is used in its literal |
370 * form. When [pathSegments] is used, each of the provided segments | 436 * form. When [pathSegments] is used, each of the provided segments |
371 * is percent-encoded and joined using the forward slash | 437 * is percent-encoded and joined using the forward slash |
372 * separator. The percent-encoding of the path segments encodes all | 438 * separator. The percent-encoding of the path segments encodes all |
373 * characters except for the unreserved characters and the following | 439 * characters except for the unreserved characters and the following |
374 * list of characters: `!$&'()*+,;=:@`. If the other components | 440 * list of characters: `!$&'()*+,;=:@`. If the other components |
375 * calls for an absolute path a leading slash `/` is prepended if | 441 * calls for an absolute path a leading slash `/` is prepended if |
376 * not already there. | 442 * not already there. |
377 * | 443 * |
378 * The query component is set through either [query] or | 444 * The query component is set through either [query] or |
379 * [queryParameters]. When [query] is used the provided string is | 445 * [queryParameters]. When [query] is used the provided string is |
380 * expected to be fully percent-encoded and is used in its literal | 446 * expected to be fully percent-encoded and is used in its literal |
381 * form. When [queryParameters] is used the query is built from the | 447 * form. When [queryParameters] is used the query is built from the |
382 * provided map. Each key and value in the map is percent-encoded | 448 * provided map. Each key and value in the map is percent-encoded |
383 * and joined using equal and ampersand characters. The | 449 * and joined using equal and ampersand characters. The |
384 * percent-encoding of the keys and values encodes all characters | 450 * percent-encoding of the keys and values encodes all characters |
385 * except for the unreserved characters. | 451 * except for the unreserved characters. |
386 * | 452 * |
387 * The fragment component is set through [fragment]. | 453 * The fragment component is set through [fragment]. |
388 */ | 454 */ |
389 Uri({String scheme, | 455 factory Uri({String scheme, |
390 this.userInfo: "", | 456 String userInfo: "", |
391 String host: "", | 457 String host: "", |
392 port: 0, | 458 port: 0, |
393 String path, | 459 String path, |
394 Iterable<String> pathSegments, | 460 Iterable<String> pathSegments, |
395 String query, | 461 String query, |
396 Map<String, String> queryParameters, | 462 Map<String, String> queryParameters, |
397 fragment: ""}) : | 463 fragment: ""}) { |
398 scheme = _makeScheme(scheme), | 464 scheme = _makeScheme(scheme, _stringOrNullLength(scheme)); |
399 _host = _makeHost(host), | 465 userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
400 query = _makeQuery(query, queryParameters), | 466 host = _makeHost(host, 0, _stringOrNullLength(host), false); |
401 fragment = _makeFragment(fragment) { | 467 query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
402 // Perform scheme specific normalization. | 468 fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
403 if (scheme == "http" && port == 80) { | 469 port = _makePort(port, scheme); |
404 _port = 0; | 470 bool ensureLeadingSlash = (host != "" || scheme == "file"); |
405 } else if (scheme == "https" && port == 443) { | 471 path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
406 _port = 0; | 472 ensureLeadingSlash); |
407 } else { | 473 |
408 _port = port; | 474 return new Uri._internal(scheme, userInfo, host, port, |
409 } | 475 path, query, fragment); |
410 // Fill the path. | |
411 _path = _makePath(path, pathSegments); | |
412 } | 476 } |
413 | 477 |
414 /** | 478 /** |
415 * Creates a new `http` URI from authority, path and query. | 479 * Creates a new `http` URI from authority, path and query. |
416 * | 480 * |
417 * Examples: | 481 * Examples: |
418 * | 482 * |
419 * ``` | 483 * ``` |
420 * // http://example.org/path?q=dart. | 484 * // http://example.org/path?q=dart. |
421 * new Uri.http("google.com", "/search", { "q" : "dart" }); | 485 * new Uri.http("google.com", "/search", { "q" : "dart" }); |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
482 var hostEnd = hostStart; | 546 var hostEnd = hostStart; |
483 if (hostStart < authority.length && | 547 if (hostStart < authority.length && |
484 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { | 548 authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { |
485 // IPv6 host. | 549 // IPv6 host. |
486 for (; hostEnd < authority.length; hostEnd++) { | 550 for (; hostEnd < authority.length; hostEnd++) { |
487 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; | 551 if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; |
488 } | 552 } |
489 if (hostEnd == authority.length) { | 553 if (hostEnd == authority.length) { |
490 throw new FormatException("Invalid IPv6 host entry."); | 554 throw new FormatException("Invalid IPv6 host entry."); |
491 } | 555 } |
492 parseIPv6Address(authority.substring(hostStart + 1, hostEnd)); | 556 parseIPv6Address(authority, hostStart + 1, hostEnd); |
493 hostEnd++; // Skip the closing bracket. | 557 hostEnd++; // Skip the closing bracket. |
494 if (hostEnd != authority.length && | 558 if (hostEnd != authority.length && |
495 authority.codeUnitAt(hostEnd) != _COLON) { | 559 authority.codeUnitAt(hostEnd) != _COLON) { |
496 throw new FormatException("Invalid end of authority"); | 560 throw new FormatException("Invalid end of authority"); |
497 } | 561 } |
498 } | 562 } |
499 // Split host and port. | 563 // Split host and port. |
500 bool hasPort = false; | 564 bool hasPort = false; |
501 for (; hostEnd < authority.length; hostEnd++) { | 565 for (; hostEnd < authority.length; hostEnd++) { |
502 if (authority.codeUnitAt(hostEnd) == _COLON) { | 566 if (authority.codeUnitAt(hostEnd) == _COLON) { |
(...skipping 255 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
758 * The returned map is unmodifiable and will throw [UnsupportedError] on any | 822 * The returned map is unmodifiable and will throw [UnsupportedError] on any |
759 * calls that would mutate it. | 823 * calls that would mutate it. |
760 */ | 824 */ |
761 Map<String, String> get queryParameters { | 825 Map<String, String> get queryParameters { |
762 if (_queryParameters == null) { | 826 if (_queryParameters == null) { |
763 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); | 827 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); |
764 } | 828 } |
765 return _queryParameters; | 829 return _queryParameters; |
766 } | 830 } |
767 | 831 |
768 static String _makeHost(String host) { | 832 static int _makePort(int port, String scheme) { |
769 if (host == null || host.isEmpty) return host; | 833 // Perform scheme specific normalization. |
770 if (host.codeUnitAt(0) == _LEFT_BRACKET) { | 834 if (port == 80 && scheme == "http") { |
771 if (host.codeUnitAt(host.length - 1) != _RIGHT_BRACKET) { | 835 return 0; |
772 throw new FormatException('Missing end `]` to match `[` in host'); | |
773 } | |
774 parseIPv6Address(host.substring(1, host.length - 1)); | |
775 return host; | |
776 } | 836 } |
777 for (int i = 0; i < host.length; i++) { | 837 if (port == 443 && scheme == "https") { |
778 if (host.codeUnitAt(i) == _COLON) { | 838 return 0; |
779 parseIPv6Address(host); | |
780 return '[$host]'; | |
781 } | |
782 } | 839 } |
783 return host; | 840 return port; |
784 } | 841 } |
785 | 842 |
786 static String _makeScheme(String scheme) { | 843 /** |
787 bool isSchemeLowerCharacter(int ch) { | 844 * Check and normalize a most name. |
788 return ch < 128 && | 845 * |
789 ((_schemeLowerTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 846 * If the host name starts and ends with '[' and ']', it is considered an |
| 847 * IPv6 address. If [strictIPv6] is false, the address is also considered |
| 848 * an IPv6 address if it contains any ':' character. |
| 849 * |
| 850 * If it is not an IPv6 address, it is case- and escape-normalized. |
| 851 * This escapes all characters not valid in a reg-name, |
| 852 * and converts all non-escape upper-case letters to lower-case. |
| 853 */ |
| 854 static String _makeHost(String host, int start, int end, bool strictIPv6) { |
| 855 // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? |
| 856 |
| 857 if (host == null) return null; |
| 858 if (start == end) return ""; |
| 859 // Host is an IPv6 address if it starts with '[' or contains a colon. |
| 860 if (host.codeUnitAt(start) == _LEFT_BRACKET) { |
| 861 if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { |
| 862 _fail(host, start, 'Missing end `]` to match `[` in host'); |
| 863 } |
| 864 parseIPv6Address(host, start + 1, end - 1); |
| 865 // RFC 5952 requires hex digits to be lower case. |
| 866 return host.substring(start, end).toLowerCase(); |
790 } | 867 } |
791 | 868 if (!strictIPv6) { |
792 if (scheme == null) return ""; | 869 // TODO(lrn): skip if too short to be a valid IPv6 address? |
793 bool allLowercase = true; | 870 for (int i = start; i < end; i++) { |
794 int length = scheme.length; | 871 if (host.codeUnitAt(i) == _COLON) { |
795 for (int i = 0; i < length; i++) { | 872 parseIPv6Address(host, start, end); |
796 int codeUnit = scheme.codeUnitAt(i); | 873 return '[$host]'; |
797 if (i == 0 && !_isAlphabeticCharacter(codeUnit)) { | |
798 // First code unit must be an alphabetic character. | |
799 throw new ArgumentError('Illegal scheme: $scheme'); | |
800 } | |
801 if (!isSchemeLowerCharacter(codeUnit)) { | |
802 if (_isSchemeCharacter(codeUnit)) { | |
803 allLowercase = false; | |
804 } else { | |
805 throw new ArgumentError('Illegal scheme: $scheme'); | |
806 } | 874 } |
807 } | 875 } |
808 } | 876 } |
809 | 877 return _normalizeRegName(host, start, end); |
810 return allLowercase ? scheme : scheme.toLowerCase(); | |
811 } | 878 } |
812 | 879 |
813 String _makePath(String path, Iterable<String> pathSegments) { | 880 static bool _isRegNameChar(int char) { |
| 881 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; |
| 882 } |
| 883 |
| 884 /** |
| 885 * Validates and does case- and percent-encoding normalization. |
| 886 * |
| 887 * The [host] must be an RFC3986 "reg-name". It is converted |
| 888 * to lower case, and percent escapes are converted to either |
| 889 * lower case unreserved characters or upper case escapes. |
| 890 */ |
| 891 static String _normalizeRegName(String host, int start, int end) { |
| 892 StringBuffer buffer; |
| 893 int sectionStart = start; |
| 894 int index = start; |
| 895 // Whether all characters between sectionStart and index are normalized, |
| 896 bool isNormalized = true; |
| 897 |
| 898 while (index < end) { |
| 899 int char = host.codeUnitAt(index); |
| 900 if (char == _PERCENT) { |
| 901 // The _regNameTable contains "%", so we check that first. |
| 902 String replacement = _normalizeEscape(host, index, true); |
| 903 if (replacement == null && isNormalized) { |
| 904 index += 3; |
| 905 continue; |
| 906 } |
| 907 if (buffer == null) buffer = new StringBuffer(); |
| 908 String slice = host.substring(sectionStart, index); |
| 909 if (!isNormalized) slice = slice.toLowerCase(); |
| 910 buffer.write(slice); |
| 911 int sourceLength = 3; |
| 912 if (replacement == null) { |
| 913 replacement = host.substring(index, index + 3); |
| 914 } else if (replacement == "%") { |
| 915 replacement = "%25"; |
| 916 sourceLength = 1; |
| 917 } |
| 918 buffer.write(replacement); |
| 919 index += sourceLength; |
| 920 sectionStart = index; |
| 921 isNormalized = true; |
| 922 } else if (_isRegNameChar(char)) { |
| 923 if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) { |
| 924 // Put initial slice in buffer and continue in non-normalized mode |
| 925 if (buffer == null) buffer = new StringBuffer(); |
| 926 if (sectionStart < index) { |
| 927 buffer.write(host.substring(sectionStart, index)); |
| 928 sectionStart = index; |
| 929 } |
| 930 isNormalized = false; |
| 931 } |
| 932 index++; |
| 933 } else if (_isGeneralDelimiter(char)) { |
| 934 _fail(host, index, "Invalid character"); |
| 935 } else { |
| 936 int sourceLength = 1; |
| 937 if ((char & 0xFC00) == 0xD800 && (index + 1) < end) { |
| 938 int tail = host.codeUnitAt(index + 1); |
| 939 if ((tail & 0xFC00) == 0xDC00) { |
| 940 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); |
| 941 sourceLength = 2; |
| 942 } |
| 943 } |
| 944 if (buffer == null) buffer = new StringBuffer(); |
| 945 String slice = host.substring(sectionStart, index); |
| 946 if (!isNormalized) slice = slice.toLowerCase(); |
| 947 buffer.write(slice); |
| 948 buffer.write(_escapeChar(char)); |
| 949 index += sourceLength; |
| 950 sectionStart = index; |
| 951 } |
| 952 } |
| 953 if (buffer == null) return host.substring(start, end); |
| 954 if (sectionStart < end) { |
| 955 String slice = host.substring(sectionStart, end); |
| 956 if (!isNormalized) slice = slice.toLowerCase(); |
| 957 buffer.write(slice); |
| 958 } |
| 959 return buffer.toString(); |
| 960 } |
| 961 |
| 962 /** |
| 963 * Validates scheme characters and does case-normalization. |
| 964 * |
| 965 * Schemes are converted to lower case. They cannot contain escapes. |
| 966 */ |
| 967 static String _makeScheme(String scheme, int end) { |
| 968 if (end == 0) return ""; |
| 969 int char = scheme.codeUnitAt(0); |
| 970 if (!_isAlphabeticCharacter(char)) { |
| 971 _fail(scheme, 0, "Scheme not starting with alphabetic character"); |
| 972 } |
| 973 bool allLowercase = char >= _LOWER_CASE_A; |
| 974 for (int i = 0; i < end; i++) { |
| 975 int codeUnit = scheme.codeUnitAt(i); |
| 976 if (!_isSchemeCharacter(codeUnit)) { |
| 977 _fail(scheme, i, "Illegal scheme character"); |
| 978 } |
| 979 if (_LOWER_CASE_A <= char && _LOWER_CASE_Z >= char) { |
| 980 allLowercase = false; |
| 981 } |
| 982 } |
| 983 scheme = scheme.substring(0, end); |
| 984 if (!allLowercase) scheme = scheme.toLowerCase(); |
| 985 return scheme; |
| 986 } |
| 987 |
| 988 static String _makeUserInfo(String userInfo, int start, int end) { |
| 989 if (userInfo == null) return "null"; |
| 990 return _normalize(userInfo, start, end, _userinfoTable); |
| 991 } |
| 992 |
| 993 static String _makePath(String path, int start, int end, |
| 994 Iterable<String> pathSegments, |
| 995 bool ensureLeadingSlash) { |
814 if (path == null && pathSegments == null) return ""; | 996 if (path == null && pathSegments == null) return ""; |
815 if (path != null && pathSegments != null) { | 997 if (path != null && pathSegments != null) { |
816 throw new ArgumentError('Both path and pathSegments specified'); | 998 throw new ArgumentError('Both path and pathSegments specified'); |
817 } | 999 } |
818 var result; | 1000 var result; |
819 if (path != null) { | 1001 if (path != null) { |
820 result = _normalize(path); | 1002 result = _normalize(path, start, end, _pathCharOrSlashTable); |
821 } else { | 1003 } else { |
822 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); | 1004 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); |
823 } | 1005 } |
824 if ((hasAuthority || (scheme == "file")) && | 1006 if (ensureLeadingSlash && result.isNotEmpty && !result.startsWith("/")) { |
825 result.isNotEmpty && !result.startsWith("/")) { | |
826 return "/$result"; | 1007 return "/$result"; |
827 } | 1008 } |
828 return result; | 1009 return result; |
829 } | 1010 } |
830 | 1011 |
831 static String _makeQuery(String query, Map<String, String> queryParameters) { | 1012 static String _makeQuery(String query, int start, int end, |
| 1013 Map<String, String> queryParameters) { |
832 if (query == null && queryParameters == null) return ""; | 1014 if (query == null && queryParameters == null) return ""; |
833 if (query != null && queryParameters != null) { | 1015 if (query != null && queryParameters != null) { |
834 throw new ArgumentError('Both query and queryParameters specified'); | 1016 throw new ArgumentError('Both query and queryParameters specified'); |
835 } | 1017 } |
836 if (query != null) return _normalize(query); | 1018 if (query != null) return _normalize(query, start, end, _queryCharTable); |
837 | 1019 |
838 var result = new StringBuffer(); | 1020 var result = new StringBuffer(); |
839 var first = true; | 1021 var first = true; |
840 queryParameters.forEach((key, value) { | 1022 queryParameters.forEach((key, value) { |
841 if (!first) { | 1023 if (!first) { |
842 result.write("&"); | 1024 result.write("&"); |
843 } | 1025 } |
844 first = false; | 1026 first = false; |
845 result.write(Uri.encodeQueryComponent(key)); | 1027 result.write(Uri.encodeQueryComponent(key)); |
846 if (value != null && !value.isEmpty) { | 1028 if (value != null && !value.isEmpty) { |
847 result.write("="); | 1029 result.write("="); |
848 result.write(Uri.encodeQueryComponent(value)); | 1030 result.write(Uri.encodeQueryComponent(value)); |
849 } | 1031 } |
850 }); | 1032 }); |
851 return result.toString(); | 1033 return result.toString(); |
852 } | 1034 } |
853 | 1035 |
854 static String _makeFragment(String fragment) { | 1036 static String _makeFragment(String fragment, int start, int end) { |
855 if (fragment == null) return ""; | 1037 if (fragment == null) return ""; |
856 return _normalize(fragment); | 1038 return _normalize(fragment, start, end, _queryCharTable); |
857 } | 1039 } |
858 | 1040 |
859 static String _normalize(String component) { | 1041 static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
860 int index = component.indexOf('%'); | |
861 if (index < 0) return component; | |
862 | 1042 |
863 bool isNormalizedHexDigit(int digit) { | 1043 static bool _isHexDigit(int char) { |
864 return (_ZERO <= digit && digit <= _NINE) || | 1044 if (_NINE >= char) return _ZERO <= char; |
865 (_UPPER_CASE_A <= digit && digit <= _UPPER_CASE_F); | 1045 char |= 0x20; |
| 1046 return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char; |
| 1047 } |
| 1048 |
| 1049 static int _hexValue(int char) { |
| 1050 assert(_isHexDigit(char)); |
| 1051 if (_NINE >= char) return char - _ZERO; |
| 1052 char |= 0x20; |
| 1053 return char - (_LOWER_CASE_A - 10); |
| 1054 } |
| 1055 |
| 1056 /** |
| 1057 * Performs RFC 3986 Percent-Encoding Normalization. |
| 1058 * |
| 1059 * Returns a replacement string that should be replace the original escape. |
| 1060 * Returns null if no replacement is necessary because the escape is |
| 1061 * not for an unreserved character and is already non-lower-case. |
| 1062 * |
| 1063 * Returns "%" if the escape is invalid (not two valid hex digits following |
| 1064 * the percent sign). The calling code should replace the percent |
| 1065 * sign with "%25", but leave the following two characters unmodified. |
| 1066 * |
| 1067 * If [lowerCase] is true, a single character returned is always lower case, |
| 1068 */ |
| 1069 static String _normalizeEscape(String source, int index, bool lowerCase) { |
| 1070 assert(source.codeUnitAt(index) == _PERCENT); |
| 1071 if (index + 2 >= source.length) { |
| 1072 return "%"; // Marks the escape as invalid. |
866 } | 1073 } |
| 1074 int firstDigit = source.codeUnitAt(index + 1); |
| 1075 int secondDigit = source.codeUnitAt(index + 2); |
| 1076 if (!_isHexDigit(firstDigit) || !_isHexDigit(secondDigit)) { |
| 1077 return "%"; // Marks the escape as invalid. |
| 1078 } |
| 1079 int value = _hexValue(firstDigit) * 16 + _hexValue(secondDigit); |
| 1080 if (_isUnreservedChar(value)) { |
| 1081 if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { |
| 1082 value |= 0x20; |
| 1083 } |
| 1084 return new String.fromCharCode(value); |
| 1085 } |
| 1086 if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { |
| 1087 // Either digit is lower case. |
| 1088 return source.substring(index, index + 3).toUpperCase(); |
| 1089 } |
| 1090 // Escape is retained, and is already non-lower case, so return null to |
| 1091 // represent "no replacement necessary". |
| 1092 return null; |
| 1093 } |
867 | 1094 |
868 bool isLowerCaseHexDigit(int digit) { | 1095 static bool _isUnreservedChar(int ch) { |
869 return _LOWER_CASE_A <= digit && digit <= _LOWER_CASE_F; | 1096 return ch < 127 && |
870 } | 1097 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
| 1098 } |
871 | 1099 |
872 bool isUnreserved(int ch) { | 1100 static String _escapeChar(char) { |
873 return ch < 128 && | 1101 assert(char <= 0x10ffff); // It's a valid unicode code point. |
874 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 1102 const hexDigits = "0123456789ABCDEF"; |
875 } | 1103 List codeUnits; |
876 | 1104 if (char < 0x80) { |
877 int normalizeHexDigit(int index) { | 1105 // ASCII, a single percent encoded sequence. |
878 var codeUnit = component.codeUnitAt(index); | 1106 codeUnits = new List(3); |
879 if (isLowerCaseHexDigit(codeUnit)) { | 1107 codeUnits[0] = _PERCENT; |
880 return codeUnit - 0x20; | 1108 codeUnits[1] = hexDigits.codeUnitAt(char >> 4); |
881 } else if (!isNormalizedHexDigit(codeUnit)) { | 1109 codeUnits[2] = hexDigits.codeUnitAt(char & 0xf); |
882 throw new ArgumentError("Invalid URI component: $component"); | 1110 } else { |
883 } else { | 1111 // Do UTF-8 encoding of character, then percent encode bytes. |
884 return codeUnit; | 1112 int flag = 0xc0; // The high-bit markers on the first byte of UTF-8. |
| 1113 int encodedBytes = 2; |
| 1114 if (char > 0x7ff) { |
| 1115 flag = 0xe0; |
| 1116 encodedBytes = 3; |
| 1117 if (char > 0xffff) { |
| 1118 encodedBytes = 4; |
| 1119 flag = 0xf0; |
| 1120 } |
| 1121 } |
| 1122 codeUnits = new List(3 * encodedBytes); |
| 1123 int index = 0; |
| 1124 while (--encodedBytes >= 0) { |
| 1125 int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag; |
| 1126 codeUnits[index] = _PERCENT; |
| 1127 codeUnits[index + 1] = hexDigits.codeUnitAt(byte >> 4); |
| 1128 codeUnits[index + 2] = hexDigits.codeUnitAt(byte & 0xf); |
| 1129 index += 3; |
| 1130 flag = 0x80; // Following bytes have only high bit set. |
885 } | 1131 } |
886 } | 1132 } |
| 1133 return new String.fromCharCodes(codeUnits); |
| 1134 } |
887 | 1135 |
888 int decodeHexDigitPair(int index) { | 1136 /** |
889 int byte = 0; | 1137 * Runs through component checking that each character is valid and |
890 for (int i = 0; i < 2; i++) { | 1138 * normalize percent escapes. |
891 var codeUnit = component.codeUnitAt(index + i); | 1139 * |
892 if (_ZERO <= codeUnit && codeUnit <= _NINE) { | 1140 * Uses [charTable] to check if a non-`%` character is allowed. |
893 byte = byte * 16 + codeUnit - _ZERO; | 1141 * Each `%` character must be followed by two hex digits. |
| 1142 * If the hex-digits are lower case letters, they are converted to |
| 1143 * upper case. |
| 1144 */ |
| 1145 static String _normalize(String component, int start, int end, |
| 1146 List<int> charTable) { |
| 1147 StringBuffer buffer; |
| 1148 int sectionStart = start; |
| 1149 int index = start; |
| 1150 // Loop while characters are valid and escapes correct and upper-case. |
| 1151 while (index < end) { |
| 1152 int char = component.codeUnitAt(index); |
| 1153 if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) { |
| 1154 index++; |
| 1155 } else { |
| 1156 String replacement; |
| 1157 int sourceLength; |
| 1158 if (char == _PERCENT) { |
| 1159 replacement = _normalizeEscape(component, index, false); |
| 1160 // Returns null if we should keep the existing escape. |
| 1161 if (replacement == null) { |
| 1162 index += 3; |
| 1163 continue; |
| 1164 } |
| 1165 // Returns "%" if we should escape the existing percent. |
| 1166 if ("%" == replacement) { |
| 1167 replacement = "%25"; |
| 1168 sourceLength = 1; |
| 1169 } else { |
| 1170 sourceLength = 3; |
| 1171 } |
| 1172 } else if (_isGeneralDelimiter(char)) { |
| 1173 _fail(component, index, "Invalid character"); |
894 } else { | 1174 } else { |
895 // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). | 1175 sourceLength = 1; |
896 codeUnit |= 0x20; | 1176 if ((char & 0xFC00) == 0xD800) { |
897 if (_LOWER_CASE_A <= codeUnit && | 1177 // Possible lead surrogate. |
898 codeUnit <= _LOWER_CASE_F) { | 1178 if (index + 1 < end) { |
899 byte = byte * 16 + codeUnit - _LOWER_CASE_A + 10; | 1179 int tail = component.codeUnitAt(index + 1); |
900 } else { | 1180 if ((tail & 0xFC00) == 0xDC00) { |
901 throw new ArgumentError( | 1181 // Tail surrogat. |
902 "Invalid percent-encoding in URI component: $component"); | 1182 sourceLength = 2; |
| 1183 char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff); |
| 1184 } |
| 1185 } |
903 } | 1186 } |
| 1187 replacement = _escapeChar(char); |
904 } | 1188 } |
905 } | 1189 if (buffer == null) buffer = new StringBuffer(); |
906 return byte; | 1190 buffer.write(component.substring(sectionStart, index)); |
907 } | 1191 buffer.write(replacement); |
908 | 1192 index += sourceLength; |
909 // Start building the normalized component string. | 1193 sectionStart = index; |
910 StringBuffer result; | |
911 int length = component.length; | |
912 int prevIndex = 0; | |
913 | |
914 // Copy a part of the component string to the result. | |
915 void fillResult() { | |
916 if (result == null) { | |
917 assert(prevIndex == 0); | |
918 result = new StringBuffer(component.substring(prevIndex, index)); | |
919 } else { | |
920 result.write(component.substring(prevIndex, index)); | |
921 } | 1194 } |
922 } | 1195 } |
923 | 1196 if (buffer == null) { |
924 while (index < length) { | 1197 // Makes no copy if start == 0 and end == component.length. |
925 // Normalize percent-encoding to uppercase and don't encode | 1198 return component.substring(start, end); |
926 // unreserved characters. | |
927 assert(component.codeUnitAt(index) == _PERCENT); | |
928 if (length < index + 2) { | |
929 throw new ArgumentError( | |
930 "Invalid percent-encoding in URI component: $component"); | |
931 } | |
932 | |
933 var codeUnit1 = component.codeUnitAt(index + 1); | |
934 var codeUnit2 = component.codeUnitAt(index + 2); | |
935 var decodedCodeUnit = decodeHexDigitPair(index + 1); | |
936 if (isNormalizedHexDigit(codeUnit1) && | |
937 isNormalizedHexDigit(codeUnit2) && | |
938 !isUnreserved(decodedCodeUnit)) { | |
939 index += 3; | |
940 } else { | |
941 fillResult(); | |
942 if (isUnreserved(decodedCodeUnit)) { | |
943 result.writeCharCode(decodedCodeUnit); | |
944 } else { | |
945 result.write("%"); | |
946 result.writeCharCode(normalizeHexDigit(index + 1)); | |
947 result.writeCharCode(normalizeHexDigit(index + 2)); | |
948 } | |
949 index += 3; | |
950 prevIndex = index; | |
951 } | |
952 int next = component.indexOf('%', index); | |
953 if (next >= index) { | |
954 index = next; | |
955 } else { | |
956 index = length; | |
957 } | |
958 } | 1199 } |
959 if (result == null) return component; | 1200 if (sectionStart < end) { |
960 | 1201 buffer.write(component.substring(sectionStart, end)); |
961 if (result != null && prevIndex != index) fillResult(); | 1202 } |
962 assert(index == length); | 1203 return buffer.toString(); |
963 | |
964 return result.toString(); | |
965 } | 1204 } |
966 | 1205 |
967 static bool _isSchemeCharacter(int ch) { | 1206 static bool _isSchemeCharacter(int ch) { |
968 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 1207 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
969 } | 1208 } |
970 | 1209 |
| 1210 static bool _isGeneralDelimiter(int ch) { |
| 1211 return ch <= 64 && |
| 1212 ((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
| 1213 } |
971 | 1214 |
972 /** | 1215 /** |
973 * Returns whether the URI is absolute. | 1216 * Returns whether the URI is absolute. |
974 */ | 1217 */ |
975 bool get isAbsolute => scheme != "" && fragment == ""; | 1218 bool get isAbsolute => scheme != "" && fragment == ""; |
976 | 1219 |
977 String _merge(String base, String reference) { | 1220 String _merge(String base, String reference) { |
978 if (base == "") return "/$reference"; | 1221 if (base == "") return "/$reference"; |
979 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference"; | 1222 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference"; |
980 } | 1223 } |
(...skipping 477 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1458 .toList(); | 1701 .toList(); |
1459 } | 1702 } |
1460 | 1703 |
1461 /** | 1704 /** |
1462 * Parse the [host] as an IP version 6 (IPv6) address, returning the address | 1705 * Parse the [host] as an IP version 6 (IPv6) address, returning the address |
1463 * as a list of 16 bytes in network byte order (big endian). | 1706 * as a list of 16 bytes in network byte order (big endian). |
1464 * | 1707 * |
1465 * Throws a [FormatException] if [host] is not a valid IPv6 address | 1708 * Throws a [FormatException] if [host] is not a valid IPv6 address |
1466 * representation. | 1709 * representation. |
1467 * | 1710 * |
| 1711 * Acts on the substring from [start] to [end]. If [end] is omitted, it |
| 1712 * defaults ot the end of the string. |
| 1713 * |
1468 * Some examples of IPv6 addresses: | 1714 * Some examples of IPv6 addresses: |
1469 * * ::1 | 1715 * * ::1 |
1470 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 | 1716 * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 |
1471 * * 3ffe:2a00:100:7031::1 | 1717 * * 3ffe:2a00:100:7031::1 |
1472 * * ::FFFF:129.144.52.38 | 1718 * * ::FFFF:129.144.52.38 |
1473 * * 2010:836B:4179::836B:4179 | 1719 * * 2010:836B:4179::836B:4179 |
1474 */ | 1720 */ |
1475 static List<int> parseIPv6Address(String host) { | 1721 static List<int> parseIPv6Address(String host, [int start = 0, int end]) { |
| 1722 if (end == null) end = host.length; |
1476 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated | 1723 // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated |
1477 // by `:`'s, with the following exceptions: | 1724 // by `:`'s, with the following exceptions: |
1478 // | 1725 // |
1479 // - One (and only one) wildcard (`::`) may be present, representing a fill | 1726 // - One (and only one) wildcard (`::`) may be present, representing a fill |
1480 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. | 1727 // of 0's. The IPv6 `::` is thus 16 bytes of `0`. |
1481 // - The last two parts may be replaced by an IPv4 address. | 1728 // - The last two parts may be replaced by an IPv4 address. |
1482 void error(String msg) { | 1729 void error(String msg) { |
1483 throw new FormatException('Illegal IPv6 address, $msg'); | 1730 throw new FormatException('Illegal IPv6 address, $msg'); |
1484 } | 1731 } |
1485 int parseHex(int start, int end) { | 1732 int parseHex(int start, int end) { |
1486 if (end - start > 4) { | 1733 if (end - start > 4) { |
1487 error('an IPv6 part can only contain a maximum of 4 hex digits'); | 1734 error('an IPv6 part can only contain a maximum of 4 hex digits'); |
1488 } | 1735 } |
1489 int value = int.parse(host.substring(start, end), radix: 16); | 1736 int value = int.parse(host.substring(start, end), radix: 16); |
1490 if (value < 0 || value > (1 << 16) - 1) { | 1737 if (value < 0 || value > (1 << 16) - 1) { |
1491 error('each part must be in the range of `0x0..0xFFFF`'); | 1738 error('each part must be in the range of `0x0..0xFFFF`'); |
1492 } | 1739 } |
1493 return value; | 1740 return value; |
1494 } | 1741 } |
1495 if (host.length < 2) error('address is too short'); | 1742 if (host.length < 2) error('address is too short'); |
1496 List<int> parts = []; | 1743 List<int> parts = []; |
1497 bool wildcardSeen = false; | 1744 bool wildcardSeen = false; |
1498 int partStart = 0; | 1745 int partStart = start; |
1499 // Parse all parts, except a potential last one. | 1746 // Parse all parts, except a potential last one. |
1500 for (int i = 0; i < host.length; i++) { | 1747 for (int i = start; i < end; i++) { |
1501 if (host.codeUnitAt(i) == _COLON) { | 1748 if (host.codeUnitAt(i) == _COLON) { |
1502 if (i == 0) { | 1749 if (i == start) { |
1503 // If we see a `:` in the beginning, expect wildcard. | 1750 // If we see a `:` in the beginning, expect wildcard. |
1504 i++; | 1751 i++; |
1505 if (host.codeUnitAt(i) != _COLON) { | 1752 if (host.codeUnitAt(i) != _COLON) { |
1506 error('invalid start colon.'); | 1753 error('invalid start colon.'); |
1507 } | 1754 } |
1508 partStart = i; | 1755 partStart = i; |
1509 } | 1756 } |
1510 if (i == partStart) { | 1757 if (i == partStart) { |
1511 // Wildcard. We only allow one. | 1758 // Wildcard. We only allow one. |
1512 if (wildcardSeen) { | 1759 if (wildcardSeen) { |
1513 error('only one wildcard `::` is allowed'); | 1760 error('only one wildcard `::` is allowed'); |
1514 } | 1761 } |
1515 wildcardSeen = true; | 1762 wildcardSeen = true; |
1516 parts.add(-1); | 1763 parts.add(-1); |
1517 } else { | 1764 } else { |
1518 // Found a single colon. Parse [partStart..i] as a hex entry. | 1765 // Found a single colon. Parse [partStart..i] as a hex entry. |
1519 parts.add(parseHex(partStart, i)); | 1766 parts.add(parseHex(partStart, i)); |
1520 } | 1767 } |
1521 partStart = i + 1; | 1768 partStart = i + 1; |
1522 } | 1769 } |
1523 } | 1770 } |
1524 if (parts.length == 0) error('too few parts'); | 1771 if (parts.length == 0) error('too few parts'); |
1525 bool atEnd = partStart == host.length; | 1772 bool atEnd = (partStart == end); |
1526 bool isLastWildcard = parts.last == -1; | 1773 bool isLastWildcard = (parts.last == -1); |
1527 if (atEnd && !isLastWildcard) { | 1774 if (atEnd && !isLastWildcard) { |
1528 error('expected a part after last `:`'); | 1775 error('expected a part after last `:`'); |
1529 } | 1776 } |
1530 if (!atEnd) { | 1777 if (!atEnd) { |
1531 try { | 1778 try { |
1532 parts.add(parseHex(partStart, host.length)); | 1779 parts.add(parseHex(partStart, end)); |
1533 } catch (e) { | 1780 } catch (e) { |
1534 // Failed to parse the last chunk as hex. Try IPv4. | 1781 // Failed to parse the last chunk as hex. Try IPv4. |
1535 try { | 1782 try { |
1536 List<int> last = parseIPv4Address(host.substring(partStart)); | 1783 List<int> last = parseIPv4Address(host.substring(partStart, end)); |
1537 parts.add(last[0] << 8 | last[1]); | 1784 parts.add(last[0] << 8 | last[1]); |
1538 parts.add(last[2] << 8 | last[3]); | 1785 parts.add(last[2] << 8 | last[3]); |
1539 } catch (e) { | 1786 } catch (e) { |
1540 error('invalid end of IPv6 address.'); | 1787 error('invalid end of IPv6 address.'); |
1541 } | 1788 } |
1542 } | 1789 } |
1543 } | 1790 } |
1544 if (wildcardSeen) { | 1791 if (wildcardSeen) { |
1545 if (parts.length > 7) { | 1792 if (parts.length > 7) { |
1546 error('an address with a wildcard must have less than 7 parts'); | 1793 error('an address with a wildcard must have less than 7 parts'); |
1547 } | 1794 } |
1548 } else if (parts.length != 8) { | 1795 } else if (parts.length != 8) { |
1549 error('an address without a wildcard must contain exactly 8 parts'); | 1796 error('an address without a wildcard must contain exactly 8 parts'); |
1550 } | 1797 } |
1551 // TODO(ajohnsen): Consider using Uint8List. | 1798 // TODO(ajohnsen): Consider using Uint8List. |
1552 return parts | 1799 List bytes = new List<int>(16); |
1553 .expand((value) { | 1800 for (int i = 0, index = 0; i < parts.length; i++) { |
1554 if (value == -1) { | 1801 int value = parts[i]; |
1555 return new List.filled((9 - parts.length) * 2, 0); | 1802 if (value == -1) { |
1556 } else { | 1803 int wildCardLength = 9 - parts.length; |
1557 return [(value >> 8) & 0xFF, value & 0xFF]; | 1804 for (int j = 0; j < wildCardLength; j++) { |
1558 } | 1805 bytes[index] = 0; |
1559 }) | 1806 bytes[index + 1] = 0; |
1560 .toList(); | 1807 index += 2; |
| 1808 } |
| 1809 } else { |
| 1810 bytes[index] = value >> 8; |
| 1811 bytes[index + 1] = value & 0xff; |
| 1812 index += 2; |
| 1813 } |
| 1814 } |
| 1815 return bytes; |
1561 } | 1816 } |
1562 | 1817 |
1563 // Frequently used character codes. | 1818 // Frequently used character codes. |
1564 static const int _SPACE = 0x20; | 1819 static const int _SPACE = 0x20; |
1565 static const int _DOUBLE_QUOTE = 0x22; | 1820 static const int _DOUBLE_QUOTE = 0x22; |
1566 static const int _NUMBER_SIGN = 0x23; | 1821 static const int _NUMBER_SIGN = 0x23; |
1567 static const int _PERCENT = 0x25; | 1822 static const int _PERCENT = 0x25; |
1568 static const int _ASTERISK = 0x2A; | 1823 static const int _ASTERISK = 0x2A; |
1569 static const int _PLUS = 0x2B; | 1824 static const int _PLUS = 0x2B; |
1570 static const int _SLASH = 0x2F; | 1825 static const int _SLASH = 0x2F; |
(...skipping 242 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1813 0x2bff, // 0x30 - 0x3f 1111111111010100 | 2068 0x2bff, // 0x30 - 0x3f 1111111111010100 |
1814 // ABCDEFGHIJKLMNO | 2069 // ABCDEFGHIJKLMNO |
1815 0xfffe, // 0x40 - 0x4f 0111111111111111 | 2070 0xfffe, // 0x40 - 0x4f 0111111111111111 |
1816 // PQRSTUVWXYZ _ | 2071 // PQRSTUVWXYZ _ |
1817 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2072 0x87ff, // 0x50 - 0x5f 1111111111100001 |
1818 // abcdefghijklmno | 2073 // abcdefghijklmno |
1819 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2074 0xfffe, // 0x60 - 0x6f 0111111111111111 |
1820 // pqrstuvwxyz ~ | 2075 // pqrstuvwxyz ~ |
1821 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2076 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
1822 | 2077 |
| 2078 // General delimiter characters, RFC 3986 section 2.2. |
| 2079 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" |
| 2080 // |
| 2081 static const _genDelimitersTable = const [ |
| 2082 // LSB MSB |
| 2083 // | | |
| 2084 0x0000, // 0x00 - 0x0f 0000000000000000 |
| 2085 0x0000, // 0x10 - 0x1f 0000000000000000 |
| 2086 // # / |
| 2087 0x8008, // 0x20 - 0x2f 0001000000000001 |
| 2088 // : [ ]? |
| 2089 0xe400, // 0x30 - 0x3f 0000000000101011 |
| 2090 // @ |
| 2091 0x0001, // 0x40 - 0x4f 1000000000000000 |
| 2092 // |
| 2093 0x0000, // 0x50 - 0x5f 0000000000000000 |
| 2094 // |
| 2095 0x0000, // 0x60 - 0x6f 0000000000000000 |
| 2096 // |
| 2097 0x0000]; // 0x70 - 0x7f 0000000000000000 |
| 2098 |
| 2099 // Characters allowed in the userinfo as of RFC 3986. |
| 2100 // RFC 3986 Apendix A |
| 2101 // userinfo = *( unreserved / pct-encoded / sub-delims / ':') |
| 2102 static const _userinfoTable = const [ |
| 2103 // LSB MSB |
| 2104 // | | |
| 2105 0x0000, // 0x00 - 0x0f 0000000000000000 |
| 2106 0x0000, // 0x10 - 0x1f 0000000000000000 |
| 2107 // ! $ &'()*+,-. |
| 2108 0x7fd2, // 0x20 - 0x2f 0100101111111110 |
| 2109 // 0123456789:; = |
| 2110 0x2fff, // 0x30 - 0x3f 1111111111110100 |
| 2111 // ABCDEFGHIJKLMNO |
| 2112 0xfffe, // 0x40 - 0x4f 0111111111111111 |
| 2113 // PQRSTUVWXYZ _ |
| 2114 0x87ff, // 0x50 - 0x5f 1111111111100001 |
| 2115 // abcdefghijklmno |
| 2116 0xfffe, // 0x60 - 0x6f 0111111111111111 |
| 2117 // pqrstuvwxyz ~ |
| 2118 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
| 2119 |
1823 // Characters allowed in the reg-name as of RFC 3986. | 2120 // Characters allowed in the reg-name as of RFC 3986. |
1824 // RFC 3986 Apendix A | 2121 // RFC 3986 Apendix A |
1825 // reg-name = *( unreserved / pct-encoded / sub-delims ) | 2122 // reg-name = *( unreserved / pct-encoded / sub-delims ) |
1826 static const _regNameTable = const [ | 2123 static const _regNameTable = const [ |
1827 // LSB MSB | 2124 // LSB MSB |
1828 // | | | 2125 // | | |
1829 0x0000, // 0x00 - 0x0f 0000000000000000 | 2126 0x0000, // 0x00 - 0x0f 0000000000000000 |
1830 0x0000, // 0x10 - 0x1f 0000000000000000 | 2127 0x0000, // 0x10 - 0x1f 0000000000000000 |
1831 // ! $%&'()*+,-. | 2128 // ! $%&'()*+,-. |
1832 0x7ff2, // 0x20 - 0x2f 0100111111111110 | 2129 0x7ff2, // 0x20 - 0x2f 0100111111111110 |
(...skipping 22 matching lines...) Expand all Loading... |
1855 0x2fff, // 0x30 - 0x3f 1111111111110100 | 2152 0x2fff, // 0x30 - 0x3f 1111111111110100 |
1856 // @ABCDEFGHIJKLMNO | 2153 // @ABCDEFGHIJKLMNO |
1857 0xffff, // 0x40 - 0x4f 1111111111111111 | 2154 0xffff, // 0x40 - 0x4f 1111111111111111 |
1858 // PQRSTUVWXYZ _ | 2155 // PQRSTUVWXYZ _ |
1859 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2156 0x87ff, // 0x50 - 0x5f 1111111111100001 |
1860 // abcdefghijklmno | 2157 // abcdefghijklmno |
1861 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2158 0xfffe, // 0x60 - 0x6f 0111111111111111 |
1862 // pqrstuvwxyz ~ | 2159 // pqrstuvwxyz ~ |
1863 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2160 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
1864 | 2161 |
| 2162 // Characters allowed in the path as of RFC 3986. |
| 2163 // RFC 3986 section 3.3 *and* slash. |
| 2164 static const _pathCharOrSlashTable = const [ |
| 2165 // LSB MSB |
| 2166 // | | |
| 2167 0x0000, // 0x00 - 0x0f 0000000000000000 |
| 2168 0x0000, // 0x10 - 0x1f 0000000000000000 |
| 2169 // ! $ &'()*+,-./ |
| 2170 0xffd2, // 0x20 - 0x2f 0100101111111111 |
| 2171 // 0123456789:; = |
| 2172 0x2fff, // 0x30 - 0x3f 1111111111110100 |
| 2173 // @ABCDEFGHIJKLMNO |
| 2174 0xffff, // 0x40 - 0x4f 1111111111111111 |
| 2175 |
| 2176 // PQRSTUVWXYZ _ |
| 2177 0x87ff, // 0x50 - 0x5f 1111111111100001 |
| 2178 // abcdefghijklmno |
| 2179 0xfffe, // 0x60 - 0x6f 0111111111111111 |
| 2180 // pqrstuvwxyz ~ |
| 2181 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
| 2182 |
1865 // Characters allowed in the query as of RFC 3986. | 2183 // Characters allowed in the query as of RFC 3986. |
1866 // RFC 3986 section 3.4. | 2184 // RFC 3986 section 3.4. |
1867 // query = *( pchar / "/" / "?" ) | 2185 // query = *( pchar / "/" / "?" ) |
1868 static const _queryCharTable = const [ | 2186 static const _queryCharTable = const [ |
1869 // LSB MSB | 2187 // LSB MSB |
1870 // | | | 2188 // | | |
1871 0x0000, // 0x00 - 0x0f 0000000000000000 | 2189 0x0000, // 0x00 - 0x0f 0000000000000000 |
1872 0x0000, // 0x10 - 0x1f 0000000000000000 | 2190 0x0000, // 0x10 - 0x1f 0000000000000000 |
1873 // ! $ &'()*+,-./ | 2191 // ! $ &'()*+,-./ |
1874 0xffd2, // 0x20 - 0x2f 0100101111111111 | 2192 0xffd2, // 0x20 - 0x2f 0100101111111111 |
1875 // 0123456789:; = ? | 2193 // 0123456789:; = ? |
1876 0xafff, // 0x30 - 0x3f 1111111111110101 | 2194 0xafff, // 0x30 - 0x3f 1111111111110101 |
1877 // @ABCDEFGHIJKLMNO | 2195 // @ABCDEFGHIJKLMNO |
1878 0xffff, // 0x40 - 0x4f 1111111111111111 | 2196 0xffff, // 0x40 - 0x4f 1111111111111111 |
1879 // PQRSTUVWXYZ _ | 2197 // PQRSTUVWXYZ _ |
1880 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2198 0x87ff, // 0x50 - 0x5f 1111111111100001 |
1881 // abcdefghijklmno | 2199 // abcdefghijklmno |
1882 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2200 0xfffe, // 0x60 - 0x6f 0111111111111111 |
1883 // pqrstuvwxyz ~ | 2201 // pqrstuvwxyz ~ |
1884 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2202 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
1885 } | 2203 } |
OLD | NEW |