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, this class will implement 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   * </p>
58   * <p>
59   * This class is immutable and thread-safe.
60   * </p>
61   *
62   * @see <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing
63   *      the Format of Internet Message Bodies </a>
64   *
65   * @since 1.3
66   */
67  public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
68  
69      /**
70       * BitSet of printable characters as defined in RFC 1521.
71       */
72      private static final BitSet PRINTABLE_CHARS = new BitSet(256);
73      private static final byte ESCAPE_CHAR = '=';
74      private static final byte TAB = 9;
75      private static final byte CR = 13;
76      private static final byte LF = 10;
77  
78      /**
79       * Minimum length required for the byte arrays used by encodeQuotedPrintable method.
80       */
81      private static final int MIN_BYTES = 3;
82  
83      /**
84       * Safe line length for quoted printable encoded text.
85       */
86      private static final int SAFE_LENGTH = 73;
87  
88      // Static initializer for printable chars collection
89      static {
90          // alpha characters
91          for (int i = 33; i <= 60; i++) {
92              PRINTABLE_CHARS.set(i);
93          }
94          for (int i = 62; i <= 126; i++) {
95              PRINTABLE_CHARS.set(i);
96          }
97          PRINTABLE_CHARS.set(TAB);
98          PRINTABLE_CHARS.set(Utils.SPACE);
99      }
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 }