001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.codec.net; 019 020import java.io.ByteArrayOutputStream; 021import java.io.UnsupportedEncodingException; 022import java.nio.charset.Charset; 023import java.nio.charset.IllegalCharsetNameException; 024import java.nio.charset.StandardCharsets; 025import java.nio.charset.UnsupportedCharsetException; 026import java.util.BitSet; 027 028import org.apache.commons.codec.BinaryDecoder; 029import org.apache.commons.codec.BinaryEncoder; 030import org.apache.commons.codec.DecoderException; 031import org.apache.commons.codec.EncoderException; 032import org.apache.commons.codec.StringDecoder; 033import org.apache.commons.codec.StringEncoder; 034import org.apache.commons.codec.binary.StringUtils; 035 036/** 037 * Codec for the Quoted-Printable section of <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>. 038 * <p> 039 * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to printable characters in the ASCII character 040 * set. It encodes the data in such a way that the resulting octets are unlikely to be modified by mail transport. If the data being encoded are mostly ASCII 041 * text, the encoded form of the data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable to ensure 042 * the integrity of the data should the message pass through a character- translating, and/or line-wrapping gateway. 043 * </p> 044 * <p> 045 * Note: 046 * </p> 047 * <p> 048 * Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the quoted-printable spec: 049 * </p> 050 * <ul> 051 * <li>{@code strict=false}: only rules #1 and #2 are implemented</li> 052 * <li>{@code strict=true}: all rules #1 through #5 are implemented</li> 053 * </ul> 054 * <p> 055 * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used for certain applications that do not 056 * require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec. The strict mode has been added in 1.10. 057 * </p> 058 * <p> 059 * This class is immutable and thread-safe. 060 * </p> 061 * 062 * @see <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing 063 * the Format of Internet Message Bodies </a> 064 * 065 * @since 1.3 066 */ 067public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder { 068 069 /** 070 * BitSet of printable characters as defined in RFC 1521. 071 */ 072 private static final BitSet PRINTABLE_CHARS = new BitSet(256); 073 private static final byte ESCAPE_CHAR = '='; 074 private static final byte TAB = 9; 075 private static final byte CR = 13; 076 private static final byte LF = 10; 077 078 /** 079 * Minimum length required for the byte arrays used by encodeQuotedPrintable method. 080 */ 081 private static final int MIN_BYTES = 3; 082 083 /** 084 * Safe line length for quoted printable encoded text. 085 */ 086 private static final int SAFE_LENGTH = 73; 087 088 // Static initializer for printable chars collection 089 static { 090 // alpha characters 091 for (int i = 33; i <= 60; i++) { 092 PRINTABLE_CHARS.set(i); 093 } 094 for (int i = 62; i <= 126; i++) { 095 PRINTABLE_CHARS.set(i); 096 } 097 PRINTABLE_CHARS.set(TAB); 098 PRINTABLE_CHARS.set(Utils.SPACE); 099 } 100 101 /** 102 * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation. 103 * <p> 104 * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521. 105 * </p> 106 * 107 * @param bytes array of quoted-printable characters. 108 * @return array of original bytes. 109 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 110 */ 111 public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException { 112 if (bytes == null) { 113 return null; 114 } 115 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 116 for (int i = 0; i < bytes.length; i++) { 117 final int b = bytes[i]; 118 if (b == ESCAPE_CHAR) { 119 try { 120 // if the next octet is a CR we have found a soft line break 121 if (bytes[++i] == CR) { 122 continue; 123 } 124 final int u = Utils.digit16(bytes[i]); 125 final int l = Utils.digit16(bytes[++i]); 126 buffer.write((char) ((u << 4) + l)); 127 } catch (final ArrayIndexOutOfBoundsException e) { 128 throw new DecoderException("Invalid quoted-printable encoding", e); 129 } 130 } else if (b != CR && b != LF) { 131 // every other octet is appended except for CR & LF 132 buffer.write(b); 133 } 134 } 135 return buffer.toByteArray(); 136 } 137 138 /** 139 * Encodes a byte in the buffer. 140 * 141 * @param b byte to write. 142 * @param encode indicates whether the octet shall be encoded. 143 * @param buffer The buffer to write to. 144 * @return The number of bytes that have been written to the buffer. 145 */ 146 private static int encodeByte(final int b, final boolean encode, final ByteArrayOutputStream buffer) { 147 if (encode) { 148 return encodeQuotedPrintable(b, buffer); 149 } 150 buffer.write(b); 151 return 1; 152 } 153 154 /** 155 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 156 * <p> 157 * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding 158 * binary data and unformatted text. 159 * </p> 160 * 161 * @param printable bitset of characters deemed quoted-printable. 162 * @param bytes array of bytes to be encoded. 163 * @return array of bytes containing quoted-printable data. 164 */ 165 public static final byte[] encodeQuotedPrintable(final BitSet printable, final byte[] bytes) { 166 return encodeQuotedPrintable(printable, bytes, false); 167 } 168 169 /** 170 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 171 * <p> 172 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 173 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 174 * </p> 175 * 176 * @param printable bitset of characters deemed quoted-printable. 177 * @param bytes array of bytes to be encoded. 178 * @param strict if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2. 179 * @return array of bytes containing quoted-printable data. 180 * @since 1.10 181 */ 182 public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, final boolean strict) { 183 if (bytes == null) { 184 return null; 185 } 186 if (printable == null) { 187 printable = PRINTABLE_CHARS; 188 } 189 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 190 final int bytesLength = bytes.length; 191 if (strict) { 192 if (bytesLength < MIN_BYTES) { 193 return null; 194 } 195 int pos = 1; 196 // encode up to buffer.length - 3, the last three octets will be treated 197 // separately for simplification of note #3 198 for (int i = 0; i < bytesLength - 3; i++) { 199 final int b = getUnsignedOctet(i, bytes); 200 if (pos < SAFE_LENGTH) { 201 // up to this length it is safe to add any byte, encoded or not 202 pos += encodeByte(b, !printable.get(b), buffer); 203 } else { 204 // rule #3: whitespace at the end of a line *must* be encoded 205 encodeByte(b, !printable.get(b) || isWhitespace(b), buffer); 206 // rule #5: soft line break 207 buffer.write(ESCAPE_CHAR); 208 buffer.write(CR); 209 buffer.write(LF); 210 pos = 1; 211 } 212 } 213 // rule #3: whitespace at the end of a line *must* be encoded 214 // if we would do a soft break line after this octet, encode whitespace 215 int b = getUnsignedOctet(bytesLength - 3, bytes); 216 boolean encode = !printable.get(b) || isWhitespace(b) && pos > SAFE_LENGTH - 5; 217 pos += encodeByte(b, encode, buffer); 218 // note #3: '=' *must not* be the ultimate or penultimate character 219 // simplification: if < 6 bytes left, do a soft line break as we may need 220 // exactly 6 bytes space for the last 2 bytes 221 if (pos > SAFE_LENGTH - 2) { 222 buffer.write(ESCAPE_CHAR); 223 buffer.write(CR); 224 buffer.write(LF); 225 } 226 for (int i = bytesLength - 2; i < bytesLength; i++) { 227 b = getUnsignedOctet(i, bytes); 228 // rule #3: trailing whitespace shall be encoded 229 encode = !printable.get(b) || i > bytesLength - 2 && isWhitespace(b); 230 encodeByte(b, encode, buffer); 231 } 232 } else { 233 for (final byte c : bytes) { 234 int b = c; 235 if (b < 0) { 236 b = 256 + b; 237 } 238 if (printable.get(b)) { 239 buffer.write(b); 240 } else { 241 encodeQuotedPrintable(b, buffer); 242 } 243 } 244 } 245 return buffer.toByteArray(); 246 } 247 248 /** 249 * Encodes byte into its quoted-printable representation. 250 * 251 * @param b byte to encode. 252 * @param buffer The buffer to write to. 253 * @return The number of bytes written to the {@code buffer}. 254 */ 255 private static int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) { 256 buffer.write(ESCAPE_CHAR); 257 final char hex1 = Utils.hexChar(b >> 4); 258 final char hex2 = Utils.hexChar(b); 259 buffer.write(hex1); 260 buffer.write(hex2); 261 return 3; 262 } 263 264 /** 265 * Gets the byte at position {@code index} of the byte array and make sure it is unsigned. 266 * 267 * @param index position in the array. 268 * @param bytes The byte array. 269 * @return The unsigned octet at position {@code index} from the array. 270 */ 271 private static int getUnsignedOctet(final int index, final byte[] bytes) { 272 int b = bytes[index]; 273 if (b < 0) { 274 b = 256 + b; 275 } 276 return b; 277 } 278 279 /** 280 * Checks whether the given byte is whitespace. 281 * 282 * @param b byte to be checked. 283 * @return {@code true} if the byte is either a space or tab character. 284 */ 285 private static boolean isWhitespace(final int b) { 286 return b == Utils.SPACE || b == TAB; 287 } 288 289 /** 290 * The default Charset used for string decoding and encoding. 291 */ 292 private final Charset charset; 293 294 /** 295 * Indicates whether soft line breaks shall be used during encoding (rule #3-5). 296 */ 297 private final boolean strict; 298 299 /** 300 * Constructs a new instance, assumes default Charset of {@link StandardCharsets#UTF_8} 301 */ 302 public QuotedPrintableCodec() { 303 this(StandardCharsets.UTF_8, false); 304 } 305 306 /** 307 * Constructs a new instance for the selection of the strict mode. 308 * 309 * @param strict if {@code true}, soft line breaks will be used. 310 * @since 1.10 311 */ 312 public QuotedPrintableCodec(final boolean strict) { 313 this(StandardCharsets.UTF_8, strict); 314 } 315 316 /** 317 * Constructs a new instance for the selection of a default Charset. 318 * 319 * @param charset The default string Charset to use. 320 * @since 1.7 321 */ 322 public QuotedPrintableCodec(final Charset charset) { 323 this(charset, false); 324 } 325 326 /** 327 * Constructs a new instance for the selection of a default Charset and strict mode. 328 * 329 * @param charset The default string Charset to use. 330 * @param strict if {@code true}, soft line breaks will be used. 331 * @since 1.10 332 */ 333 public QuotedPrintableCodec(final Charset charset, final boolean strict) { 334 this.charset = charset; 335 this.strict = strict; 336 } 337 338 /** 339 * Constructs a new instance for the selection of a default Charset. 340 * 341 * @param charsetName The default string Charset to use. 342 * @throws UnsupportedCharsetException If no support for the named Charset is available in this instance of the Java virtual machine. 343 * @throws IllegalArgumentException If the given charsetName is null. 344 * @throws IllegalCharsetNameException If the given Charset name is illegal. 345 * 346 * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable 347 */ 348 public QuotedPrintableCodec(final String charsetName) throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException { 349 this(Charset.forName(charsetName), false); 350 } 351 352 /** 353 * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation. 354 * <p> 355 * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521. 356 * </p> 357 * 358 * @param bytes array of quoted-printable characters. 359 * @return array of original bytes. 360 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 361 */ 362 @Override 363 public byte[] decode(final byte[] bytes) throws DecoderException { 364 return decodeQuotedPrintable(bytes); 365 } 366 367 /** 368 * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original representation. 369 * 370 * @param obj quoted-printable object to convert into its original form. 371 * @return original object. 372 * @throws DecoderException Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure condition is encountered during the decode 373 * process. 374 */ 375 @Override 376 public Object decode(final Object obj) throws DecoderException { 377 if (obj == null) { 378 return null; 379 } 380 if (obj instanceof byte[]) { 381 return decode((byte[]) obj); 382 } 383 if (obj instanceof String) { 384 return decode((String) obj); 385 } 386 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable decoded"); 387 } 388 389 /** 390 * Decodes a quoted-printable string into its original form using the default string Charset. Escaped characters are converted back to their original 391 * representation. 392 * 393 * @param sourceStr quoted-printable string to convert into its original form. 394 * @return original string. 395 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. Thrown if Charset is not supported. 396 * @see #getCharset() 397 */ 398 @Override 399 public String decode(final String sourceStr) throws DecoderException { 400 return this.decode(sourceStr, getCharset()); 401 } 402 403 /** 404 * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original 405 * representation. 406 * 407 * @param sourceStr quoted-printable string to convert into its original form. 408 * @param sourceCharset The original string Charset. 409 * @return original string. 410 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 411 * @since 1.7 412 */ 413 public String decode(final String sourceStr, final Charset sourceCharset) throws DecoderException { 414 if (sourceStr == null) { 415 return null; 416 } 417 return new String(this.decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset); 418 } 419 420 /** 421 * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original 422 * representation. 423 * 424 * @param sourceStr quoted-printable string to convert into its original form. 425 * @param sourceCharset The original string Charset. 426 * @return original string. 427 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 428 * @throws UnsupportedEncodingException Thrown if Charset is not supported. 429 */ 430 public String decode(final String sourceStr, final String sourceCharset) throws DecoderException, UnsupportedEncodingException { 431 if (sourceStr == null) { 432 return null; 433 } 434 return new String(decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset); 435 } 436 437 /** 438 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 439 * <p> 440 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 441 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 442 * </p> 443 * 444 * @param bytes array of bytes to be encoded. 445 * @return array of bytes containing quoted-printable data. 446 */ 447 @Override 448 public byte[] encode(final byte[] bytes) { 449 return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict); 450 } 451 452 /** 453 * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped. 454 * 455 * @param obj string to convert to a quoted-printable form. 456 * @return quoted-printable object. 457 * @throws EncoderException Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is unsuccessful. 458 */ 459 @Override 460 public Object encode(final Object obj) throws EncoderException { 461 if (obj == null) { 462 return null; 463 } 464 if (obj instanceof byte[]) { 465 return encode((byte[]) obj); 466 } 467 if (obj instanceof String) { 468 return encode((String) obj); 469 } 470 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable encoded"); 471 } 472 473 /** 474 * Encodes a string into its quoted-printable form using the default string Charset. Unsafe characters are escaped. 475 * <p> 476 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 477 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 478 * </p> 479 * 480 * @param sourceStr string to convert to quoted-printable form. 481 * @return quoted-printable string. 482 * @throws EncoderException Thrown if quoted-printable encoding is unsuccessful. 483 * 484 * @see #getCharset() 485 */ 486 @Override 487 public String encode(final String sourceStr) throws EncoderException { 488 return encode(sourceStr, getCharset()); 489 } 490 491 /** 492 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 493 * <p> 494 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 495 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 496 * </p> 497 * 498 * @param sourceStr string to convert to quoted-printable form. 499 * @param sourceCharset The Charset for sourceStr. 500 * @return quoted-printable string. 501 * @since 1.7 502 */ 503 public String encode(final String sourceStr, final Charset sourceCharset) { 504 if (sourceStr == null) { 505 return null; 506 } 507 return StringUtils.newStringUsAscii(this.encode(sourceStr.getBytes(sourceCharset))); 508 } 509 510 /** 511 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 512 * <p> 513 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 514 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 515 * </p> 516 * 517 * @param sourceStr string to convert to quoted-printable form. 518 * @param sourceCharset The Charset for sourceStr. 519 * @return quoted-printable string. 520 * @throws UnsupportedEncodingException Thrown if the Charset is not supported. 521 */ 522 public String encode(final String sourceStr, final String sourceCharset) throws UnsupportedEncodingException { 523 if (sourceStr == null) { 524 return null; 525 } 526 return StringUtils.newStringUsAscii(encode(sourceStr.getBytes(sourceCharset))); 527 } 528 529 /** 530 * Gets the default Charset name used for string decoding and encoding. 531 * 532 * @return The default Charset name. 533 * @since 1.7 534 */ 535 public Charset getCharset() { 536 return this.charset; 537 } 538 539 /** 540 * Gets the default Charset name used for string decoding and encoding. 541 * 542 * @return The default Charset name. 543 */ 544 public String getDefaultCharset() { 545 return this.charset.name(); 546 } 547}