• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
isRetained(char c)39     protected abstract boolean isRetained(char c);
40 
41     /**
42      * Throws if {@code s} is invalid according to this encoder.
43      */
validate(String uri, int start, int end, String name)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      */
validateSimple(String s, String legal)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      */
appendEncoded(StringBuilder builder, String s, Charset charset, boolean isPartiallyEncoded)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 
encode(String s, Charset charset)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 
appendEncoded(StringBuilder builder, String s)139     public final void appendEncoded(StringBuilder builder, String s) {
140         appendEncoded(builder, s, Charsets.UTF_8, false);
141     }
142 
appendPartiallyEncoded(StringBuilder builder, String s)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      */
decode(String s, boolean convertPlus, Charset charset)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      */
hexToInt(char c)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 
decode(String s)202     public static String decode(String s) {
203         return decode(s, false, Charsets.UTF_8);
204     }
205 
appendHex(StringBuilder builder, String s, Charset charset)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 
appendHex(StringBuilder sb, byte b)212     private static void appendHex(StringBuilder sb, byte b) {
213         sb.append('%');
214         sb.append(Byte.toHexString(b, true));
215     }
216 }
217