001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.codec.net;
019
020import java.io.ByteArrayOutputStream;
021import java.io.UnsupportedEncodingException;
022import java.net.URLDecoder;
023import java.net.URLEncoder;
024import java.util.BitSet;
025
026import org.apache.commons.codec.BinaryDecoder;
027import org.apache.commons.codec.BinaryEncoder;
028import org.apache.commons.codec.CharEncoding;
029import org.apache.commons.codec.DecoderException;
030import org.apache.commons.codec.EncoderException;
031import org.apache.commons.codec.StringDecoder;
032import org.apache.commons.codec.StringEncoder;
033import org.apache.commons.codec.binary.StringUtils;
034
035/**
036 * Implements the 'www-form-urlencoded' encoding scheme, also misleadingly known as URL encoding.
037 * <p>
038 * This codec is meant to be a replacement for standard Java classes {@link URLEncoder} and
039 * {@link URLDecoder} on older Java platforms, as these classes in Java versions below
040 * 1.4 rely on the platform's default charset encoding.
041 * </p>
042 * <p>
043 * This class is thread-safe as of 1.11
044 * </p>
045 *
046 * @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>
047 *           of the <a href="https://www.w3.org/TR/html4/">HTML 4.01 Specification</a>
048 *
049 * @since 1.2
050 */
051public class URLCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
052
053    /**
054     * Release 1.5 made this field final.
055     */
056    protected static final byte ESCAPE_CHAR = '%';
057
058    private static final byte PLUS_CHAR = '+';
059
060    /**
061     * BitSet of www-form-url safe characters.
062     * This is a copy of the internal BitSet which is now used for the conversion.
063     * Changes to this field are ignored.
064     *
065     * @deprecated 1.11 Will be removed in 2.0 (CODEC-230)
066     */
067    @Deprecated
068    protected static final BitSet WWW_FORM_URL;
069
070    private static final BitSet WWW_FORM_URL_SAFE = new BitSet(256);
071
072    // Static initializer for www_form_url
073    static {
074        // alpha characters
075        for (int i = 'a'; i <= 'z'; i++) {
076            WWW_FORM_URL_SAFE.set(i);
077        }
078        for (int i = 'A'; i <= 'Z'; i++) {
079            WWW_FORM_URL_SAFE.set(i);
080        }
081        // numeric characters
082        for (int i = '0'; i <= '9'; i++) {
083            WWW_FORM_URL_SAFE.set(i);
084        }
085        // special chars
086        WWW_FORM_URL_SAFE.set('-');
087        WWW_FORM_URL_SAFE.set('_');
088        WWW_FORM_URL_SAFE.set('.');
089        WWW_FORM_URL_SAFE.set('*');
090        // blank to be replaced with +
091        WWW_FORM_URL_SAFE.set(' ');
092
093        // Create a copy in case anyone (ab)uses it
094        WWW_FORM_URL = (BitSet) WWW_FORM_URL_SAFE.clone();
095    }
096
097    /**
098     * Decodes an array of bytes using the safe set supplied to {@link #encodeUrl(BitSet, byte[])}.
099     * <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}