View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.net;
19  
20  import java.io.ByteArrayOutputStream;
21  import java.io.UnsupportedEncodingException;
22  import java.nio.charset.Charset;
23  import java.nio.charset.IllegalCharsetNameException;
24  import java.nio.charset.StandardCharsets;
25  import java.nio.charset.UnsupportedCharsetException;
26  import java.util.BitSet;
27  
28  import org.apache.commons.codec.BinaryDecoder;
29  import org.apache.commons.codec.BinaryEncoder;
30  import org.apache.commons.codec.DecoderException;
31  import org.apache.commons.codec.EncoderException;
32  import org.apache.commons.codec.StringDecoder;
33  import org.apache.commons.codec.StringEncoder;
34  import org.apache.commons.codec.binary.StringUtils;
35  
36  /**
37   * Codec for the Quoted-Printable section of <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>.
38   * <p>
39   * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to printable characters in the ASCII character
40   * 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
41   * 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
42   * the integrity of the data should the message pass through a character- translating, and/or line-wrapping gateway.
43   * </p>
44   * <p>
45   * Note:
46   * </p>
47   * <p>
48   * Depending on the selected {@code strict} parameter, encoding implements a different set of rules of the quoted-printable spec:
49   * </p>
50   * <ul>
51   * <li>{@code strict=false}: only rules #1 and #2 are implemented</li>
52   * <li>{@code strict=true}: all rules #1 through #5 are implemented</li>
53   * </ul>
54   * <p>
55   * 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
56   * require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec. The strict mode has been added in 1.10.
57   * Decoding is independent of this parameter; see {@link #decodeQuotedPrintable(byte[])} for its behavior.
58   * </p>
59   * <p>
60   * This class is immutable and thread-safe.
61   * </p>
62   *
63   * @see <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing
64   *      the Format of Internet Message Bodies </a>
65   *
66   * @since 1.3
67   */
68  public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
69  
70      /**
71       * BitSet of printable characters as defined in RFC 1521.
72       */
73      private static final BitSet PRINTABLE_CHARS = new BitSet(256);
74      private static final byte ESCAPE_CHAR = '=';
75      private static final byte TAB = 9;
76      private static final byte CR = 13;
77      private static final byte LF = 10;
78  
79      /**
80       * Minimum length required for the byte arrays used by encodeQuotedPrintable method.
81       */
82      private static final int MIN_BYTES = 3;
83  
84      /**
85       * Safe line length for quoted printable encoded text.
86       */
87      private static final int SAFE_LENGTH = 73;
88  
89      // Static initializer for printable chars collection
90      static {
91          // alpha characters
92          for (int i = 33; i <= 60; i++) {
93              PRINTABLE_CHARS.set(i);
94          }
95          for (int i = 62; i <= 126; i++) {
96              PRINTABLE_CHARS.set(i);
97          }
98          PRINTABLE_CHARS.set(TAB);
99          PRINTABLE_CHARS.set(Utils.SPACE);
100     }
101 
102     /**
103      * Decodes quoted-printable bytes.
104      *
105      * <p>
106      * Converts hexadecimal escapes to their original bytes, removes soft line breaks ({@code =CRLF}), and preserves hard CRLF line breaks.
107      * </p>
108      *
109      * <p>
110      * As a lenient extension for malformed input, unpaired CR and LF bytes are also preserved. An equals sign followed by CR without LF is rejected.
111      * This method does not perform full MIME validation: for example, it neither removes trailing whitespace nor handles transport padding after an
112      * equals sign. The {@code strict} constructor parameter affects encoding only.
113      * </p>
114      *
115      * <p>
116      * Since 1.23.0, unescaped CR and LF bytes are preserved and {@code =CR} without a following LF is rejected. Earlier versions discarded unescaped
117      * CR and LF bytes and accepted {@code =CR} as a soft line break.
118      * </p>
119      *
120      * @param bytes array of quoted-printable characters.
121      * @return array of original bytes, or {@code null} if the input is {@code null}.
122      * @throws DecoderException Thrown if an escape is incomplete or invalid, including a soft line break without the full CRLF pair.
123      */
124     public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException {
125         if (bytes == null) {
126             return null;
127         }
128         final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
129         for (int i = 0; i < bytes.length; i++) {
130             final int b = bytes[i];
131             if (b == ESCAPE_CHAR) {
132                 try {
133                     // rule #5: a soft line break is the escape character followed by a CRLF sequence;
134                     // it is removed entirely from the decoded output
135                     if (bytes[++i] == CR) {
136                         if (++i >= bytes.length || bytes[i] != LF) {
137                             throw new DecoderException("Invalid quoted-printable encoding: soft line break must be =CRLF");
138                         }
139                         continue;
140                     }
141                     final int u = Utils.digit16(bytes[i]);
142                     final int l = Utils.digit16(bytes[++i]);
143                     buffer.write((char) ((u << 4) + l));
144                 } catch (final ArrayIndexOutOfBoundsException e) {
145                     throw new DecoderException("Invalid quoted-printable encoding", e);
146                 }
147             } else {
148                 // Preserve hard line breaks and, leniently, unpaired CR and LF bytes.
149                 buffer.write(b);
150             }
151         }
152         return buffer.toByteArray();
153     }
154 
155     /**
156      * Encodes a byte in the buffer.
157      *
158      * @param b      byte to write.
159      * @param encode indicates whether the octet shall be encoded.
160      * @param buffer The buffer to write to.
161      * @return The number of bytes that have been written to the buffer.
162      */
163     private static int encodeByte(final int b, final boolean encode, final ByteArrayOutputStream buffer) {
164         if (encode) {
165             return encodeQuotedPrintable(b, buffer);
166         }
167         buffer.write(b);
168         return 1;
169     }
170 
171     /**
172      * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
173      * <p>
174      * 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
175      * binary data and unformatted text.
176      * </p>
177      *
178      * @param printable bitset of characters deemed quoted-printable.
179      * @param bytes     array of bytes to be encoded.
180      * @return array of bytes containing quoted-printable data.
181      */
182     public static final byte[] encodeQuotedPrintable(final BitSet printable, final byte[] bytes) {
183         return encodeQuotedPrintable(printable, bytes, false);
184     }
185 
186     /**
187      * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
188      * <p>
189      * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
190      * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
191      * </p>
192      *
193      * @param printable bitset of characters deemed quoted-printable.
194      * @param bytes     array of bytes to be encoded.
195      * @param strict    if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2.
196      * @return array of bytes containing quoted-printable data.
197      * @since 1.10
198      */
199     public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, final boolean strict) {
200         if (bytes == null) {
201             return null;
202         }
203         if (printable == null) {
204             printable = PRINTABLE_CHARS;
205         }
206         final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
207         final int bytesLength = bytes.length;
208         if (strict) {
209             if (bytesLength < MIN_BYTES) {
210                 return null;
211             }
212             int pos = 1;
213             // encode up to buffer.length - 3, the last three octets will be treated
214             // separately for simplification of note #3
215             for (int i = 0; i < bytesLength - 3; i++) {
216                 final int b = getUnsignedOctet(i, bytes);
217                 if (pos < SAFE_LENGTH) {
218                     // up to this length it is safe to add any byte, encoded or not
219                     pos += encodeByte(b, !printable.get(b), buffer);
220                 } else {
221                     // rule #3: whitespace at the end of a line *must* be encoded
222                     encodeByte(b, !printable.get(b) || isWhitespace(b), buffer);
223                     // rule #5: soft line break
224                     buffer.write(ESCAPE_CHAR);
225                     buffer.write(CR);
226                     buffer.write(LF);
227                     pos = 1;
228                 }
229             }
230             // rule #3: whitespace at the end of a line *must* be encoded
231             // if we would do a soft break line after this octet, encode whitespace
232             int b = getUnsignedOctet(bytesLength - 3, bytes);
233             boolean encode = !printable.get(b) || isWhitespace(b) && pos > SAFE_LENGTH - 5;
234             pos += encodeByte(b, encode, buffer);
235             // note #3: '=' *must not* be the ultimate or penultimate character
236             // simplification: if < 6 bytes left, do a soft line break as we may need
237             // exactly 6 bytes space for the last 2 bytes
238             if (pos > SAFE_LENGTH - 2) {
239                 buffer.write(ESCAPE_CHAR);
240                 buffer.write(CR);
241                 buffer.write(LF);
242             }
243             for (int i = bytesLength - 2; i < bytesLength; i++) {
244                 b = getUnsignedOctet(i, bytes);
245                 // rule #3: trailing whitespace shall be encoded
246                 encode = !printable.get(b) || i > bytesLength - 2 && isWhitespace(b);
247                 encodeByte(b, encode, buffer);
248             }
249         } else {
250             for (final byte c : bytes) {
251                 int b = c;
252                 if (b < 0) {
253                     b = 256 + b;
254                 }
255                 if (printable.get(b)) {
256                     buffer.write(b);
257                 } else {
258                     encodeQuotedPrintable(b, buffer);
259                 }
260             }
261         }
262         return buffer.toByteArray();
263     }
264 
265     /**
266      * Encodes byte into its quoted-printable representation.
267      *
268      * @param b      byte to encode.
269      * @param buffer The buffer to write to.
270      * @return The number of bytes written to the {@code buffer}.
271      */
272     private static int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) {
273         buffer.write(ESCAPE_CHAR);
274         final char hex1 = Utils.hexChar(b >> 4);
275         final char hex2 = Utils.hexChar(b);
276         buffer.write(hex1);
277         buffer.write(hex2);
278         return 3;
279     }
280 
281     /**
282      * Gets the byte at position {@code index} of the byte array and makes sure it is unsigned.
283      *
284      * @param index position in the array.
285      * @param bytes The byte array.
286      * @return The unsigned octet at position {@code index} from the array.
287      */
288     private static int getUnsignedOctet(final int index, final byte[] bytes) {
289         int b = bytes[index];
290         if (b < 0) {
291             b = 256 + b;
292         }
293         return b;
294     }
295 
296     /**
297      * Tests whether the given byte is whitespace.
298      *
299      * @param b byte to be checked.
300      * @return {@code true} if the byte is either a space or tab character.
301      */
302     private static boolean isWhitespace(final int b) {
303         return b == Utils.SPACE || b == TAB;
304     }
305 
306     /**
307      * The default Charset used for string decoding and encoding.
308      */
309     private final Charset charset;
310 
311     /**
312      * Indicates whether soft line breaks shall be used during encoding (rule #3-5).
313      */
314     private final boolean strict;
315 
316     /**
317      * Constructs a new instance, assumes default Charset of {@link StandardCharsets#UTF_8}
318      */
319     public QuotedPrintableCodec() {
320         this(StandardCharsets.UTF_8, false);
321     }
322 
323     /**
324      * Constructs a new instance for the selection of the strict mode.
325      *
326      * @param strict if {@code true}, soft line breaks will be used.
327      * @since 1.10
328      */
329     public QuotedPrintableCodec(final boolean strict) {
330         this(StandardCharsets.UTF_8, strict);
331     }
332 
333     /**
334      * Constructs a new instance for the selection of a default Charset.
335      *
336      * @param charset The default string Charset to use.
337      * @since 1.7
338      */
339     public QuotedPrintableCodec(final Charset charset) {
340         this(charset, false);
341     }
342 
343     /**
344      * Constructs a new instance for the selection of a default Charset and strict mode.
345      *
346      * @param charset The default string Charset to use.
347      * @param strict  if {@code true}, soft line breaks will be used.
348      * @since 1.10
349      */
350     public QuotedPrintableCodec(final Charset charset, final boolean strict) {
351         this.charset = charset;
352         this.strict = strict;
353     }
354 
355     /**
356      * Constructs a new instance for the selection of a default Charset.
357      *
358      * @param charsetName The default string Charset to use.
359      * @throws UnsupportedCharsetException Thrown if no support for the named Charset is available in this instance of the Java virtual machine.
360      * @throws IllegalArgumentException    Thrown if the given charsetName is null.
361      * @throws IllegalCharsetNameException Thrown if the given Charset name is illegal.
362      *
363      * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
364      */
365     public QuotedPrintableCodec(final String charsetName) throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException {
366         this(Charset.forName(charsetName), false);
367     }
368 
369     /**
370      * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation.
371      * <p>
372      * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521.
373      * </p>
374      *
375      * @param bytes array of quoted-printable characters.
376      * @return array of original bytes.
377      * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful.
378      */
379     @Override
380     public byte[] decode(final byte[] bytes) throws DecoderException {
381         return decodeQuotedPrintable(bytes);
382     }
383 
384     /**
385      * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original representation.
386      *
387      * @param obj quoted-printable object to convert into its original form.
388      * @return original object.
389      * @throws DecoderException Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure condition is encountered during the decode
390      *                          process.
391      */
392     @Override
393     public Object decode(final Object obj) throws DecoderException {
394         if (obj == null) {
395             return null;
396         }
397         if (obj instanceof byte[]) {
398             return decode((byte[]) obj);
399         }
400         if (obj instanceof String) {
401             return decode((String) obj);
402         }
403         throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable decoded");
404     }
405 
406     /**
407      * Decodes a quoted-printable string into its original form using the default string Charset. Escaped characters are converted back to their original
408      * representation.
409      *
410      * @param sourceStr quoted-printable string to convert into its original form.
411      * @return original string.
412      * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. Thrown if Charset is not supported.
413      * @see #getCharset()
414      */
415     @Override
416     public String decode(final String sourceStr) throws DecoderException {
417         return this.decode(sourceStr, getCharset());
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      * @since 1.7
429      */
430     public String decode(final String sourceStr, final Charset sourceCharset) throws DecoderException {
431         if (sourceStr == null) {
432             return null;
433         }
434         return new String(this.decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
435     }
436 
437     /**
438      * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original
439      * representation.
440      *
441      * @param sourceStr     quoted-printable string to convert into its original form.
442      * @param sourceCharset The original string Charset.
443      * @return original string.
444      * @throws DecoderException             Thrown if quoted-printable decoding is unsuccessful.
445      * @throws UnsupportedEncodingException Thrown if Charset is not supported.
446      */
447     public String decode(final String sourceStr, final String sourceCharset) throws DecoderException, UnsupportedEncodingException {
448         if (sourceStr == null) {
449             return null;
450         }
451         return new String(decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
452     }
453 
454     /**
455      * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
456      * <p>
457      * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
458      * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
459      * </p>
460      *
461      * @param bytes array of bytes to be encoded.
462      * @return array of bytes containing quoted-printable data.
463      */
464     @Override
465     public byte[] encode(final byte[] bytes) {
466         return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict);
467     }
468 
469     /**
470      * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped.
471      *
472      * @param obj string to convert to a quoted-printable form.
473      * @return quoted-printable object.
474      * @throws EncoderException Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is unsuccessful.
475      */
476     @Override
477     public Object encode(final Object obj) throws EncoderException {
478         if (obj == null) {
479             return null;
480         }
481         if (obj instanceof byte[]) {
482             return encode((byte[]) obj);
483         }
484         if (obj instanceof String) {
485             return encode((String) obj);
486         }
487         throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable encoded");
488     }
489 
490     /**
491      * Encodes a string into its quoted-printable form using the default string Charset. Unsafe characters are escaped.
492      * <p>
493      * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
494      * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
495      * </p>
496      *
497      * @param sourceStr string to convert to quoted-printable form.
498      * @return quoted-printable string.
499      * @throws EncoderException Thrown if quoted-printable encoding is unsuccessful.
500      *
501      * @see #getCharset()
502      */
503     @Override
504     public String encode(final String sourceStr) throws EncoderException {
505         return encode(sourceStr, getCharset());
506     }
507 
508     /**
509      * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
510      * <p>
511      * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
512      * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
513      * </p>
514      *
515      * @param sourceStr     string to convert to quoted-printable form.
516      * @param sourceCharset The Charset for sourceStr.
517      * @return quoted-printable string.
518      * @since 1.7
519      */
520     public String encode(final String sourceStr, final Charset sourceCharset) {
521         if (sourceStr == null) {
522             return null;
523         }
524         return StringUtils.newStringUsAscii(this.encode(sourceStr.getBytes(sourceCharset)));
525     }
526 
527     /**
528      * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
529      * <p>
530      * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable
531      * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
532      * </p>
533      *
534      * @param sourceStr     string to convert to quoted-printable form.
535      * @param sourceCharset The Charset for sourceStr.
536      * @return quoted-printable string.
537      * @throws UnsupportedEncodingException Thrown if the Charset is not supported.
538      */
539     public String encode(final String sourceStr, final String sourceCharset) throws UnsupportedEncodingException {
540         if (sourceStr == null) {
541             return null;
542         }
543         return StringUtils.newStringUsAscii(encode(sourceStr.getBytes(sourceCharset)));
544     }
545 
546     /**
547      * Gets the default Charset name used for string decoding and encoding.
548      *
549      * @return The default Charset name.
550      * @since 1.7
551      */
552     public Charset getCharset() {
553         return this.charset;
554     }
555 
556     /**
557      * Gets the default Charset name used for string decoding and encoding.
558      *
559      * @return The default Charset name.
560      */
561     public String getDefaultCharset() {
562         return this.charset.name();
563     }
564 }