View Javadoc
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 }