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 *      http://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.UnsupportedEncodingException;
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023
024import org.apache.commons.codec.CodecPolicy;
025import org.apache.commons.codec.DecoderException;
026import org.apache.commons.codec.EncoderException;
027import org.apache.commons.codec.StringDecoder;
028import org.apache.commons.codec.StringEncoder;
029import org.apache.commons.codec.binary.Base64;
030import org.apache.commons.codec.binary.BaseNCodec;
031
032/**
033 * Identical to the Base64 encoding defined by <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>
034 * and allows a character set to be specified.
035 * <p>
036 * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
037 * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
038 * handling software.
039 * </p>
040 * <p>
041 * This class is immutable and thread-safe.
042 * </p>
043 *
044 * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
045 *          Header Extensions for Non-ASCII Text</a>
046 *
047 * @since 1.3
048 */
049public class BCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
050
051    /**
052     * The default decoding policy.
053     */
054    private static final CodecPolicy DECODING_POLICY_DEFAULT = CodecPolicy.LENIENT;
055
056    /**
057     * The default Charset used for string decoding and encoding.
058     */
059    private final Charset charset;
060
061    /**
062     * If true then decoding should throw an exception for impossible combinations of bits at the
063     * end of the byte input. The default is to decode as much of them as possible.
064     */
065    private final CodecPolicy decodingPolicy;
066
067    /**
068     * Default constructor.
069     */
070    public BCodec() {
071        this(StandardCharsets.UTF_8);
072    }
073
074    /**
075     * Constructor which allows for the selection of a default Charset
076     *
077     * @param charset
078     *            the default string Charset to use.
079     *
080     * @see Charset
081     * @since 1.7
082     */
083    public BCodec(final Charset charset) {
084        this(charset, DECODING_POLICY_DEFAULT);
085    }
086
087    /**
088     * Constructor which allows for the selection of a default Charset.
089     *
090     * @param charset
091     *            the default string Charset to use.
092     * @param decodingPolicy The decoding policy.
093     *
094     * @see Charset
095     * @since 1.15
096     */
097    public BCodec(final Charset charset, final CodecPolicy decodingPolicy) {
098        this.charset = charset;
099        this.decodingPolicy = decodingPolicy;
100    }
101
102    /**
103     * Constructor which allows for the selection of a default Charset
104     *
105     * @param charsetName
106     *            the default Charset to use.
107     * @throws java.nio.charset.UnsupportedCharsetException
108     *             If the named Charset is unavailable
109     * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
110     * @see Charset
111     */
112    public BCodec(final String charsetName) {
113        this(Charset.forName(charsetName));
114    }
115
116    /**
117     * Decodes a Base64 object into its original form. Escaped characters are converted back to their original
118     * representation.
119     *
120     * @param value
121     *            Base64 object to convert into its original form
122     * @return original object
123     * @throws DecoderException
124     *             Thrown if the argument is not a {@code String}. Thrown if a failure condition is encountered
125     *             during the decode process.
126     */
127    @Override
128    public Object decode(final Object value) throws DecoderException {
129        if (value == null) {
130            return null;
131        }
132        if (value instanceof String) {
133            return decode((String) value);
134        }
135        throw new DecoderException("Objects of type " +
136              value.getClass().getName() +
137              " cannot be decoded using BCodec");
138    }
139
140    /**
141     * Decodes a Base64 string into its original form. Escaped characters are converted back to their original
142     * representation.
143     *
144     * @param value
145     *            Base64 string to convert into its original form
146     * @return original string
147     * @throws DecoderException
148     *             A decoder exception is thrown if a failure condition is encountered during the decode process.
149     */
150    @Override
151    public String decode(final String value) throws DecoderException {
152        if (value == null) {
153            return null;
154        }
155        try {
156            return this.decodeText(value);
157        } catch (final UnsupportedEncodingException | IllegalArgumentException e) {
158            throw new DecoderException(e.getMessage(), e);
159        }
160    }
161
162    @Override
163    protected byte[] doDecoding(final byte[] bytes) {
164        if (bytes == null) {
165            return null;
166        }
167        return new Base64(0, BaseNCodec.getChunkSeparator(), false, decodingPolicy).decode(bytes);
168    }
169
170    @Override
171    protected byte[] doEncoding(final byte[] bytes) {
172        if (bytes == null) {
173            return null;
174        }
175        return Base64.encodeBase64(bytes);
176    }
177
178    /**
179     * Encodes an object into its Base64 form using the default Charset. Unsafe characters are escaped.
180     *
181     * @param value
182     *            object to convert to Base64 form
183     * @return Base64 object
184     * @throws EncoderException
185     *             thrown if a failure condition is encountered during the encoding process.
186     */
187    @Override
188    public Object encode(final Object value) throws EncoderException {
189        if (value == null) {
190            return null;
191        }
192        if (value instanceof String) {
193            return encode((String) value);
194        }
195        throw new EncoderException("Objects of type " +
196              value.getClass().getName() +
197              " cannot be encoded using BCodec");
198    }
199
200    /**
201     * Encodes a string into its Base64 form using the default Charset. Unsafe characters are escaped.
202     *
203     * @param strSource
204     *            string to convert to Base64 form
205     * @return Base64 string
206     * @throws EncoderException
207     *             thrown if a failure condition is encountered during the encoding process.
208     */
209    @Override
210    public String encode(final String strSource) throws EncoderException {
211        if (strSource == null) {
212            return null;
213        }
214        return encode(strSource, this.getCharset());
215    }
216
217    /**
218     * Encodes a string into its Base64 form using the specified Charset. Unsafe characters are escaped.
219     *
220     * @param strSource
221     *            string to convert to Base64 form
222     * @param sourceCharset
223     *            the Charset for {@code value}
224     * @return Base64 string
225     * @throws EncoderException
226     *             thrown if a failure condition is encountered during the encoding process.
227     * @since 1.7
228     */
229    public String encode(final String strSource, final Charset sourceCharset) throws EncoderException {
230        if (strSource == null) {
231            return null;
232        }
233        return encodeText(strSource, sourceCharset);
234    }
235
236    /**
237     * Encodes a string into its Base64 form using the specified Charset. Unsafe characters are escaped.
238     *
239     * @param strSource
240     *            string to convert to Base64 form
241     * @param sourceCharset
242     *            the Charset for {@code value}
243     * @return Base64 string
244     * @throws EncoderException
245     *             thrown if a failure condition is encountered during the encoding process.
246     */
247    public String encode(final String strSource, final String sourceCharset) throws EncoderException {
248        if (strSource == null) {
249            return null;
250        }
251        try {
252            return this.encodeText(strSource, sourceCharset);
253        } catch (final UnsupportedEncodingException e) {
254            throw new EncoderException(e.getMessage(), e);
255        }
256    }
257
258    /**
259     * Gets the default Charset name used for string decoding and encoding.
260     *
261     * @return the default Charset name
262     * @since 1.7
263     */
264    public Charset getCharset() {
265        return this.charset;
266    }
267
268    /**
269     * Gets the default Charset name used for string decoding and encoding.
270     *
271     * @return the default Charset name
272     */
273    public String getDefaultCharset() {
274        return this.charset.name();
275    }
276
277    @Override
278    protected String getEncoding() {
279        return "B";
280    }
281
282    /**
283     * Returns true if decoding behavior is strict. Decoding will raise a
284     * {@link DecoderException} if trailing bits are not part of a valid Base64 encoding.
285     *
286     * <p>The default is false for lenient encoding. Decoding will compose trailing bits
287     * into 8-bit bytes and discard the remainder.
288     *
289     * @return true if using strict decoding
290     * @since 1.15
291     */
292    public boolean isStrictDecoding() {
293        return decodingPolicy == CodecPolicy.STRICT;
294    }
295}