Source code
Description: source: package: org.apache.commons.codec.net, class: PercentCodec

    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.nio.ByteBuffer;
    021import java.util.BitSet;
    022
    023import org.apache.commons.codec.BinaryDecoder;
    024import org.apache.commons.codec.BinaryEncoder;
    025import org.apache.commons.codec.DecoderException;
    026import org.apache.commons.codec.EncoderException;
    027
    028/**
    029 * Implements the Percent-Encoding scheme, as described in HTTP 1.1 specification. For extensibility, an array of
    030 * special US-ASCII characters can be specified in order to perform proper URI encoding for the different parts
    031 * of the URI.
    032 * <p>
    033 * This class is immutable. It is also thread-safe besides using BitSet which is not thread-safe, but its public
    034 * interface only call the access
    035 * </p>
    036 *
    037 * @see <a href="https://tools.ietf.org/html/rfc3986#section-2.1">Percent-Encoding</a>
    038 * @since 1.12
    039 */
    040public class PercentCodec implements BinaryEncoder, BinaryDecoder {
    041
    042    /**
    043     * The escape character used by the Percent-Encoding in order to introduce an encoded character.
    044     */
    045    private static final byte ESCAPE_CHAR = '%';
    046
    047    /**
    048     * The plus character used to encode spaces when plusForSpace is true.
    049     */
    050    private static final byte PLUS_CHAR = '+';
    051
    052    /**
    053     * The bit set used to store the character that should be always encoded.
    054     */
    055    private final BitSet alwaysEncodeChars = new BitSet();
    056
    057    /**
    058     * The flag defining if the space character should be encoded as '+'.
    059     */
    060    private final boolean plusForSpace;
    061
    062    /**
    063     * The minimum and maximum code of the bytes that is inserted in the bit set, used to prevent look-ups
    064     */
    065    private int alwaysEncodeCharsMin = Integer.MAX_VALUE, alwaysEncodeCharsMax = Integer.MIN_VALUE;
    066
    067    /**
    068     * Constructs a Percent coded that will encode all the non US-ASCII characters using the Percent-Encoding
    069     * while it will not encode all the US-ASCII characters, except for character '%' that is used as escape
    070     * character for Percent-Encoding.
    071     */
    072    public PercentCodec() {
    073        this.plusForSpace = false;
    074        insertAlwaysEncodeChar(ESCAPE_CHAR);
    075    }
    076
    077    /**
    078     * Constructs a Percent codec by specifying the characters that belong to US-ASCII that should
    079     * always be encoded. The rest US-ASCII characters will not be encoded, except for character '%' that
    080     * is used as escape character for Percent-Encoding.
    081     *
    082     * @param alwaysEncodeChars The unsafe characters that should always be encoded.
    083     * @param plusForSpace      The flag defining if the space character should be encoded as '+'.
    084     */
    085    public PercentCodec(final byte[] alwaysEncodeChars, final boolean plusForSpace) {
    086        this.plusForSpace = plusForSpace;
    087        insertAlwaysEncodeChars(alwaysEncodeChars);
    088        if (plusForSpace) {
    089            insertAlwaysEncodeChar(PLUS_CHAR);
    090        }
    091    }
    092
    093    private boolean canEncode(final byte c) {
    094        return !isAsciiChar(c) || inAlwaysEncodeCharsRange(c) && alwaysEncodeChars.get(c);
    095    }
    096
    097    private boolean containsSpace(final byte[] bytes) {
    098        for (final byte b : bytes) {
    099            if (b == ' ') {
    100                return true;
    101            }
    102        }
    103        return false;
    104    }
    105
    106    /**
    107     * Decodes bytes encoded with Percent-Encoding based on RFC 3986. The reverse process is performed in order to
    108     * decode the encoded characters to Unicode.
    109     */
    110    @Override
    111    public byte[] decode(final byte[] bytes) throws DecoderException {
    112        if (bytes == null) {
    113            return null;
    114        }
    115        final ByteBuffer buffer = ByteBuffer.allocate(expectedDecodingBytes(bytes));
    116        for (int i = 0; i < bytes.length; i++) {
    117            final byte b = bytes[i];
    118            if (b == ESCAPE_CHAR) {
    119                try {
    120                    final int u = Utils.digit16(bytes[++i]);
    121                    final int l = Utils.digit16(bytes[++i]);
    122                    buffer.put((byte) ((u << 4) + l));
    123                } catch (final ArrayIndexOutOfBoundsException e) {
    124                    throw new DecoderException("Invalid percent decoding: ", e);
    125                }
    126            } else if (plusForSpace && b == '+') {
    127                buffer.put((byte) ' ');
    128            } else {
    129                buffer.put(b);
    130            }
    131        }
    132        return buffer.array();
    133    }
    134
    135    /**
    136     * Decodes a byte[] Object, whose bytes are encoded with Percent-Encoding.
    137     *
    138     * @param obj The object to decode.
    139     * @return The decoding result byte[] as Object.
    140     * @throws DecoderException Thrown if the object is not a byte array.
    141     */
    142    @Override
    143    public Object decode(final Object obj) throws DecoderException {
    144        if (obj == null) {
    145            return null;
    146        }
    147        if (obj instanceof byte[]) {
    148            return decode((byte[]) obj);
    149        }
    150        throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be Percent decoded");
    151    }
    152
    153    private byte[] doEncode(final byte[] bytes, final int expectedLength, final boolean willEncode) {
    154        final ByteBuffer buffer = ByteBuffer.allocate(expectedLength);
    155        for (final byte b : bytes) {
    156            if (willEncode && canEncode(b)) {
    157                byte bb = b;
    158                if (bb < 0) {
    159                    bb = (byte) (256 + bb);
    160                }
    161                final char hex1 = Utils.hexChar(bb >> 4);
    162                final char hex2 = Utils.hexChar(bb);
    163                buffer.put(ESCAPE_CHAR);
    164                buffer.put((byte) hex1);
    165                buffer.put((byte) hex2);
    166            } else if (plusForSpace && b == ' ') {
    167                buffer.put((byte) '+');
    168            } else {
    169                buffer.put(b);
    170            }
    171        }
    172        return buffer.array();
    173    }
    174
    175    /**
    176     * Percent-Encoding based on RFC 3986. The non US-ASCII characters are encoded, as well as the
    177     * US-ASCII characters that are configured to be always encoded.
    178     */
    179    @Override
    180    public byte[] encode(final byte[] bytes) throws EncoderException {
    181        if (bytes == null) {
    182            return null;
    183        }
    184        final int expectedEncodingBytes = expectedEncodingBytes(bytes);
    185        final boolean willEncode = expectedEncodingBytes != bytes.length;
    186        if (willEncode || plusForSpace && containsSpace(bytes)) {
    187            return doEncode(bytes, expectedEncodingBytes, willEncode);
    188        }
    189        return bytes;
    190    }
    191
    192    /**
    193     * Encodes an object into using the Percent-Encoding. Only byte[] objects are accepted.
    194     *
    195     * @param obj The object to encode.
    196     * @return The encoding result byte[] as Object.
    197     * @throws EncoderException Thrown if the object is not a byte array.
    198     */
    199    @Override
    200    public Object encode(final Object obj) throws EncoderException {
    201        if (obj == null) {
    202            return null;
    203        }
    204        if (obj instanceof byte[]) {
    205            return encode((byte[]) obj);
    206        }
    207        throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be Percent encoded");
    208    }
    209
    210    private int expectedDecodingBytes(final byte[] bytes) {
    211        int byteCount = 0;
    212        for (int i = 0; i < bytes.length;) {
    213            final byte b = bytes[i];
    214            i += b == ESCAPE_CHAR ? 3 : 1;
    215            byteCount++;
    216        }
    217        return byteCount;
    218    }
    219
    220    private int expectedEncodingBytes(final byte[] bytes) {
    221        int byteCount = 0;
    222        for (final byte b : bytes) {
    223            byteCount += canEncode(b) ? 3 : 1;
    224        }
    225        return byteCount;
    226    }
    227
    228    private boolean inAlwaysEncodeCharsRange(final byte c) {
    229        return c >= alwaysEncodeCharsMin && c <= alwaysEncodeCharsMax;
    230    }
    231
    232    /**
    233     * Inserts a single character into a BitSet and maintains the min and max of the characters of the
    234     * {@code BitSet alwaysEncodeChars} in order to avoid look-ups when a byte is out of this range.
    235     *
    236     * @param b The byte that is candidate for min and max limit.
    237     */
    238    private void insertAlwaysEncodeChar(final byte b) {
    239        if (b < 0) {
    240            throw new IllegalArgumentException("byte must be >= 0");
    241        }
    242        this.alwaysEncodeChars.set(b);
    243        if (b < alwaysEncodeCharsMin) {
    244            alwaysEncodeCharsMin = b;
    245        }
    246        if (b > alwaysEncodeCharsMax) {
    247            alwaysEncodeCharsMax = b;
    248        }
    249    }
    250
    251    /**
    252     * Inserts the byte array into a BitSet for faster lookup.
    253     *
    254     * @param alwaysEncodeCharsArray The byte array into a BitSet for faster lookup.
    255     */
    256    private void insertAlwaysEncodeChars(final byte[] alwaysEncodeCharsArray) {
    257        if (alwaysEncodeCharsArray != null) {
    258            for (final byte b : alwaysEncodeCharsArray) {
    259                insertAlwaysEncodeChar(b);
    260            }
    261        }
    262        insertAlwaysEncodeChar(ESCAPE_CHAR);
    263    }
    264
    265    private boolean isAsciiChar(final byte c) {
    266        return c >= 0;
    267    }
    268}