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 bytes using the safe set supplied to {@link #encodeUrl(BitSet, byte[])}.
99 * <p>
100 * A percent sign marked safe is copied literally; otherwise it starts a two-digit hexadecimal escape. A plus sign marked safe is copied literally.
101 * Otherwise, a plus sign becomes a space only if space is marked safe. All other bytes are copied unchanged. A {@code null} bitset selects the default
102 * {@code www-form-urlencoded} safe set, giving the same behavior as {@link #decodeUrl(byte[])}.
103 * </p>
104 * <p>
105 * Not every safe set permits a round trip. If both space and plus are marked safe, the encoder maps both to plus and this method preserves that plus. If
106 * percent is marked safe, literal percent signs cannot be distinguished from generated escapes, so this method preserves all percent signs, including
107 * generated escapes. Use a safe set that excludes percent and does not mark both space and plus safe when a round trip is required.
108 * </p>
109 *
110 * @param urlsafe bitset of characters deemed URL safe during encoding, or {@code null} to use the default safe set.
111 * @param bytes array of encoded bytes, or {@code null}.
112 * @return array of decoded bytes, or {@code null} if the input is {@code null}.
113 * @throws DecoderException if percent is not marked safe and an escape is incomplete or contains invalid hexadecimal digits.
114 * @since 1.23.0
115 */
116 public static final byte[] decodeUrl(BitSet urlsafe, final byte[] bytes) throws DecoderException {
117 if (bytes == null) {
118 return null;
119 }
120 if (urlsafe == null) {
121 urlsafe = WWW_FORM_URL_SAFE;
122 }
123 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
124 for (int i = 0; i < bytes.length; i++) {
125 final int b = bytes[i];
126 if (b == PLUS_CHAR && !urlsafe.get(PLUS_CHAR) && urlsafe.get(' ')) {
127 buffer.write(' ');
128 } else if (b == ESCAPE_CHAR && !urlsafe.get(ESCAPE_CHAR)) {
129 try {
130 final int u = Utils.digit16(bytes[++i]);
131 final int l = Utils.digit16(bytes[++i]);
132 buffer.write((char) ((u << 4) + l));
133 } catch (final ArrayIndexOutOfBoundsException e) {
134 throw new DecoderException("Invalid URL encoding: ", e);
135 }
136 } else {
137 buffer.write(b);
138 }
139 }
140 return buffer.toByteArray();
141 }
142
143 /**
144 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted
145 * back to their original representation.
146 *
147 * <p>
148 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape.
149 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a
150 * {@link DecoderException}, depending on which characters were marked safe.
151 * </p>
152 *
153 * @param bytes
154 * array of URL safe characters.
155 * @return array of original bytes.
156 * @throws DecoderException
157 * Thrown if URL decoding is unsuccessful.
158 */
159 public static final byte[] decodeUrl(final byte[] bytes) throws DecoderException {
160 return decodeUrl(null, bytes);
161 }
162
163 /**
164 * Encodes an array of bytes using the given set of URL safe characters.
165 * <p>
166 * Unsafe characters are percent-escaped. Characters marked safe are copied unchanged, except that a space marked safe is converted to {@code +}. A
167 * {@code null} bitset selects the default {@code www-form-urlencoded} safe set, which escapes both {@code %} and {@code +}.
168 * </p>
169 * <p>
170 * A custom bitset can produce output that {@link #decodeUrl(byte[])} and the {@code decode} methods cannot decode back to the original input. These
171 * decoders always convert {@code +} to a space and interpret {@code %} as the start of a hexadecimal escape, regardless of the bitset used for encoding. If
172 * the custom bitset marks either character safe, decoding can change the original data or throw {@link DecoderException}. Callers using a custom bitset
173 * can use {@link #decodeUrl(BitSet, byte[])} with the same bitset, subject to its documented limitations for ambiguous safe sets.
174 * </p>
175 *
176 * @param urlsafe bitset of characters deemed URL safe, or {@code null} to use the default {@code www-form-urlencoded} safe set.
177 * @param bytes array of bytes to convert to URL safe characters.
178 * @return array of bytes containing URL safe characters.
179 */
180 public static final byte[] encodeUrl(BitSet urlsafe, final byte[] bytes) {
181 if (bytes == null) {
182 return null;
183 }
184 if (urlsafe == null) {
185 urlsafe = WWW_FORM_URL_SAFE;
186 }
187
188 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
189 for (final byte c : bytes) {
190 int b = c;
191 if (b < 0) {
192 b = 256 + b;
193 }
194 if (urlsafe.get(b)) {
195 if (b == ' ') {
196 b = PLUS_CHAR;
197 }
198 buffer.write(b);
199 } else {
200 buffer.write(ESCAPE_CHAR);
201 final char hex1 = Utils.hexChar(b >> 4);
202 final char hex2 = Utils.hexChar(b);
203 buffer.write(hex1);
204 buffer.write(hex2);
205 }
206 }
207 return buffer.toByteArray();
208 }
209
210 /**
211 * The default charset used for string decoding and encoding.
212 *
213 * @deprecated TODO: This field will be changed to a private final Charset in 2.0. (CODEC-126)
214 */
215 @Deprecated
216 protected volatile String charset; // added volatile: see CODEC-232
217
218 /**
219 * Default constructor.
220 */
221 public URLCodec() {
222 this(CharEncoding.UTF_8);
223 }
224
225 /**
226 * Constructs a new instance for the selection of a default charset.
227 *
228 * @param charset The default string charset to use.
229 */
230 public URLCodec(final String charset) {
231 this.charset = charset;
232 }
233
234 /**
235 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted
236 * back to their original representation.
237 *
238 * <p>
239 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape.
240 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a
241 * {@link DecoderException}, depending on which characters were marked safe.
242 * </p>
243 *
244 * @param bytes
245 * array of URL safe characters.
246 * @return array of original bytes.
247 * @throws DecoderException
248 * Thrown if URL decoding is unsuccessful.
249 */
250 @Override
251 public byte[] decode(final byte[] bytes) throws DecoderException {
252 return decodeUrl(bytes);
253 }
254
255 /**
256 * Decodes a URL safe object into its original form. Escaped characters are converted back to their original
257 * representation.
258 *
259 * <p>
260 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape.
261 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a
262 * {@link DecoderException}, depending on which characters were marked safe.
263 * </p>
264 *
265 * @param obj
266 * URL safe object to convert into its original form.
267 * @return original object.
268 * @throws DecoderException
269 * Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure
270 * condition is encountered during the decode process.
271 */
272 @Override
273 public Object decode(final Object obj) throws DecoderException {
274 if (obj == null) {
275 return null;
276 }
277 if (obj instanceof byte[]) {
278 return decode((byte[]) obj);
279 }
280 if (obj instanceof String) {
281 return decode((String) obj);
282 }
283 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be URL decoded");
284 }
285
286 /**
287 * Decodes a URL safe string into its original form using the default string charset. Escaped characters are
288 * converted back to their original representation.
289 *
290 * <p>
291 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape.
292 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a
293 * {@link DecoderException}, depending on which characters were marked safe.
294 * </p>
295 *
296 * @param str
297 * URL safe string to convert into its original form.
298 * @return original string.
299 * @throws DecoderException
300 * Thrown if URL decoding is unsuccessful.
301 * @see #getDefaultCharset()
302 */
303 @Override
304 public String decode(final String str) throws DecoderException {
305 if (str == null) {
306 return null;
307 }
308 try {
309 return decode(str, getDefaultCharset());
310 } catch (final UnsupportedEncodingException e) {
311 throw new DecoderException(e.getMessage(), e);
312 }
313 }
314
315 /**
316 * Decodes a URL safe string into its original form using the specified encoding. Escaped characters are converted
317 * back to their original representation.
318 *
319 * <p>
320 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape.
321 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a
322 * {@link DecoderException}, depending on which characters were marked safe.
323 * </p>
324 *
325 * @param str
326 * URL safe string to convert into its original form.
327 * @param charsetName
328 * the original string charset.
329 * @return original string.
330 * @throws DecoderException
331 * Thrown if URL decoding is unsuccessful.
332 * @throws UnsupportedEncodingException
333 * Thrown if charset is not supported.
334 */
335 public String decode(final String str, final String charsetName)
336 throws DecoderException, UnsupportedEncodingException {
337 if (str == null) {
338 return null;
339 }
340 return new String(decode(StringUtils.getBytesUsAscii(str)), charsetName);
341 }
342
343 /**
344 * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped.
345 *
346 * @param bytes
347 * array of bytes to convert to URL safe characters.
348 * @return array of bytes containing URL safe characters.
349 */
350 @Override
351 public byte[] encode(final byte[] bytes) {
352 return encodeUrl(WWW_FORM_URL_SAFE, bytes);
353 }
354
355 /**
356 * Encodes an object into its URL safe form. Unsafe characters are escaped.
357 *
358 * @param obj
359 * string to convert to a URL safe form.
360 * @return URL safe object.
361 * @throws EncoderException
362 * Thrown if URL encoding is not applicable to objects of this type or if encoding is unsuccessful.
363 */
364 @Override
365 public Object encode(final Object obj) throws EncoderException {
366 if (obj == null) {
367 return null;
368 }
369 if (obj instanceof byte[]) {
370 return encode((byte[]) obj);
371 }
372 if (obj instanceof String) {
373 return encode((String) obj);
374 }
375 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be URL encoded");
376 }
377
378 /**
379 * Encodes a string into its URL safe form using the default string charset. Unsafe characters are escaped.
380 *
381 * @param str
382 * string to convert to a URL safe form.
383 * @return URL safe string.
384 * @throws EncoderException
385 * Thrown if URL encoding is unsuccessful.
386 * @see #getDefaultCharset()
387 */
388 @Override
389 public String encode(final String str) throws EncoderException {
390 if (str == null) {
391 return null;
392 }
393 try {
394 return encode(str, getDefaultCharset());
395 } catch (final UnsupportedEncodingException e) {
396 throw new EncoderException(e.getMessage(), e);
397 }
398 }
399
400 /**
401 * Encodes a string into its URL safe form using the specified string charset. Unsafe characters are escaped.
402 *
403 * @param str
404 * string to convert to a URL safe form.
405 * @param charsetName
406 * the charset for str.
407 * @return URL safe string.
408 * @throws UnsupportedEncodingException
409 * Thrown if charset is not supported.
410 */
411 public String encode(final String str, final String charsetName) throws UnsupportedEncodingException {
412 if (str == null) {
413 return null;
414 }
415 return StringUtils.newStringUsAscii(encode(str.getBytes(charsetName)));
416 }
417
418 /**
419 * Gets the default charset used for string decoding and encoding.
420 *
421 * @return The default string charset.
422 */
423 public String getDefaultCharset() {
424 return this.charset;
425 }
426
427 /**
428 * Gets the {@code String} encoding used for decoding and encoding.
429 *
430 * @return The encoding.
431 * @deprecated Use {@link #getDefaultCharset()}, will be removed in 2.0.
432 */
433 @Deprecated
434 public String getEncoding() {
435 return this.charset;
436 }
437
438 }