blob: e1470e0a958f2ecc2c4c6ef32b4f2d799f91ede9 [file] [log] [blame]
Neil Fuller0f6f3bd2018-07-13 20:02:58 +01001/*
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
17package android.net;
18
19import java.net.URISyntaxException;
20import java.nio.ByteBuffer;
21import java.nio.charset.CharacterCodingException;
22import java.nio.charset.Charset;
23import java.nio.charset.CharsetDecoder;
24import java.nio.charset.CodingErrorAction;
25
26/**
27 * Decodes “application/x-www-form-urlencoded” content.
28 *
29 * @hide
30 */
31public final class UriCodec {
32
33 private UriCodec() {}
34
35 /**
36 * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f').
37 */
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
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
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 */
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
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
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}