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 }