1/* 2 * Copyright (C) 2015 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.squareup.okhttp; 17 18import com.squareup.okhttp.internal.Util; 19import java.io.IOException; 20import java.net.InetAddress; 21import java.net.URI; 22import java.net.URL; 23import java.util.ArrayList; 24import java.util.List; 25import java.util.Set; 26import okio.Buffer; 27 28/** 29 * A <a href="https://url.spec.whatwg.org/">URL</a> with an {@code http} or {@code https} scheme. 30 * 31 * TODO: discussion on canonicalization 32 * 33 * TODO: discussion on encoding-by-parts 34 * 35 * TODO: discussion on this vs. java.net.URL vs. java.net.URI 36 */ 37public final class HttpUrl { 38 private static final char[] HEX_DIGITS = 39 { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; 40 41 /** Either "http" or "https". */ 42 private final String scheme; 43 44 /** Encoded username. */ 45 private final String username; 46 47 /** Encoded password. */ 48 private final String password; 49 50 /** Encoded hostname. */ 51 // TODO(jwilson): implement punycode. 52 private final String host; 53 54 /** Either 80, 443 or a user-specified port. In range [1..65535]. */ 55 private final int port; 56 57 /** Encoded path. */ 58 private final String path; 59 60 /** Encoded query. */ 61 private final String query; 62 63 /** Encoded fragment. */ 64 private final String fragment; 65 66 /** Canonical URL. */ 67 private final String url; 68 69 private HttpUrl(String scheme, String username, String password, String host, int port, 70 String path, String query, String fragment, String url) { 71 this.scheme = scheme; 72 this.username = username; 73 this.password = password; 74 this.host = host; 75 this.port = port; 76 this.path = path; 77 this.query = query; 78 this.fragment = fragment; 79 this.url = url; 80 } 81 82 public URL url() { 83 throw new UnsupportedOperationException(); // TODO(jwilson). 84 } 85 86 public URI uri() throws IOException { 87 throw new UnsupportedOperationException(); // TODO(jwilson). 88 } 89 90 /** Returns either "http" or "https". */ 91 public String scheme() { 92 return scheme; 93 } 94 95 public boolean isHttps() { 96 return scheme.equals("https"); 97 } 98 99 public String username() { 100 return username; 101 } 102 103 public String decodeUsername() { 104 return decode(username, 0, username.length()); 105 } 106 107 /** Returns the encoded password if one is present; null otherwise. */ 108 public String password() { 109 return password; 110 } 111 112 /** Returns the decoded password if one is present; null otherwise. */ 113 public String decodePassword() { 114 return password != null ? decode(password, 0, password.length()) : null; 115 } 116 117 /** 118 * Returns the host address suitable for use with {@link InetAddress#getAllByName(String)}. May 119 * be: 120 * <ul> 121 * <li>A regular host name, like {@code android.com}. 122 * <li>An IPv4 address, like {@code 127.0.0.1}. 123 * <li>An IPv6 address, like {@code ::1}. Note that there are no square braces. 124 * <li>An encoded IDN, like {@code xn--n3h.net}. 125 * </ul> 126 */ 127 public String host() { 128 return host; 129 } 130 131 /** 132 * Returns the decoded (potentially non-ASCII) hostname. The returned string may contain non-ASCII 133 * characters and is <strong>not suitable</strong> for DNS lookups; for that use {@link 134 * #host}. For example, this may return {@code ☃.net} which is a user-displayable IDN that cannot 135 * be used for DNS lookups without encoding. 136 */ 137 public String decodeHost() { 138 throw new UnsupportedOperationException(); // TODO(jwilson). 139 } 140 141 /** 142 * Returns the explicitly-specified port if one was provided, or the default port for this URL's 143 * scheme. For example, this returns 8443 for {@code https://square.com:8443/} and 443 for {@code 144 * https://square.com/}. The result is in {@code [1..65535]}. 145 */ 146 public int port() { 147 return port; 148 } 149 150 /** 151 * Returns 80 if {@code scheme.equals("http")}, 443 if {@code scheme.equals("https")} and -1 152 * otherwise. 153 */ 154 public static int defaultPort(String scheme) { 155 if (scheme.equals("http")) { 156 return 80; 157 } else if (scheme.equals("https")) { 158 return 443; 159 } else { 160 return -1; 161 } 162 } 163 164 /** 165 * Returns the entire path of this URL, encoded for use in HTTP resource resolution. The 166 * returned path is always nonempty and is prefixed with {@code /}. 167 */ 168 public String path() { 169 return path; 170 } 171 172 public List<String> decodePathSegments() { 173 List<String> result = new ArrayList<>(); 174 int segmentStart = 1; // Path always starts with '/'. 175 for (int i = segmentStart; i < path.length(); i++) { 176 if (path.charAt(i) == '/') { 177 result.add(decode(path, segmentStart, i)); 178 segmentStart = i + 1; 179 } 180 } 181 result.add(decode(path, segmentStart, path.length())); 182 return Util.immutableList(result); 183 } 184 185 /** 186 * Returns the query of this URL, encoded for use in HTTP resource resolution. The returned string 187 * may be null (for URLs with no query), empty (for URLs with an empty query) or non-empty (all 188 * other URLs). 189 */ 190 public String query() { 191 return query; 192 } 193 194 /** 195 * Returns the first query parameter named {@code name} decoded using UTF-8, or null if there is 196 * no such query parameter. 197 */ 198 public String queryParameter(String name) { 199 throw new UnsupportedOperationException(); // TODO(jwilson). 200 } 201 202 public Set<String> queryParameterNames() { 203 throw new UnsupportedOperationException(); // TODO(jwilson). 204 } 205 206 public List<String> queryParameterValues(String name) { 207 throw new UnsupportedOperationException(); // TODO(jwilson). 208 } 209 210 public String queryParameterName(int index) { 211 throw new UnsupportedOperationException(); // TODO(jwilson). 212 } 213 214 public String queryParameterValue(int index) { 215 throw new UnsupportedOperationException(); // TODO(jwilson). 216 } 217 218 public String fragment() { 219 return fragment; 220 } 221 222 /** 223 * Returns the URL that would be retrieved by following {@code link} from this URL. 224 * 225 * TODO: explain better. 226 */ 227 public HttpUrl resolve(String link) { 228 return new Builder().parse(this, link); 229 } 230 231 public Builder newBuilder() { 232 return new Builder(this); 233 } 234 235 /** 236 * Returns a new {@code OkUrl} representing {@code url} if it is a well-formed HTTP or HTTPS URL, 237 * or null if it isn't. 238 */ 239 public static HttpUrl parse(String url) { 240 return new Builder().parse(null, url); 241 } 242 243 public static HttpUrl get(URL url) { 244 return parse(url.toString()); 245 } 246 247 public static HttpUrl get(URI uri) { 248 return parse(uri.toString()); 249 } 250 251 @Override public boolean equals(Object o) { 252 return o instanceof HttpUrl && ((HttpUrl) o).url.equals(url); 253 } 254 255 @Override public int hashCode() { 256 return url.hashCode(); 257 } 258 259 @Override public String toString() { 260 return url; 261 } 262 263 public static final class Builder { 264 String scheme; 265 String username = ""; 266 String password; 267 String host; 268 int port = -1; 269 StringBuilder pathBuilder = new StringBuilder(); 270 String query; 271 String fragment; 272 273 public Builder() { 274 } 275 276 private Builder(HttpUrl url) { 277 throw new UnsupportedOperationException(); // TODO(jwilson) 278 } 279 280 public Builder scheme(String scheme) { 281 throw new UnsupportedOperationException(); // TODO(jwilson) 282 } 283 284 public Builder user(String user) { 285 throw new UnsupportedOperationException(); // TODO(jwilson) 286 } 287 288 public Builder encodedUser(String encodedUser) { 289 throw new UnsupportedOperationException(); // TODO(jwilson) 290 } 291 292 public Builder password(String password) { 293 throw new UnsupportedOperationException(); // TODO(jwilson) 294 } 295 296 public Builder encodedPassword(String encodedPassword) { 297 throw new UnsupportedOperationException(); // TODO(jwilson) 298 } 299 300 /** 301 * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 302 * address. 303 */ 304 public Builder host(String host) { 305 throw new UnsupportedOperationException(); // TODO(jwilson) 306 } 307 308 public Builder port(int port) { 309 throw new UnsupportedOperationException(); // TODO(jwilson) 310 } 311 312 public Builder addPathSegment(String pathSegment) { 313 if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); 314 throw new UnsupportedOperationException(); // TODO(jwilson) 315 } 316 317 public Builder addEncodedPathSegment(String encodedPathSegment) { 318 if (encodedPathSegment == null) { 319 throw new IllegalArgumentException("encodedPathSegment == null"); 320 } 321 throw new UnsupportedOperationException(); // TODO(jwilson) 322 } 323 324 public Builder encodedPath(String encodedPath) { 325 throw new UnsupportedOperationException(); // TODO(jwilson) 326 } 327 328 public Builder encodedQuery(String encodedQuery) { 329 throw new UnsupportedOperationException(); // TODO(jwilson) 330 } 331 332 /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ 333 public Builder addQueryParameter(String name, String value) { 334 if (name == null) throw new IllegalArgumentException("name == null"); 335 if (value == null) throw new IllegalArgumentException("value == null"); 336 throw new UnsupportedOperationException(); // TODO(jwilson) 337 } 338 339 /** Adds the pre-encoded query parameter to this URL's query string. */ 340 public Builder addEncodedQueryParameter(String encodedName, String encodedValue) { 341 if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); 342 if (encodedValue == null) throw new IllegalArgumentException("encodedValue == null"); 343 throw new UnsupportedOperationException(); // TODO(jwilson) 344 } 345 346 public Builder setQueryParameter(String name, String value) { 347 if (name == null) throw new IllegalArgumentException("name == null"); 348 if (value == null) throw new IllegalArgumentException("value == null"); 349 throw new UnsupportedOperationException(); // TODO(jwilson) 350 } 351 352 public Builder setEncodedQueryParameter(String encodedName, String encodedValue) { 353 if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); 354 if (encodedValue == null) throw new IllegalArgumentException("encodedValue == null"); 355 throw new UnsupportedOperationException(); // TODO(jwilson) 356 } 357 358 public Builder removeAllQueryParameters(String name) { 359 if (name == null) throw new IllegalArgumentException("name == null"); 360 throw new UnsupportedOperationException(); // TODO(jwilson) 361 } 362 363 public Builder removeAllEncodedQueryParameters(String encodedName) { 364 if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); 365 throw new UnsupportedOperationException(); // TODO(jwilson) 366 } 367 368 public Builder fragment(String fragment) { 369 throw new UnsupportedOperationException(); // TODO(jwilson) 370 } 371 372 public HttpUrl build() { 373 StringBuilder url = new StringBuilder(); 374 url.append(scheme); 375 url.append("://"); 376 377 String effectivePassword = (password != null && !password.isEmpty()) ? password : null; 378 if (!username.isEmpty() || effectivePassword != null) { 379 url.append(username); 380 if (effectivePassword != null) { 381 url.append(':'); 382 url.append(effectivePassword); 383 } 384 url.append('@'); 385 } 386 387 if (host.indexOf(':') != -1) { 388 // Host is an IPv6 address. 389 url.append('['); 390 url.append(host); 391 url.append(']'); 392 } else { 393 url.append(host); 394 } 395 396 int defaultPort = defaultPort(scheme); 397 int effectivePort = port != -1 ? port : defaultPort; 398 if (effectivePort != defaultPort) { 399 url.append(':'); 400 url.append(port); 401 } 402 403 String effectivePath = pathBuilder.length() > 0 404 ? pathBuilder.toString() 405 : "/"; 406 url.append(effectivePath); 407 408 if (query != null) { 409 url.append('?'); 410 url.append(query); 411 } 412 413 if (fragment != null) { 414 url.append('#'); 415 url.append(fragment); 416 } 417 418 return new HttpUrl(scheme, username, effectivePassword, host, effectivePort, effectivePath, 419 query, fragment, url.toString()); 420 } 421 422 HttpUrl parse(HttpUrl base, String input) { 423 int pos = skipLeadingAsciiWhitespace(input, 0, input.length()); 424 int limit = skipTrailingAsciiWhitespace(input, pos, input.length()); 425 426 // Scheme. 427 int schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit); 428 if (schemeDelimiterOffset != -1) { 429 if (input.regionMatches(true, pos, "https:", 0, 6)) { 430 this.scheme = "https"; 431 pos += "https:".length(); 432 } else if (input.regionMatches(true, pos, "http:", 0, 5)) { 433 this.scheme = "http"; 434 pos += "http:".length(); 435 } else { 436 return null; // Not an HTTP scheme. 437 } 438 } else if (base != null) { 439 this.scheme = base.scheme; 440 } else { 441 return null; // No scheme. 442 } 443 444 // Authority. 445 boolean hasUsername = false; 446 int slashCount = slashCount(input, pos, limit); 447 if (slashCount >= 2 || base == null || !base.scheme.equals(this.scheme)) { 448 // Read an authority if either: 449 // * The input starts with 2 or more slashes. These follow the scheme if it exists. 450 // * The input scheme exists and is different from the base URL's scheme. 451 // 452 // The structure of an authority is: 453 // username:password@host:port 454 // 455 // Username, password and port are optional. 456 // [username[:password]@]host[:port] 457 pos += slashCount; 458 authority: 459 while (true) { 460 int componentDelimiterOffset = delimiterOffset(input, pos, limit, "@/\\?#"); 461 int c = componentDelimiterOffset != limit 462 ? input.charAt(componentDelimiterOffset) 463 : -1; 464 switch (c) { 465 case '@': 466 // User info precedes. 467 if (this.password == null) { 468 int passwordColonOffset = delimiterOffset( 469 input, pos, componentDelimiterOffset, ":"); 470 this.username = hasUsername 471 ? (this.username + "%40" + username(input, pos, passwordColonOffset)) 472 : username(input, pos, passwordColonOffset); 473 if (passwordColonOffset != componentDelimiterOffset) { 474 this.password = password( 475 input, passwordColonOffset + 1, componentDelimiterOffset); 476 } 477 hasUsername = true; 478 } else { 479 this.password = this.password + "%40" 480 + password(input, pos, componentDelimiterOffset); 481 } 482 pos = componentDelimiterOffset + 1; 483 break; 484 485 case -1: 486 case '/': 487 case '\\': 488 case '?': 489 case '#': 490 // Host info precedes. 491 int portColonOffset = portColonOffset(input, pos, componentDelimiterOffset); 492 if (portColonOffset + 1 < componentDelimiterOffset) { 493 this.host = host(input, pos, portColonOffset); 494 this.port = port(input, portColonOffset + 1, componentDelimiterOffset); 495 if (this.port == -1) return null; // Invalid port. 496 } else { 497 this.host = host(input, pos, portColonOffset); 498 this.port = defaultPort(this.scheme); 499 } 500 if (this.host == null) return null; // Invalid host. 501 pos = componentDelimiterOffset; 502 break authority; 503 } 504 } 505 } else { 506 // This is a relative link. Copy over all authority components. Also maybe the path & query. 507 this.username = base.username; 508 this.password = base.password; 509 this.host = base.host; 510 this.port = base.port; 511 int c = pos != limit 512 ? input.charAt(pos) 513 : -1; 514 switch (c) { 515 case -1: 516 case '#': 517 pathBuilder.append(base.path); 518 this.query = base.query; 519 break; 520 521 case '?': 522 pathBuilder.append(base.path); 523 break; 524 525 case '/': 526 case '\\': 527 break; 528 529 default: 530 pathBuilder.append(base.path); 531 pathBuilder.append('/'); // Because pop wants the input to end with '/'. 532 pop(); 533 break; 534 } 535 } 536 537 // Resolve the relative path. 538 int pathDelimiterOffset = delimiterOffset(input, pos, limit, "?#"); 539 while (pos < pathDelimiterOffset) { 540 int pathSegmentDelimiterOffset = delimiterOffset(input, pos, pathDelimiterOffset, "/\\"); 541 int segmentLength = pathSegmentDelimiterOffset - pos; 542 543 if ((segmentLength == 2 && input.regionMatches(false, pos, "..", 0, 2)) 544 || (segmentLength == 4 && input.regionMatches(true, pos, "%2e.", 0, 4)) 545 || (segmentLength == 4 && input.regionMatches(true, pos, ".%2e", 0, 4)) 546 || (segmentLength == 6 && input.regionMatches(true, pos, "%2e%2e", 0, 6))) { 547 pop(); 548 } else if ((segmentLength == 1 && input.regionMatches(false, pos, ".", 0, 1)) 549 || (segmentLength == 3 && input.regionMatches(true, pos, "%2e", 0, 3))) { 550 // Skip '.' path segments. 551 } else if (pathSegmentDelimiterOffset < pathDelimiterOffset) { 552 pathSegment(input, pos, pathSegmentDelimiterOffset); 553 pathBuilder.append('/'); 554 } else { 555 pathSegment(input, pos, pathSegmentDelimiterOffset); 556 } 557 558 pos = pathSegmentDelimiterOffset; 559 if (pathSegmentDelimiterOffset < pathDelimiterOffset) { 560 pos++; // Eat '/'. 561 } 562 } 563 564 // Query. 565 if (pos < limit && input.charAt(pos) == '?') { 566 int queryDelimiterOffset = delimiterOffset(input, pos, limit, "#"); 567 this.query = query(input, pos + 1, queryDelimiterOffset); 568 pos = queryDelimiterOffset; 569 } 570 571 // Fragment. 572 if (pos < limit && input.charAt(pos) == '#') { 573 this.fragment = fragment(input, pos + 1, limit); 574 } 575 576 return build(); 577 } 578 579 /** Remove the last character '/' of path, plus all characters after the preceding '/'. */ 580 private void pop() { 581 if (pathBuilder.charAt(pathBuilder.length() - 1) != '/') throw new IllegalStateException(); 582 583 for (int i = pathBuilder.length() - 2; i >= 0; i--) { 584 if (pathBuilder.charAt(i) == '/') { 585 pathBuilder.delete(i + 1, pathBuilder.length()); 586 return; 587 } 588 } 589 590 // If we get this far, there's nothing to pop. Do nothing. 591 } 592 593 /** 594 * Increments {@code pos} until {@code input[pos]} is not ASCII whitespace. Stops at {@code 595 * limit}. 596 */ 597 private int skipLeadingAsciiWhitespace(String input, int pos, int limit) { 598 for (int i = pos; i < limit; i++) { 599 switch (input.charAt(i)) { 600 case '\t': 601 case '\n': 602 case '\f': 603 case '\r': 604 case ' ': 605 continue; 606 default: 607 return i; 608 } 609 } 610 return limit; 611 } 612 613 /** 614 * Decrements {@code limit} until {@code input[limit - 1]} is not ASCII whitespace. Stops at 615 * {@code pos}. 616 */ 617 private int skipTrailingAsciiWhitespace(String input, int pos, int limit) { 618 for (int i = limit - 1; i >= pos; i--) { 619 switch (input.charAt(i)) { 620 case '\t': 621 case '\n': 622 case '\f': 623 case '\r': 624 case ' ': 625 continue; 626 default: 627 return i + 1; 628 } 629 } 630 return pos; 631 } 632 633 /** 634 * Returns the index of the ':' in {@code input} that is after scheme characters. Returns -1 if 635 * {@code input} does not have a scheme that starts at {@code pos}. 636 */ 637 private static int schemeDelimiterOffset(String input, int pos, int limit) { 638 if (limit - pos < 2) return -1; 639 640 char c0 = input.charAt(pos); 641 if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1; // Not a scheme start char. 642 643 for (int i = pos + 1; i < limit; i++) { 644 char c = input.charAt(i); 645 646 if ((c >= 'a' && c <= 'z') 647 || (c >= 'A' && c <= 'Z') 648 || c == '+' 649 || c == '-' 650 || c == '.') { 651 continue; // Scheme character. Keep going. 652 } else if (c == ':') { 653 return i; // Scheme prefix! 654 } else { 655 return -1; // Non-scheme character before the first ':'. 656 } 657 } 658 659 return -1; // No ':'; doesn't start with a scheme. 660 } 661 662 /** Returns the number of '/' and '\' slashes in {@code input}, starting at {@code pos}. */ 663 private static int slashCount(String input, int pos, int limit) { 664 int slashCount = 0; 665 while (pos < limit) { 666 char c = input.charAt(pos); 667 if (c == '\\' || c == '/') { 668 slashCount++; 669 pos++; 670 } else { 671 break; 672 } 673 } 674 return slashCount; 675 } 676 677 /** 678 * Returns the index of the first character in {@code input} that contains a character in {@code 679 * delimiters}. Returns limit if there is no such character. 680 */ 681 private static int delimiterOffset(String input, int pos, int limit, String delimiters) { 682 for (int i = pos; i < limit; i++) { 683 if (delimiters.indexOf(input.charAt(i)) != -1) return i; 684 } 685 return limit; 686 } 687 688 /** Finds the first ':' in {@code input}, skipping characters between square braces "[...]". */ 689 private static int portColonOffset(String input, int pos, int limit) { 690 for (int i = pos; i < limit; i++) { 691 switch (input.charAt(i)) { 692 case '[': 693 while (++i < limit) { 694 if (input.charAt(i) == ']') break; 695 } 696 break; 697 case ':': 698 return i; 699 } 700 } 701 return limit; // No colon. 702 } 703 704 private String username(String input, int pos, int limit) { 705 return encode(input, pos, limit, " \"';<=>@[]^`{}|"); 706 } 707 708 private String password(String input, int pos, int limit) { 709 return encode(input, pos, limit, " \"':;<=>@[]\\^`{}|"); 710 } 711 712 private static String host(String input, int pos, int limit) { 713 // Start by percent decoding the host. The WHATWG spec suggests doing this only after we've 714 // checked for IPv6 square braces. But Chrome does it first, and that's more lenient. 715 String percentDecoded = decode(input, pos, limit); 716 717 // If the input is encased in square braces "[...]", drop 'em. We have an IPv6 address. 718 if (percentDecoded.startsWith("[") && percentDecoded.endsWith("]")) { 719 return decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1); 720 } 721 722 // Do IDN decoding. This converts {@code ☃.net} to {@code xn--n3h.net}. 723 String idnDecoded = domainToAscii(percentDecoded); 724 725 // Confirm that the decoded result doesn't contain any illegal characters. 726 int length = idnDecoded.length(); 727 if (delimiterOffset(idnDecoded, 0, length, "\u0000\t\n\r #%/:?@[\\]") != length) { 728 return null; 729 } 730 731 return idnDecoded; 732 } 733 734 private static String decodeIpv6(String input, int pos, int limit) { 735 return input.substring(pos, limit); // TODO(jwilson) implement IPv6 decoding. 736 } 737 738 private static String domainToAscii(String input) { 739 return input; // TODO(jwilson): implement IDN decoding. 740 } 741 742 private int port(String input, int pos, int limit) { 743 try { 744 String portString = encode(input, pos, limit, ""); // To skip '\n' etc. 745 int i = Integer.parseInt(portString); 746 if (i > 0 && i <= 65535) return i; 747 return -1; 748 } catch (NumberFormatException e) { 749 return -1; // Invalid port. 750 } 751 } 752 753 private void pathSegment(String input, int pos, int limit) { 754 encode(pathBuilder, input, pos, limit, " \"<>^`{}|"); 755 } 756 757 private String query(String input, int pos, int limit) { 758 return encode(input, pos, limit, " \"'<>"); 759 } 760 761 private String fragment(String input, int pos, int limit) { 762 return encode(input, pos, limit, ""); // To skip '\n' etc. 763 } 764 } 765 766 private static String decode(String encoded, int pos, int limit) { 767 for (int i = pos; i < limit; i++) { 768 if (encoded.charAt(i) == '%') { 769 // Slow path: the character at i requires decoding! 770 Buffer out = new Buffer(); 771 out.writeUtf8(encoded, pos, i); 772 return decode(out, encoded, i, limit); 773 } 774 } 775 776 // Fast path: no characters in [pos..limit) required decoding. 777 return encoded.substring(pos, limit); 778 } 779 780 private static String decode(Buffer out, String encoded, int pos, int limit) { 781 int codePoint; 782 for (int i = pos; i < limit; i += Character.charCount(codePoint)) { 783 codePoint = encoded.codePointAt(i); 784 if (codePoint == '%' && i + 2 < limit) { 785 int d1 = decodeHexDigit(encoded.charAt(i + 1)); 786 int d2 = decodeHexDigit(encoded.charAt(i + 2)); 787 if (d1 != -1 && d2 != -1) { 788 out.writeByte((d1 << 4) + d2); 789 i += 2; 790 continue; 791 } 792 } 793 out.writeUtf8CodePoint(codePoint); 794 } 795 return out.readUtf8(); 796 } 797 798 private static int decodeHexDigit(char c) { 799 if (c >= '0' && c <= '9') return c - '0'; 800 if (c >= 'a' && c <= 'f') return c - 'a' + 10; 801 if (c >= 'A' && c <= 'F') return c - 'A' + 10; 802 return -1; 803 } 804 805 /** 806 * Returns a substring of {@code input} on the range {@code [pos..limit)} with the following 807 * transformations: 808 * <ul> 809 * <li>Tabs, newlines, form feeds and carriage returns are skipped. 810 * <li>Characters in {@code encodeSet} are percent-encoded. 811 * <li>Control characters and non-ASCII characters are percent-encoded. 812 * <li>All other characters are copied without transformation. 813 * </ul> 814 */ 815 static String encode(String input, int pos, int limit, String encodeSet) { 816 int codePoint; 817 for (int i = pos; i < limit; i += Character.charCount(codePoint)) { 818 codePoint = input.codePointAt(i); 819 if (codePoint < 0x20 820 || codePoint >= 0x7f 821 || encodeSet.indexOf(codePoint) != -1) { 822 // Slow path: the character at i requires encoding! 823 StringBuilder out = new StringBuilder(); 824 out.append(input, pos, i); 825 encode(out, input, i, limit, encodeSet); 826 return out.toString(); 827 } 828 } 829 830 // Fast path: no characters in [pos..limit) required encoding. 831 return input.substring(pos, limit); 832 } 833 834 static void encode(StringBuilder out, String input, int pos, int limit, String encodeSet) { 835 Buffer utf8Buffer = null; // Lazily allocated. 836 int codePoint; 837 for (int i = pos; i < limit; i += Character.charCount(codePoint)) { 838 codePoint = input.codePointAt(i); 839 if (codePoint == '\t' 840 || codePoint == '\n' 841 || codePoint == '\f' 842 || codePoint == '\r') { 843 // Skip this character. 844 } else if (codePoint < 0x20 845 || codePoint >= 0x7f 846 || encodeSet.indexOf(codePoint) != -1) { 847 // Percent encode this character. 848 if (utf8Buffer == null) { 849 utf8Buffer = new Buffer(); 850 } 851 utf8Buffer.writeUtf8CodePoint(codePoint); 852 while (!utf8Buffer.exhausted()) { 853 int b = utf8Buffer.readByte() & 0xff; 854 out.append('%'); 855 out.append(HEX_DIGITS[(b >> 4) & 0xf]); 856 out.append(HEX_DIGITS[b & 0xf]); 857 } 858 } else { 859 // This character doesn't need encoding. Just copy it over. 860 out.append((char) codePoint); 861 } 862 } 863 } 864} 865