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 * https://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 org.apache.commons.codec.net;
19
20 import java.io.ByteArrayOutputStream;
21 import java.io.UnsupportedEncodingException;
22 import java.net.URLDecoder;
23 import java.net.URLEncoder;
24 import java.util.BitSet;
25
26 import org.apache.commons.codec.BinaryDecoder;
27 import org.apache.commons.codec.BinaryEncoder;
28 import org.apache.commons.codec.CharEncoding;
29 import org.apache.commons.codec.DecoderException;
30 import org.apache.commons.codec.EncoderException;
31 import org.apache.commons.codec.StringDecoder;
32 import org.apache.commons.codec.StringEncoder;
33 import org.apache.commons.codec.binary.StringUtils;
34
35 /**
36 * Implements the 'www-form-urlencoded' encoding scheme, also misleadingly known as URL encoding.
37 * <p>
38 * This codec is meant to be a replacement for standard Java classes {@link URLEncoder} and
39 * {@link URLDecoder} on older Java platforms, as these classes in Java versions below
40 * 1.4 rely on the platform's default charset encoding.
41 * </p>
42 * <p>
43 * This class is thread-safe as of 1.11
44 * </p>
45 *
46 * @see <a href="https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1">Chapter 17.13.4 Form content types</a>
47 * of the <a href="https://www.w3.org/TR/html4/">HTML 4.01 Specification</a>
48 *
49 * @since 1.2
50 */
51 public class URLCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
52
53 /**
54 * Release 1.5 made this field final.
55 */
56 protected static final byte ESCAPE_CHAR = '%';
57
58 private static final byte PLUS_CHAR = '+';
59
60 /**
61 * BitSet of www-form-url safe characters.
62 * This is a copy of the internal BitSet which is now used for the conversion.
63 * Changes to this field are ignored.
64 *
65 * @deprecated 1.11 Will be removed in 2.0 (CODEC-230)
66 */
67 @Deprecated
68 protected static final BitSet WWW_FORM_URL;
69
70 private static final BitSet WWW_FORM_URL_SAFE = new BitSet(256);
71
72 // Static initializer for www_form_url
73 static {
74 // alpha characters
75 for (int i = 'a'; i <= 'z'; i++) {
76 WWW_FORM_URL_SAFE.set(i);
77 }
78 for (int i = 'A'; i <= 'Z'; i++) {
79 WWW_FORM_URL_SAFE.set(i);
80 }
81 // numeric characters
82 for (int i = '0'; i <= '9'; i++) {
83 WWW_FORM_URL_SAFE.set(i);
84 }
85 // special chars
86 WWW_FORM_URL_SAFE.set('-');
87 WWW_FORM_URL_SAFE.set('_');
88 WWW_FORM_URL_SAFE.set('.');
89 WWW_FORM_URL_SAFE.set('*');
90 // blank to be replaced with +
91 WWW_FORM_URL_SAFE.set(' ');
92
93 // Create a copy in case anyone (ab)uses it
94 WWW_FORM_URL = (BitSet) WWW_FORM_URL_SAFE.clone();
95 }
96
97 /**
98 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted
99 * back to their original representation.
100 *
101 * @param bytes
102 * array of URL safe characters.
103 * @return array of original bytes.
104 * @throws DecoderException
105 * Thrown if URL decoding is unsuccessful.
106 */
107 public static final byte[] decodeUrl(final byte[] bytes) throws DecoderException {
108 if (bytes == null) {
109 return null;
110 }
111 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
112 for (int i = 0; i < bytes.length; i++) {
113 final int b = bytes[i];
114 if (b == PLUS_CHAR) {
115 buffer.write(' ');
116 } else if (b == ESCAPE_CHAR) {
117 try {
118 final int u = Utils.digit16(bytes[++i]);
119 final int l = Utils.digit16(bytes[++i]);
120 buffer.write((char) ((u << 4) + l));
121 } catch (final ArrayIndexOutOfBoundsException e) {
122 throw new DecoderException("Invalid URL encoding: ", e);
123 }
124 } else {
125 buffer.write(b);
126 }
127 }
128 return buffer.toByteArray();
129 }
130
131 /**
132 * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped.
133 * The characters {@code %} and {@code +} are always escaped because {@link #decodeUrl(byte[])}
134 * treats them as URL-encoding syntax.
135 *
136 * @param urlsafe
137 * bitset of characters deemed URL safe, except for {@code %} and {@code +}.
138 * @param bytes
139 * array of bytes to convert to URL safe characters.
140 * @return array of bytes containing URL safe characters.
141 */
142 public static final byte[] encodeUrl(BitSet urlsafe, final byte[] bytes) {
143 if (bytes == null) {
144 return null;
145 }
146 if (urlsafe == null) {
147 urlsafe = WWW_FORM_URL_SAFE;
148 }
149
150 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
151 for (final byte c : bytes) {
152 int b = c;
153 if (b < 0) {
154 b = 256 + b;
155 }
156 if (urlsafe.get(b) && b != ESCAPE_CHAR && b != PLUS_CHAR) {
157 if (b == ' ') {
158 b = PLUS_CHAR;
159 }
160 buffer.write(b);
161 } else {
162 buffer.write(ESCAPE_CHAR);
163 final char hex1 = Utils.hexChar(b >> 4);
164 final char hex2 = Utils.hexChar(b);
165 buffer.write(hex1);
166 buffer.write(hex2);
167 }
168 }
169 return buffer.toByteArray();
170 }
171
172 /**
173 * The default charset used for string decoding and encoding.
174 *
175 * @deprecated TODO: This field will be changed to a private final Charset in 2.0. (CODEC-126)
176 */
177 @Deprecated
178 protected volatile String charset; // added volatile: see CODEC-232
179
180 /**
181 * Default constructor.
182 */
183 public URLCodec() {
184 this(CharEncoding.UTF_8);
185 }
186
187 /**
188 * Constructs a new instance for the selection of a default charset.
189 *
190 * @param charset The default string charset to use.
191 */
192 public URLCodec(final String charset) {
193 this.charset = charset;
194 }
195
196 /**
197 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted
198 * back to their original representation.
199 *
200 * @param bytes
201 * array of URL safe characters.
202 * @return array of original bytes.
203 * @throws DecoderException
204 * Thrown if URL decoding is unsuccessful.
205 */
206 @Override
207 public byte[] decode(final byte[] bytes) throws DecoderException {
208 return decodeUrl(bytes);
209 }
210
211 /**
212 * Decodes a URL safe object into its original form. Escaped characters are converted back to their original
213 * representation.
214 *
215 * @param obj
216 * URL safe object to convert into its original form.
217 * @return original object.
218 * @throws DecoderException
219 * Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure
220 * condition is encountered during the decode process.
221 */
222 @Override
223 public Object decode(final Object obj) throws DecoderException {
224 if (obj == null) {
225 return null;
226 }
227 if (obj instanceof byte[]) {
228 return decode((byte[]) obj);
229 }
230 if (obj instanceof String) {
231 return decode((String) obj);
232 }
233 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be URL decoded");
234 }
235
236 /**
237 * Decodes a URL safe string into its original form using the default string charset. Escaped characters are
238 * converted back to their original representation.
239 *
240 * @param str
241 * URL safe string to convert into its original form.
242 * @return original string.
243 * @throws DecoderException
244 * Thrown if URL decoding is unsuccessful.
245 * @see #getDefaultCharset()
246 */
247 @Override
248 public String decode(final String str) throws DecoderException {
249 if (str == null) {
250 return null;
251 }
252 try {
253 return decode(str, getDefaultCharset());
254 } catch (final UnsupportedEncodingException e) {
255 throw new DecoderException(e.getMessage(), e);
256 }
257 }
258
259 /**
260 * Decodes a URL safe string into its original form using the specified encoding. Escaped characters are converted
261 * back to their original representation.
262 *
263 * @param str
264 * URL safe string to convert into its original form.
265 * @param charsetName
266 * the original string charset.
267 * @return original string.
268 * @throws DecoderException
269 * Thrown if URL decoding is unsuccessful.
270 * @throws UnsupportedEncodingException
271 * Thrown if charset is not supported.
272 */
273 public String decode(final String str, final String charsetName)
274 throws DecoderException, UnsupportedEncodingException {
275 if (str == null) {
276 return null;
277 }
278 return new String(decode(StringUtils.getBytesUsAscii(str)), charsetName);
279 }
280
281 /**
282 * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped.
283 *
284 * @param bytes
285 * array of bytes to convert to URL safe characters.
286 * @return array of bytes containing URL safe characters.
287 */
288 @Override
289 public byte[] encode(final byte[] bytes) {
290 return encodeUrl(WWW_FORM_URL_SAFE, bytes);
291 }
292
293 /**
294 * Encodes an object into its URL safe form. Unsafe characters are escaped.
295 *
296 * @param obj
297 * string to convert to a URL safe form.
298 * @return URL safe object.
299 * @throws EncoderException
300 * Thrown if URL encoding is not applicable to objects of this type or if encoding is unsuccessful.
301 */
302 @Override
303 public Object encode(final Object obj) throws EncoderException {
304 if (obj == null) {
305 return null;
306 }
307 if (obj instanceof byte[]) {
308 return encode((byte[]) obj);
309 }
310 if (obj instanceof String) {
311 return encode((String) obj);
312 }
313 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be URL encoded");
314 }
315
316 /**
317 * Encodes a string into its URL safe form using the default string charset. Unsafe characters are escaped.
318 *
319 * @param str
320 * string to convert to a URL safe form.
321 * @return URL safe string.
322 * @throws EncoderException
323 * Thrown if URL encoding is unsuccessful.
324 * @see #getDefaultCharset()
325 */
326 @Override
327 public String encode(final String str) throws EncoderException {
328 if (str == null) {
329 return null;
330 }
331 try {
332 return encode(str, getDefaultCharset());
333 } catch (final UnsupportedEncodingException e) {
334 throw new EncoderException(e.getMessage(), e);
335 }
336 }
337
338 /**
339 * Encodes a string into its URL safe form using the specified string charset. Unsafe characters are escaped.
340 *
341 * @param str
342 * string to convert to a URL safe form.
343 * @param charsetName
344 * the charset for str.
345 * @return URL safe string.
346 * @throws UnsupportedEncodingException
347 * Thrown if charset is not supported.
348 */
349 public String encode(final String str, final String charsetName) throws UnsupportedEncodingException {
350 if (str == null) {
351 return null;
352 }
353 return StringUtils.newStringUsAscii(encode(str.getBytes(charsetName)));
354 }
355
356 /**
357 * The default charset used for string decoding and encoding.
358 *
359 * @return The default string charset.
360 */
361 public String getDefaultCharset() {
362 return this.charset;
363 }
364
365 /**
366 * The {@code String} encoding used for decoding and encoding.
367 *
368 * @return The encoding.
369 * @deprecated Use {@link #getDefaultCharset()}, will be removed in 2.0.
370 */
371 @Deprecated
372 public String getEncoding() {
373 return this.charset;
374 }
375
376 }