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 }