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