001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.net;
018
019import static com.google.common.base.Preconditions.checkNotNull;
020
021import com.google.common.annotations.Beta;
022import com.google.common.annotations.GwtCompatible;
023import com.google.common.escape.UnicodeEscaper;
024
025/**
026 * A {@code UnicodeEscaper} that escapes some set of Java characters using a
027 * UTF-8 based percent encoding scheme. The set of safe characters (those which
028 * remain unescaped) can be specified on construction.
029 *
030 * <p>This class is primarily used for creating URI escapers in {@link
031 * UrlEscapers} but can be used directly if required. While URI escapers impose
032 * specific semantics on which characters are considered 'safe', this class has
033 * a minimal set of restrictions.
034 *
035 * <p>When escaping a String, the following rules apply:
036 * <ul>
037 * <li>All specified safe characters remain unchanged.
038 * <li>If {@code plusForSpace} was specified, the space character " " is
039 *     converted into a plus sign {@code "+"}.
040 * <li>All other characters are converted into one or more bytes using UTF-8
041 *     encoding and each byte is then represented by the 3-character string
042 *     "%XX", where "XX" is the two-digit, uppercase, hexadecimal representation
043 *     of the byte value.
044 * </ul>
045 *
046 * <p>For performance reasons the only currently supported character encoding of
047 * this class is UTF-8.
048 *
049 * <p><b>Note</b>: This escaper produces uppercase hexadecimal sequences. From
050 * <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>:<br>
051 * <i>"URI producers and normalizers should use uppercase hexadecimal digits
052 * for all percent-encodings."</i>
053 *
054 * @author David Beaumont
055 * @since 15.0
056 */
057@Beta
058@GwtCompatible
059public final class PercentEscaper extends UnicodeEscaper {
060
061  // In some escapers spaces are escaped to '+'
062  private static final char[] PLUS_SIGN = { '+' };
063
064  // Percent escapers output upper case hex digits (uri escapers require this).
065  private static final char[] UPPER_HEX_DIGITS =
066      "0123456789ABCDEF".toCharArray();
067
068  /**
069   * If true we should convert space to the {@code +} character.
070   */
071  private final boolean plusForSpace;
072
073  /**
074   * An array of flags where for any {@code char c} if {@code safeOctets[c]} is
075   * true then {@code c} should remain unmodified in the output. If
076   * {@code c > safeOctets.length} then it should be escaped.
077   */
078  private final boolean[] safeOctets;
079
080  /**
081   * Constructs a percent escaper with the specified safe characters and
082   * optional handling of the space character.
083   *
084   * <p>Not that it is allowed, but not necessarily desirable to specify {@code %}
085   * as a safe character. This has the effect of creating an escaper which has no
086   * well defined inverse but it can be useful when escaping additional characters.
087   *
088   * @param safeChars a non null string specifying additional safe characters
089   *        for this escaper (the ranges 0..9, a..z and A..Z are always safe and
090   *        should not be specified here)
091   * @param plusForSpace true if ASCII space should be escaped to {@code +}
092   *        rather than {@code %20}
093   * @throws IllegalArgumentException if any of the parameters were invalid
094   */
095  public PercentEscaper(String safeChars, boolean plusForSpace) {
096    // TODO(user): Switch to static factory methods for creation now that class is final.
097    // TODO(user): Support escapers where alphanumeric chars are not safe.
098    checkNotNull(safeChars);  // eager for GWT.
099    // Avoid any misunderstandings about the behavior of this escaper
100    if (safeChars.matches(".*[0-9A-Za-z].*")) {
101      throw new IllegalArgumentException(
102          "Alphanumeric characters are always 'safe' and should not be " +
103          "explicitly specified");
104    }
105    safeChars += "abcdefghijklmnopqrstuvwxyz" +
106                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
107                 "0123456789";
108    // Avoid ambiguous parameters. Safe characters are never modified so if
109    // space is a safe character then setting plusForSpace is meaningless.
110    if (plusForSpace && safeChars.contains(" ")) {
111      throw new IllegalArgumentException(
112          "plusForSpace cannot be specified when space is a 'safe' character");
113    }
114    this.plusForSpace = plusForSpace;
115    this.safeOctets = createSafeOctets(safeChars);
116  }
117
118  /**
119   * Creates a boolean array with entries corresponding to the character values
120   * specified in safeChars set to true. The array is as small as is required to
121   * hold the given character information.
122   */
123  private static boolean[] createSafeOctets(String safeChars) {
124    int maxChar = -1;
125    char[] safeCharArray = safeChars.toCharArray();
126    for (char c : safeCharArray) {
127      maxChar = Math.max(c, maxChar);
128    }
129    boolean[] octets = new boolean[maxChar + 1];
130    for (char c : safeCharArray) {
131      octets[c] = true;
132    }
133    return octets;
134  }
135
136  /*
137   * Overridden for performance. For unescaped strings this improved the
138   * performance of the uri escaper from ~760ns to ~400ns as measured by
139   * {@link CharEscapersBenchmark}.
140   */
141  @Override
142  protected int nextEscapeIndex(CharSequence csq, int index, int end) {
143    checkNotNull(csq);
144    for (; index < end; index++) {
145      char c = csq.charAt(index);
146      if (c >= safeOctets.length || !safeOctets[c]) {
147        break;
148      }
149    }
150    return index;
151  }
152
153  /*
154   * Overridden for performance. For unescaped strings this improved the
155   * performance of the uri escaper from ~400ns to ~170ns as measured by
156   * {@link CharEscapersBenchmark}.
157   */
158  @Override
159  public String escape(String s) {
160    checkNotNull(s);
161    int slen = s.length();
162    for (int index = 0; index < slen; index++) {
163      char c = s.charAt(index);
164      if (c >= safeOctets.length || !safeOctets[c]) {
165        return escapeSlow(s, index);
166      }
167    }
168    return s;
169  }
170
171  /**
172   * Escapes the given Unicode code point in UTF-8.
173   */
174  @Override
175  protected char[] escape(int cp) {
176    // We should never get negative values here but if we do it will throw an
177    // IndexOutOfBoundsException, so at least it will get spotted.
178    if (cp < safeOctets.length && safeOctets[cp]) {
179      return null;
180    } else if (cp == ' ' && plusForSpace) {
181      return PLUS_SIGN;
182    } else if (cp <= 0x7F) {
183      // Single byte UTF-8 characters
184      // Start with "%--" and fill in the blanks
185      char[] dest = new char[3];
186      dest[0] = '%';
187      dest[2] = UPPER_HEX_DIGITS[cp & 0xF];
188      dest[1] = UPPER_HEX_DIGITS[cp >>> 4];
189      return dest;
190    } else if (cp <= 0x7ff) {
191      // Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff]
192      // Start with "%--%--" and fill in the blanks
193      char[] dest = new char[6];
194      dest[0] = '%';
195      dest[3] = '%';
196      dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
197      cp >>>= 4;
198      dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
199      cp >>>= 2;
200      dest[2] = UPPER_HEX_DIGITS[cp & 0xF];
201      cp >>>= 4;
202      dest[1] = UPPER_HEX_DIGITS[0xC | cp];
203      return dest;
204    } else if (cp <= 0xffff) {
205      // Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff]
206      // Start with "%E-%--%--" and fill in the blanks
207      char[] dest = new char[9];
208      dest[0] = '%';
209      dest[1] = 'E';
210      dest[3] = '%';
211      dest[6] = '%';
212      dest[8] = UPPER_HEX_DIGITS[cp & 0xF];
213      cp >>>= 4;
214      dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
215      cp >>>= 2;
216      dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
217      cp >>>= 4;
218      dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
219      cp >>>= 2;
220      dest[2] = UPPER_HEX_DIGITS[cp];
221      return dest;
222    } else if (cp <= 0x10ffff) {
223      char[] dest = new char[12];
224      // Four byte UTF-8 characters [cp >= 0xffff && cp <= 0x10ffff]
225      // Start with "%F-%--%--%--" and fill in the blanks
226      dest[0] = '%';
227      dest[1] = 'F';
228      dest[3] = '%';
229      dest[6] = '%';
230      dest[9] = '%';
231      dest[11] = UPPER_HEX_DIGITS[cp & 0xF];
232      cp >>>= 4;
233      dest[10] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
234      cp >>>= 2;
235      dest[8] = UPPER_HEX_DIGITS[cp & 0xF];
236      cp >>>= 4;
237      dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
238      cp >>>= 2;
239      dest[5] = UPPER_HEX_DIGITS[cp & 0xF];
240      cp >>>= 4;
241      dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)];
242      cp >>>= 2;
243      dest[2] = UPPER_HEX_DIGITS[cp & 0x7];
244      return dest;
245    } else {
246      // If this ever happens it is due to bug in UnicodeEscaper, not bad input.
247      throw new IllegalArgumentException(
248          "Invalid unicode character value " + cp);
249    }
250  }
251}