1 /*
2  * Copyright (C) 2015 The Android Open Source Project
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  */
16 
17 package android.net;
18 
19 import java.net.URISyntaxException;
20 import java.nio.ByteBuffer;
21 import java.nio.charset.CharacterCodingException;
22 import java.nio.charset.Charset;
23 import java.nio.charset.CharsetDecoder;
24 import java.nio.charset.CodingErrorAction;
25 
26 /**
27  * Decodes “application/x-www-form-urlencoded” content.
28  *
29  * @hide
30  */
31 public final class UriCodec {
32 
UriCodec()33     private UriCodec() {}
34 
35     /**
36      * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f').
37      */
hexCharToValue(char c)38     private static int hexCharToValue(char c) {
39         if ('0' <= c && c <= '9') {
40             return c - '0';
41         }
42         if ('a' <= c && c <= 'f') {
43             return 10 + c - 'a';
44         }
45         if ('A' <= c && c <= 'F') {
46             return 10 + c - 'A';
47         }
48         return -1;
49     }
50 
unexpectedCharacterException( String uri, String name, char unexpected, int index)51     private static URISyntaxException unexpectedCharacterException(
52             String uri, String name, char unexpected, int index) {
53         String nameString = (name == null) ? "" :  " in [" + name + "]";
54         return new URISyntaxException(
55                 uri, "Unexpected character" + nameString + ": " + unexpected, index);
56     }
57 
getNextCharacter(String uri, int index, int end, String name)58     private static char getNextCharacter(String uri, int index, int end, String name)
59              throws URISyntaxException {
60         if (index >= end) {
61             String nameString = (name == null) ? "" :  " in [" + name + "]";
62             throw new URISyntaxException(
63                     uri, "Unexpected end of string" + nameString, index);
64         }
65         return uri.charAt(index);
66     }
67 
68     /**
69      * Decode a string according to the rules of this decoder.
70      *
71      * - if {@code convertPlus == true} all ‘+’ chars in the decoded output are converted to ‘ ‘
72      *   (white space)
73      * - if {@code throwOnFailure == true}, an {@link IllegalArgumentException} is thrown for
74      *   invalid inputs. Else, U+FFFd is emitted to the output in place of invalid input octets.
75      */
decode( String s, boolean convertPlus, Charset charset, boolean throwOnFailure)76     public static String decode(
77             String s, boolean convertPlus, Charset charset, boolean throwOnFailure) {
78         StringBuilder builder = new StringBuilder(s.length());
79         appendDecoded(builder, s, convertPlus, charset, throwOnFailure);
80         return builder.toString();
81     }
82 
83     /**
84      * Character to be output when there's an error decoding an input.
85      */
86     private static final char INVALID_INPUT_CHARACTER = '\ufffd';
87 
appendDecoded( StringBuilder builder, String s, boolean convertPlus, Charset charset, boolean throwOnFailure)88     private static void appendDecoded(
89             StringBuilder builder,
90             String s,
91             boolean convertPlus,
92             Charset charset,
93             boolean throwOnFailure) {
94         CharsetDecoder decoder = charset.newDecoder()
95                 .onMalformedInput(CodingErrorAction.REPLACE)
96                 .replaceWith("\ufffd")
97                 .onUnmappableCharacter(CodingErrorAction.REPORT);
98         // Holds the bytes corresponding to the escaped chars being read (empty if the last char
99         // wasn't a escaped char).
100         ByteBuffer byteBuffer = ByteBuffer.allocate(s.length());
101         int i = 0;
102         while (i < s.length()) {
103             char c = s.charAt(i);
104             i++;
105             switch (c) {
106                 case '+':
107                     flushDecodingByteAccumulator(
108                             builder, decoder, byteBuffer, throwOnFailure);
109                     builder.append(convertPlus ? ' ' : '+');
110                     break;
111                 case '%':
112                     // Expect two characters representing a number in hex.
113                     byte hexValue = 0;
114                     for (int j = 0; j < 2; j++) {
115                         try {
116                             c = getNextCharacter(s, i, s.length(), null /* name */);
117                         } catch (URISyntaxException e) {
118                             // Unexpected end of input.
119                             if (throwOnFailure) {
120                                 throw new IllegalArgumentException(e);
121                             } else {
122                                 flushDecodingByteAccumulator(
123                                         builder, decoder, byteBuffer, throwOnFailure);
124                                 builder.append(INVALID_INPUT_CHARACTER);
125                                 return;
126                             }
127                         }
128                         i++;
129                         int newDigit = hexCharToValue(c);
130                         if (newDigit < 0) {
131                             if (throwOnFailure) {
132                                 throw new IllegalArgumentException(
133                                         unexpectedCharacterException(s, null /* name */, c, i - 1));
134                             } else {
135                                 flushDecodingByteAccumulator(
136                                         builder, decoder, byteBuffer, throwOnFailure);
137                                 builder.append(INVALID_INPUT_CHARACTER);
138                                 break;
139                             }
140                         }
141                         hexValue = (byte) (hexValue * 0x10 + newDigit);
142                     }
143                     byteBuffer.put(hexValue);
144                     break;
145                 default:
146                     flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
147                     builder.append(c);
148             }
149         }
150         flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
151     }
152 
flushDecodingByteAccumulator( StringBuilder builder, CharsetDecoder decoder, ByteBuffer byteBuffer, boolean throwOnFailure)153     private static void flushDecodingByteAccumulator(
154             StringBuilder builder,
155             CharsetDecoder decoder,
156             ByteBuffer byteBuffer,
157             boolean throwOnFailure) {
158         if (byteBuffer.position() == 0) {
159             return;
160         }
161         byteBuffer.flip();
162         try {
163             builder.append(decoder.decode(byteBuffer));
164         } catch (CharacterCodingException e) {
165             if (throwOnFailure) {
166                 throw new IllegalArgumentException(e);
167             } else {
168                 builder.append(INVALID_INPUT_CHARACTER);
169             }
170         } finally {
171             // Use the byte buffer to write again.
172             byteBuffer.flip();
173             byteBuffer.limit(byteBuffer.capacity());
174         }
175     }
176 }
177