Home | History | Annotate | Download | only in utility
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 package com.android.exchange.utility;
     19 
     20 import java.io.ByteArrayOutputStream;
     21 import java.net.URISyntaxException;
     22 import java.nio.charset.Charset;
     23 import java.nio.charset.Charsets;
     24 
     25 // Note: This class copied verbatim from libcore.net
     26 
     27 /**
     28  * Encodes and decodes {@code application/x-www-form-urlencoded} content.
     29  * Subclasses define exactly which characters are legal.
     30  *
     31  * <p>By default, UTF-8 is used to encode escaped characters. A single input
     32  * character like "\u0080" may be encoded to multiple octets like %C2%80.
     33  */
     34 public abstract class UriCodec {
     35 
     36     /**
     37      * Returns true if {@code c} does not need to be escaped.
     38      */
     39     protected abstract boolean isRetained(char c);
     40 
     41     /**
     42      * Throws if {@code s} is invalid according to this encoder.
     43      */
     44     public final String validate(String uri, int start, int end, String name)
     45             throws URISyntaxException {
     46         for (int i = start; i < end; ) {
     47             char ch = uri.charAt(i);
     48             if ((ch >= 'a' && ch <= 'z')
     49                     || (ch >= 'A' && ch <= 'Z')
     50                     || (ch >= '0' && ch <= '9')
     51                     || isRetained(ch)) {
     52                 i++;
     53             } else if (ch == '%') {
     54                 if (i + 2 >= end) {
     55                     throw new URISyntaxException(uri, "Incomplete % sequence in " + name, i);
     56                 }
     57                 int d1 = hexToInt(uri.charAt(i + 1));
     58                 int d2 = hexToInt(uri.charAt(i + 2));
     59                 if (d1 == -1 || d2 == -1) {
     60                     throw new URISyntaxException(uri, "Invalid % sequence: "
     61                             + uri.substring(i, i + 3) + " in " + name, i);
     62                 }
     63                 i += 3;
     64             } else {
     65                 throw new URISyntaxException(uri, "Illegal character in " + name, i);
     66             }
     67         }
     68         return uri.substring(start, end);
     69     }
     70 
     71     /**
     72      * Throws if {@code s} contains characters that are not letters, digits or
     73      * in {@code legal}.
     74      */
     75     public static void validateSimple(String s, String legal)
     76             throws URISyntaxException {
     77         for (int i = 0; i < s.length(); i++) {
     78             char ch = s.charAt(i);
     79             if (!((ch >= 'a' && ch <= 'z')
     80                     || (ch >= 'A' && ch <= 'Z')
     81                     || (ch >= '0' && ch <= '9')
     82                     || legal.indexOf(ch) > -1)) {
     83                 throw new URISyntaxException(s, "Illegal character", i);
     84             }
     85         }
     86     }
     87 
     88     /**
     89      * Encodes {@code s} and appends the result to {@code builder}.
     90      *
     91      * @param isPartiallyEncoded true to fix input that has already been
     92      *     partially or fully encoded. For example, input of "hello%20world" is
     93      *     unchanged with isPartiallyEncoded=true but would be double-escaped to
     94      *     "hello%2520world" otherwise.
     95      */
     96     private void appendEncoded(StringBuilder builder, String s, Charset charset,
     97             boolean isPartiallyEncoded) {
     98         if (s == null) {
     99             throw new NullPointerException();
    100         }
    101 
    102         int escapeStart = -1;
    103         for (int i = 0; i < s.length(); i++) {
    104             char c = s.charAt(i);
    105             if ((c >= 'a' && c <= 'z')
    106                     || (c >= 'A' && c <= 'Z')
    107                     || (c >= '0' && c <= '9')
    108                     || isRetained(c)
    109                     || (c == '%' && isPartiallyEncoded)) {
    110                 if (escapeStart != -1) {
    111                     appendHex(builder, s.substring(escapeStart, i), charset);
    112                     escapeStart = -1;
    113                 }
    114                 if (c == '%' && isPartiallyEncoded) {
    115                     // this is an encoded 3-character sequence like "%20"
    116                     builder.append(s, i, i + 3);
    117                     i += 2;
    118                 } else if (c == ' ') {
    119                     builder.append('+');
    120                 } else {
    121                     builder.append(c);
    122                 }
    123             } else if (escapeStart == -1) {
    124                 escapeStart = i;
    125             }
    126         }
    127         if (escapeStart != -1) {
    128             appendHex(builder, s.substring(escapeStart, s.length()), charset);
    129         }
    130     }
    131 
    132     public final String encode(String s, Charset charset) {
    133         // Guess a bit larger for encoded form
    134         StringBuilder builder = new StringBuilder(s.length() + 16);
    135         appendEncoded(builder, s, charset, false);
    136         return builder.toString();
    137     }
    138 
    139     public final void appendEncoded(StringBuilder builder, String s) {
    140         appendEncoded(builder, s, Charsets.UTF_8, false);
    141     }
    142 
    143     public final void appendPartiallyEncoded(StringBuilder builder, String s) {
    144         appendEncoded(builder, s, Charsets.UTF_8, true);
    145     }
    146 
    147     /**
    148      * @param convertPlus true to convert '+' to ' '.
    149      */
    150     public static String decode(String s, boolean convertPlus, Charset charset) {
    151         if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) {
    152             return s;
    153         }
    154 
    155         StringBuilder result = new StringBuilder(s.length());
    156         ByteArrayOutputStream out = new ByteArrayOutputStream();
    157         for (int i = 0; i < s.length();) {
    158             char c = s.charAt(i);
    159             if (c == '%') {
    160                 do {
    161                     if (i + 2 >= s.length()) {
    162                         throw new IllegalArgumentException("Incomplete % sequence at: " + i);
    163                     }
    164                     int d1 = hexToInt(s.charAt(i + 1));
    165                     int d2 = hexToInt(s.charAt(i + 2));
    166                     if (d1 == -1 || d2 == -1) {
    167                         throw new IllegalArgumentException("Invalid % sequence " +
    168                                 s.substring(i, i + 3) + " at " + i);
    169                     }
    170                     out.write((byte) ((d1 << 4) + d2));
    171                     i += 3;
    172                 } while (i < s.length() && s.charAt(i) == '%');
    173                 result.append(new String(out.toByteArray(), charset));
    174                 out.reset();
    175             } else {
    176                 if (convertPlus && c == '+') {
    177                     c = ' ';
    178                 }
    179                 result.append(c);
    180                 i++;
    181             }
    182         }
    183         return result.toString();
    184     }
    185 
    186     /**
    187      * Like {@link Character#digit}, but without support for non-ASCII
    188      * characters.
    189      */
    190     private static int hexToInt(char c) {
    191         if ('0' <= c && c <= '9') {
    192             return c - '0';
    193         } else if ('a' <= c && c <= 'f') {
    194             return 10 + (c - 'a');
    195         } else if ('A' <= c && c <= 'F') {
    196             return 10 + (c - 'A');
    197         } else {
    198             return -1;
    199         }
    200     }
    201 
    202     public static String decode(String s) {
    203         return decode(s, false, Charsets.UTF_8);
    204     }
    205 
    206     private static void appendHex(StringBuilder builder, String s, Charset charset) {
    207         for (byte b : s.getBytes(charset)) {
    208             appendHex(builder, b);
    209         }
    210     }
    211 
    212     private static void appendHex(StringBuilder sb, byte b) {
    213         sb.append('%');
    214         sb.append(Byte.toHexString(b, true));
    215     }
    216 }
    217