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.nio.charset.Charset;
023import java.nio.charset.IllegalCharsetNameException;
024import java.nio.charset.StandardCharsets;
025import java.nio.charset.UnsupportedCharsetException;
026import java.util.BitSet;
027
028import org.apache.commons.codec.BinaryDecoder;
029import org.apache.commons.codec.BinaryEncoder;
030import org.apache.commons.codec.DecoderException;
031import org.apache.commons.codec.EncoderException;
032import org.apache.commons.codec.StringDecoder;
033import org.apache.commons.codec.StringEncoder;
034import org.apache.commons.codec.binary.StringUtils;
035
036/**
037 * Codec for the Quoted-Printable section of <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>.
038 * <p>
039 * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to printable characters in the ASCII character
040 * set. It encodes the data in such a way that the resulting octets are unlikely to be modified by mail transport. If the data being encoded are mostly ASCII
041 * text, the encoded form of the data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable to ensure
042 * the integrity of the data should the message pass through a character- translating, and/or line-wrapping gateway.
043 * </p>
044 * <p>
045 * Note:
046 * </p>
047 * <p>
048 * Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the quoted-printable spec:
049 * </p>
050 * <ul>
051 * <li>{@code strict=false}: only rules #1 and #2 are implemented</li>
052 * <li>{@code strict=true}: all rules #1 through #5 are implemented</li>
053 * </ul>
054 * <p>
055 * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used for certain applications that do not
056 * require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec. The strict mode has been added in 1.10.
057 * </p>
058 * <p>
059 * This class is immutable and thread-safe.
060 * </p>
061 *
062 * @see <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing
063 *      the Format of Internet Message Bodies </a>
064 *
065 * @since 1.3
066 */
067public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
068
069    /**
070     * BitSet of printable characters as defined in RFC 1521.
071     */
072    private static final BitSet PRINTABLE_CHARS = new BitSet(256);
073    private static final byte ESCAPE_CHAR = '=';
074    private static final byte TAB = 9;
075    private static final byte CR = 13;
076    private static final byte LF = 10;
077
078    /**
079     * Minimum length required for the byte arrays used by encodeQuotedPrintable method.
080     */
081    private static final int MIN_BYTES = 3;
082
083    /**
084     * Safe line length for quoted printable encoded text.
085     */
086    private static final int SAFE_LENGTH = 73;
087
088    // Static initializer for printable chars collection
089    static {
090        // alpha characters
091        for (int i = 33; i <= 60; i++) {
092            PRINTABLE_CHARS.set(i);
093        }
094        for (int i = 62; i <= 126; i++) {
095            PRINTABLE_CHARS.set(i);
096        }
097        PRINTABLE_CHARS.set(TAB);
098        PRINTABLE_CHARS.set(Utils.SPACE);
099    }
100
101    /**
102     * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation.
103     * <p>
104     * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521.
105     * </p>
106     *
107     * @param bytes array of quoted-printable characters.
108     * @return array of original bytes.
109     * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful.
110     */
111    public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException {
112        if (bytes == null) {
113            return null;
114        }
115        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
116        for (int i = 0; i < bytes.length; i++) {
117            final int b = bytes[i];
118            if (b == ESCAPE_CHAR) {
119                try {
120                    // if the next octet is a CR we have found a soft line break
121                    if (bytes[++i] == CR) {
122                        continue;
123                    }
124                    final int u = Utils.digit16(bytes[i]);
125                    final int l = Utils.digit16(bytes[++i]);
126                    buffer.write((char) ((u << 4) + l));
127                } catch (final ArrayIndexOutOfBoundsException e) {
128                    throw new DecoderException("Invalid quoted-printable encoding", e);
129                }
130            } else if (b != CR && b != LF) {
131                // every other octet is appended except for CR & LF
132                buffer.write(b);
133            }
134        }
135        return buffer.toByteArray();
136    }
137
138    /**
139     * Encodes a byte in the buffer.
140     *
141     * @param b      byte to write.
142     * @param encode indicates whether the octet shall be encoded.
143     * @param buffer The buffer to write to.
144     * @return The number of bytes that have been written to the buffer.
145     */
146    private static int encodeByte(final int b, final boolean encode, final ByteArrayOutputStream buffer) {
147        if (encode) {
148            return encodeQuotedPrintable(b, buffer);
149        }
150        buffer.write(b);
151        return 1;
152    }
153
154    /**
155     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
156     * <p>
157     * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding
158     * binary data and unformatted text.
159     * </p>
160     *
161     * @param printable bitset of characters deemed quoted-printable.
162     * @param bytes     array of bytes to be encoded.
163     * @return array of bytes containing quoted-printable data.
164     */
165    public static final byte[] encodeQuotedPrintable(final BitSet printable, final byte[] bytes) {
166        return encodeQuotedPrintable(printable, bytes, false);
167    }
168
169    /**
170     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
171     * <p>
172     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
173     * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
174     * </p>
175     *
176     * @param printable bitset of characters deemed quoted-printable.
177     * @param bytes     array of bytes to be encoded.
178     * @param strict    if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2.
179     * @return array of bytes containing quoted-printable data.
180     * @since 1.10
181     */
182    public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, final boolean strict) {
183        if (bytes == null) {
184            return null;
185        }
186        if (printable == null) {
187            printable = PRINTABLE_CHARS;
188        }
189        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
190        final int bytesLength = bytes.length;
191        if (strict) {
192            if (bytesLength < MIN_BYTES) {
193                return null;
194            }
195            int pos = 1;
196            // encode up to buffer.length - 3, the last three octets will be treated
197            // separately for simplification of note #3
198            for (int i = 0; i < bytesLength - 3; i++) {
199                final int b = getUnsignedOctet(i, bytes);
200                if (pos < SAFE_LENGTH) {
201                    // up to this length it is safe to add any byte, encoded or not
202                    pos += encodeByte(b, !printable.get(b), buffer);
203                } else {
204                    // rule #3: whitespace at the end of a line *must* be encoded
205                    encodeByte(b, !printable.get(b) || isWhitespace(b), buffer);
206                    // rule #5: soft line break
207                    buffer.write(ESCAPE_CHAR);
208                    buffer.write(CR);
209                    buffer.write(LF);
210                    pos = 1;
211                }
212            }
213            // rule #3: whitespace at the end of a line *must* be encoded
214            // if we would do a soft break line after this octet, encode whitespace
215            int b = getUnsignedOctet(bytesLength - 3, bytes);
216            boolean encode = !printable.get(b) || isWhitespace(b) && pos > SAFE_LENGTH - 5;
217            pos += encodeByte(b, encode, buffer);
218            // note #3: '=' *must not* be the ultimate or penultimate character
219            // simplification: if < 6 bytes left, do a soft line break as we may need
220            // exactly 6 bytes space for the last 2 bytes
221            if (pos > SAFE_LENGTH - 2) {
222                buffer.write(ESCAPE_CHAR);
223                buffer.write(CR);
224                buffer.write(LF);
225            }
226            for (int i = bytesLength - 2; i < bytesLength; i++) {
227                b = getUnsignedOctet(i, bytes);
228                // rule #3: trailing whitespace shall be encoded
229                encode = !printable.get(b) || i > bytesLength - 2 && isWhitespace(b);
230                encodeByte(b, encode, buffer);
231            }
232        } else {
233            for (final byte c : bytes) {
234                int b = c;
235                if (b < 0) {
236                    b = 256 + b;
237                }
238                if (printable.get(b)) {
239                    buffer.write(b);
240                } else {
241                    encodeQuotedPrintable(b, buffer);
242                }
243            }
244        }
245        return buffer.toByteArray();
246    }
247
248    /**
249     * Encodes byte into its quoted-printable representation.
250     *
251     * @param b      byte to encode.
252     * @param buffer The buffer to write to.
253     * @return The number of bytes written to the {@code buffer}.
254     */
255    private static int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) {
256        buffer.write(ESCAPE_CHAR);
257        final char hex1 = Utils.hexChar(b >> 4);
258        final char hex2 = Utils.hexChar(b);
259        buffer.write(hex1);
260        buffer.write(hex2);
261        return 3;
262    }
263
264    /**
265     * Gets the byte at position {@code index} of the byte array and make sure it is unsigned.
266     *
267     * @param index position in the array.
268     * @param bytes The byte array.
269     * @return The unsigned octet at position {@code index} from the array.
270     */
271    private static int getUnsignedOctet(final int index, final byte[] bytes) {
272        int b = bytes[index];
273        if (b < 0) {
274            b = 256 + b;
275        }
276        return b;
277    }
278
279    /**
280     * Checks whether the given byte is whitespace.
281     *
282     * @param b byte to be checked.
283     * @return {@code true} if the byte is either a space or tab character.
284     */
285    private static boolean isWhitespace(final int b) {
286        return b == Utils.SPACE || b == TAB;
287    }
288
289    /**
290     * The default Charset used for string decoding and encoding.
291     */
292    private final Charset charset;
293
294    /**
295     * Indicates whether soft line breaks shall be used during encoding (rule #3-5).
296     */
297    private final boolean strict;
298
299    /**
300     * Constructs a new instance, assumes default Charset of {@link StandardCharsets#UTF_8}
301     */
302    public QuotedPrintableCodec() {
303        this(StandardCharsets.UTF_8, false);
304    }
305
306    /**
307     * Constructs a new instance for the selection of the strict mode.
308     *
309     * @param strict if {@code true}, soft line breaks will be used.
310     * @since 1.10
311     */
312    public QuotedPrintableCodec(final boolean strict) {
313        this(StandardCharsets.UTF_8, strict);
314    }
315
316    /**
317     * Constructs a new instance for the selection of a default Charset.
318     *
319     * @param charset The default string Charset to use.
320     * @since 1.7
321     */
322    public QuotedPrintableCodec(final Charset charset) {
323        this(charset, false);
324    }
325
326    /**
327     * Constructs a new instance for the selection of a default Charset and strict mode.
328     *
329     * @param charset The default string Charset to use.
330     * @param strict  if {@code true}, soft line breaks will be used.
331     * @since 1.10
332     */
333    public QuotedPrintableCodec(final Charset charset, final boolean strict) {
334        this.charset = charset;
335        this.strict = strict;
336    }
337
338    /**
339     * Constructs a new instance for the selection of a default Charset.
340     *
341     * @param charsetName The default string Charset to use.
342     * @throws UnsupportedCharsetException If no support for the named Charset is available in this instance of the Java virtual machine.
343     * @throws IllegalArgumentException    If the given charsetName is null.
344     * @throws IllegalCharsetNameException If the given Charset name is illegal.
345     *
346     * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
347     */
348    public QuotedPrintableCodec(final String charsetName) throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException {
349        this(Charset.forName(charsetName), false);
350    }
351
352    /**
353     * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation.
354     * <p>
355     * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521.
356     * </p>
357     *
358     * @param bytes array of quoted-printable characters.
359     * @return array of original bytes.
360     * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful.
361     */
362    @Override
363    public byte[] decode(final byte[] bytes) throws DecoderException {
364        return decodeQuotedPrintable(bytes);
365    }
366
367    /**
368     * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original representation.
369     *
370     * @param obj quoted-printable object to convert into its original form.
371     * @return original object.
372     * @throws DecoderException Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure condition is encountered during the decode
373     *                          process.
374     */
375    @Override
376    public Object decode(final Object obj) throws DecoderException {
377        if (obj == null) {
378            return null;
379        }
380        if (obj instanceof byte[]) {
381            return decode((byte[]) obj);
382        }
383        if (obj instanceof String) {
384            return decode((String) obj);
385        }
386        throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable decoded");
387    }
388
389    /**
390     * Decodes a quoted-printable string into its original form using the default string Charset. Escaped characters are converted back to their original
391     * representation.
392     *
393     * @param sourceStr quoted-printable string to convert into its original form.
394     * @return original string.
395     * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. Thrown if Charset is not supported.
396     * @see #getCharset()
397     */
398    @Override
399    public String decode(final String sourceStr) throws DecoderException {
400        return this.decode(sourceStr, getCharset());
401    }
402
403    /**
404     * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original
405     * representation.
406     *
407     * @param sourceStr     quoted-printable string to convert into its original form.
408     * @param sourceCharset The original string Charset.
409     * @return original string.
410     * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful.
411     * @since 1.7
412     */
413    public String decode(final String sourceStr, final Charset sourceCharset) throws DecoderException {
414        if (sourceStr == null) {
415            return null;
416        }
417        return new String(this.decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
418    }
419
420    /**
421     * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original
422     * representation.
423     *
424     * @param sourceStr     quoted-printable string to convert into its original form.
425     * @param sourceCharset The original string Charset.
426     * @return original string.
427     * @throws DecoderException             Thrown if quoted-printable decoding is unsuccessful.
428     * @throws UnsupportedEncodingException Thrown if Charset is not supported.
429     */
430    public String decode(final String sourceStr, final String sourceCharset) throws DecoderException, UnsupportedEncodingException {
431        if (sourceStr == null) {
432            return null;
433        }
434        return new String(decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
435    }
436
437    /**
438     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
439     * <p>
440     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
441     * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
442     * </p>
443     *
444     * @param bytes array of bytes to be encoded.
445     * @return array of bytes containing quoted-printable data.
446     */
447    @Override
448    public byte[] encode(final byte[] bytes) {
449        return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict);
450    }
451
452    /**
453     * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped.
454     *
455     * @param obj string to convert to a quoted-printable form.
456     * @return quoted-printable object.
457     * @throws EncoderException Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is unsuccessful.
458     */
459    @Override
460    public Object encode(final Object obj) throws EncoderException {
461        if (obj == null) {
462            return null;
463        }
464        if (obj instanceof byte[]) {
465            return encode((byte[]) obj);
466        }
467        if (obj instanceof String) {
468            return encode((String) obj);
469        }
470        throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable encoded");
471    }
472
473    /**
474     * Encodes a string into its quoted-printable form using the default string Charset. Unsafe characters are escaped.
475     * <p>
476     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
477     * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
478     * </p>
479     *
480     * @param sourceStr string to convert to quoted-printable form.
481     * @return quoted-printable string.
482     * @throws EncoderException Thrown if quoted-printable encoding is unsuccessful.
483     *
484     * @see #getCharset()
485     */
486    @Override
487    public String encode(final String sourceStr) throws EncoderException {
488        return encode(sourceStr, getCharset());
489    }
490
491    /**
492     * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
493     * <p>
494     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
495     * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
496     * </p>
497     *
498     * @param sourceStr     string to convert to quoted-printable form.
499     * @param sourceCharset The Charset for sourceStr.
500     * @return quoted-printable string.
501     * @since 1.7
502     */
503    public String encode(final String sourceStr, final Charset sourceCharset) {
504        if (sourceStr == null) {
505            return null;
506        }
507        return StringUtils.newStringUsAscii(this.encode(sourceStr.getBytes(sourceCharset)));
508    }
509
510    /**
511     * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
512     * <p>
513     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
514     * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
515     * </p>
516     *
517     * @param sourceStr     string to convert to quoted-printable form.
518     * @param sourceCharset The Charset for sourceStr.
519     * @return quoted-printable string.
520     * @throws UnsupportedEncodingException Thrown if the Charset is not supported.
521     */
522    public String encode(final String sourceStr, final String sourceCharset) throws UnsupportedEncodingException {
523        if (sourceStr == null) {
524            return null;
525        }
526        return StringUtils.newStringUsAscii(encode(sourceStr.getBytes(sourceCharset)));
527    }
528
529    /**
530     * Gets the default Charset name used for string decoding and encoding.
531     *
532     * @return The default Charset name.
533     * @since 1.7
534     */
535    public Charset getCharset() {
536        return this.charset;
537    }
538
539    /**
540     * Gets the default Charset name used for string decoding and encoding.
541     *
542     * @return The default Charset name.
543     */
544    public String getDefaultCharset() {
545        return this.charset.name();
546    }
547}