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 URL safe 7-bit characters into an array of original bytes. Escaped characters are converted
099     * 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}