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

    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}