Skip to content

Commit bdc77f1

Browse files
committed
[CODEC-121] QuotedPrintableCodec does not support soft line break per the 'quoted-printable' example on Wikipedia. Apply patch from TN.
git-svn-id: https://svn.apache.org/repos/asf/commons/proper/codec/trunk@1637008 13f79535-47bb-0310-9956-ffa450edef68
1 parent 0110214 commit bdc77f1

3 files changed

Lines changed: 274 additions & 49 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The <action> type attribute can be add,update,fix,remove.
4444
<body>
4545
<release version="1.10" date="DD November 2014" description="Feature and fix release.">
4646
<action dev="ggregory" type="add" issue="CODEC-192" due-to="Thomas Neidhart">Add Daitch-Mokotoff Soundex</action>
47+
<action dev="ggregory" type="add" issue="CODEC-121" due-to="Thomas Neidhart, Java John">QuotedPrintableCodec does not support soft line break per the 'quoted-printable' example on Wikipedia</action>
4748
<action dev="tn" type="fix" issue="CODEC-185" due-to="Sean Busbey">Added clarification to Javadoc of Base64 concerning the use of the urlSafe parameter</action>
4849
<action dev="tn" type="fix" issue="CODEC-191" due-to="Igor Savin">Added clarification to the Javadoc of Base[32|64]OutputStream that it is mandatory to call close()</action>
4950
<action dev="ggregory" type="fix" issue="CODEC-188" due-to="Hendrik Saly">Add support for HMAC Message Authentication Code (MAC) digests</action>

src/main/java/org/apache/commons/codec/net/QuotedPrintableCodec.java

Lines changed: 198 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,19 @@
4545
* <p>
4646
* Note:
4747
* <p>
48-
* Rules #3, #4, and #5 of the quoted-printable spec are not implemented yet because the complete quoted-printable spec
49-
* does not lend itself well into the byte[] oriented codec framework. Complete the codec once the streamable codec
50-
* framework is ready. The motivation behind providing the codec in a partial form is that it can already come in handy
51-
* for those applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
48+
* Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the
49+
* quoted-printable spec:
50+
* <ul>
51+
* <li>{@code strict=false}: only rules #1 and #2 are implemented
52+
* <li>{@code strict=true}: all rules #1 through #5 are implemented
53+
* </ul>
54+
* Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used
55+
* for certain applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
56+
* The strict mode has been added in 1.10.
5257
* <p>
5358
* This class is immutable and thread-safe.
5459
*
55-
* @see <a href="http://www.ietf.org/rfc/rfc1521.txt"> RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
60+
* @see <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
5661
* Mechanisms for Specifying and Describing the Format of Internet Message Bodies </a>
5762
*
5863
* @since 1.3
@@ -64,6 +69,11 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
6469
*/
6570
private final Charset charset;
6671

72+
/**
73+
* Indicates whether soft line breaks shall be used during encoding (rule #3-5).
74+
*/
75+
private final boolean strict;
76+
6777
/**
6878
* BitSet of printable characters as defined in RFC 1521.
6979
*/
@@ -74,6 +84,16 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
7484
private static final byte TAB = 9;
7585

7686
private static final byte SPACE = 32;
87+
88+
private static final byte CR = 13;
89+
90+
private static final byte LF = 10;
91+
92+
/**
93+
* Safe line length for quoted printable encoded text.
94+
*/
95+
private static final int SAFE_LENGTH = 73;
96+
7797
// Static initializer for printable chars collection
7898
static {
7999
// alpha characters
@@ -91,7 +111,18 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
91111
* Default constructor, assumes default charset of {@link Charsets#UTF_8}
92112
*/
93113
public QuotedPrintableCodec() {
94-
this(Charsets.UTF_8);
114+
this(Charsets.UTF_8, false);
115+
}
116+
117+
/**
118+
* Constructor which allows for the selection of the strict mode.
119+
*
120+
* @param strict
121+
* if {@code true}, soft line breaks will be used
122+
* @since 1.10
123+
*/
124+
public QuotedPrintableCodec(final boolean strict) {
125+
this(Charsets.UTF_8, strict);
95126
}
96127

97128
/**
@@ -102,7 +133,21 @@ public QuotedPrintableCodec() {
102133
* @since 1.7
103134
*/
104135
public QuotedPrintableCodec(final Charset charset) {
136+
this(charset, false);
137+
}
138+
139+
/**
140+
* Constructor which allows for the selection of a default charset and strict mode.
141+
*
142+
* @param charset
143+
* the default string charset to use.
144+
* @param strict
145+
* if {@code true}, soft line breaks will be used
146+
* @since 1.10
147+
*/
148+
public QuotedPrintableCodec(final Charset charset, final boolean strict) {
105149
this.charset = charset;
150+
this.strict = strict;
106151
}
107152

108153
/**
@@ -122,7 +167,7 @@ public QuotedPrintableCodec(final Charset charset) {
122167
*/
123168
public QuotedPrintableCodec(final String charsetName)
124169
throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException {
125-
this(Charset.forName(charsetName));
170+
this(Charset.forName(charsetName), false);
126171
}
127172

128173
/**
@@ -132,13 +177,65 @@ public QuotedPrintableCodec(final String charsetName)
132177
* byte to encode
133178
* @param buffer
134179
* the buffer to write to
180+
* @return The number of bytes written to the <code>buffer</code>
135181
*/
136-
private static final void encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) {
182+
private static final int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) {
137183
buffer.write(ESCAPE_CHAR);
138184
final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
139185
final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
140186
buffer.write(hex1);
141187
buffer.write(hex2);
188+
return 3;
189+
}
190+
191+
/**
192+
* Return the byte at position <code>index</code> of the byte array and
193+
* make sure it is unsigned.
194+
*
195+
* @param index
196+
* position in the array
197+
* @param bytes
198+
* the byte array
199+
* @return the unsigned octet at position <code>index</code> from the array
200+
*/
201+
private static int getUnsignedOctet(final int index, final byte[] bytes) {
202+
int b = bytes[index];
203+
if (b < 0) {
204+
b = 256 + b;
205+
}
206+
return b;
207+
}
208+
209+
/**
210+
* Write a byte to the buffer.
211+
*
212+
* @param b
213+
* byte to write
214+
* @param encode
215+
* indicates whether the octet shall be encoded
216+
* @param buffer
217+
* the buffer to write to
218+
* @return the number of bytes that have been written to the buffer
219+
*/
220+
private static int encodeByte(final int b, final boolean encode,
221+
final ByteArrayOutputStream buffer) {
222+
if (encode) {
223+
return encodeQuotedPrintable(b, buffer);
224+
} else {
225+
buffer.write(b);
226+
return 1;
227+
}
228+
}
229+
230+
/**
231+
* Checks whether the given byte is whitespace.
232+
*
233+
* @param b
234+
* byte to be checked
235+
* @return <code>true</code> if the byte is either a space or tab character
236+
*/
237+
private static boolean isWhitespace(final int b) {
238+
return b == SPACE || b == TAB;
142239
}
143240

144241
/**
@@ -154,22 +251,86 @@ private static final void encodeQuotedPrintable(final int b, final ByteArrayOutp
154251
* @return array of bytes containing quoted-printable data
155252
*/
156253
public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes) {
254+
return encodeQuotedPrintable(printable, bytes, false);
255+
}
256+
257+
/**
258+
* Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
259+
* <p>
260+
* Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
261+
* or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
262+
* RFC 1521 and is suitable for encoding binary data and unformatted text.
263+
*
264+
* @param printable
265+
* bitset of characters deemed quoted-printable
266+
* @param bytes
267+
* array of bytes to be encoded
268+
* @param strict
269+
* if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2
270+
* @return array of bytes containing quoted-printable data
271+
* @since 1.10
272+
*/
273+
public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, boolean strict) {
157274
if (bytes == null) {
158275
return null;
159276
}
160277
if (printable == null) {
161278
printable = PRINTABLE_CHARS;
162279
}
163280
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
164-
for (final byte c : bytes) {
165-
int b = c;
166-
if (b < 0) {
167-
b = 256 + b;
281+
282+
if (strict) {
283+
int pos = 1;
284+
// encode up to buffer.length - 3, the last three octets will be treated
285+
// separately for simplification of note #3
286+
for (int i = 0; i < bytes.length - 3; i++) {
287+
int b = getUnsignedOctet(i, bytes);
288+
if (pos < SAFE_LENGTH) {
289+
// up to this length it is safe to add any byte, encoded or not
290+
pos += encodeByte(b, !printable.get(b), buffer);
291+
} else {
292+
// rule #3: whitespace at the end of a line *must* be encoded
293+
encodeByte(b, !printable.get(b) || isWhitespace(b), buffer);
294+
295+
// rule #5: soft line break
296+
buffer.write(ESCAPE_CHAR);
297+
buffer.write(CR);
298+
buffer.write(LF);
299+
pos = 1;
300+
}
168301
}
169-
if (printable.get(b)) {
170-
buffer.write(b);
171-
} else {
172-
encodeQuotedPrintable(b, buffer);
302+
303+
// rule #3: whitespace at the end of a line *must* be encoded
304+
// if we would do a soft break line after this octet, encode whitespace
305+
int b = getUnsignedOctet(bytes.length - 3, bytes);
306+
boolean encode = !printable.get(b) || (isWhitespace(b) && pos > SAFE_LENGTH - 5);
307+
pos += encodeByte(b, encode, buffer);
308+
309+
// note #3: '=' *must not* be the ultimate or penultimate character
310+
// simplification: if < 6 bytes left, do a soft line break as we may need
311+
// exactly 6 bytes space for the last 2 bytes
312+
if (pos > SAFE_LENGTH - 2) {
313+
buffer.write(ESCAPE_CHAR);
314+
buffer.write(CR);
315+
buffer.write(LF);
316+
}
317+
for (int i = bytes.length - 2; i < bytes.length; i++) {
318+
b = getUnsignedOctet(i, bytes);
319+
// rule #3: trailing whitespace shall be encoded
320+
encode = !printable.get(b) || (i > bytes.length - 2 && isWhitespace(b));
321+
encodeByte(b, encode, buffer);
322+
}
323+
} else {
324+
for (final byte c : bytes) {
325+
int b = c;
326+
if (b < 0) {
327+
b = 256 + b;
328+
}
329+
if (printable.get(b)) {
330+
buffer.write(b);
331+
} else {
332+
encodeQuotedPrintable(b, buffer);
333+
}
173334
}
174335
}
175336
return buffer.toByteArray();
@@ -179,8 +340,8 @@ public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[]
179340
* Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted
180341
* back to their original representation.
181342
* <p>
182-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
183-
* RFC 1521.
343+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as
344+
* defined in RFC 1521.
184345
*
185346
* @param bytes
186347
* array of quoted-printable characters
@@ -197,13 +358,18 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
197358
final int b = bytes[i];
198359
if (b == ESCAPE_CHAR) {
199360
try {
200-
final int u = Utils.digit16(bytes[++i]);
361+
// if the next octet is a CR we have found a soft line break
362+
if (bytes[++i] == CR) {
363+
continue;
364+
}
365+
final int u = Utils.digit16(bytes[i]);
201366
final int l = Utils.digit16(bytes[++i]);
202367
buffer.write((char) ((u << 4) + l));
203368
} catch (final ArrayIndexOutOfBoundsException e) {
204369
throw new DecoderException("Invalid quoted-printable encoding", e);
205370
}
206-
} else {
371+
} else if (b != CR && b != LF) {
372+
// every other octet is appended except for CR & LF
207373
buffer.write(b);
208374
}
209375
}
@@ -213,7 +379,8 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
213379
/**
214380
* Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
215381
* <p>
216-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
382+
* Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
383+
* or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
217384
* RFC 1521 and is suitable for encoding binary data and unformatted text.
218385
*
219386
* @param bytes
@@ -222,15 +389,15 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
222389
*/
223390
@Override
224391
public byte[] encode(final byte[] bytes) {
225-
return encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
392+
return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict);
226393
}
227394

228395
/**
229396
* Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted
230397
* back to their original representation.
231398
* <p>
232-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
233-
* RFC 1521.
399+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as
400+
* defined in RFC 1521.
234401
*
235402
* @param bytes
236403
* array of quoted-printable characters
@@ -246,8 +413,9 @@ public byte[] decode(final byte[] bytes) throws DecoderException {
246413
/**
247414
* Encodes a string into its quoted-printable form using the default string charset. Unsafe characters are escaped.
248415
* <p>
249-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
250-
* RFC 1521 and is suitable for encoding binary data.
416+
* Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
417+
* or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
418+
* RFC 1521 and is suitable for encoding binary data and unformatted text.
251419
*
252420
* @param str
253421
* string to convert to quoted-printable form
@@ -392,7 +560,8 @@ public String getDefaultCharset() {
392560
/**
393561
* Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
394562
* <p>
395-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
563+
* Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
564+
* or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
396565
* RFC 1521 and is suitable for encoding binary data and unformatted text.
397566
*
398567
* @param str
@@ -412,7 +581,8 @@ public String encode(final String str, final Charset charset) {
412581
/**
413582
* Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
414583
* <p>
415-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
584+
* Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
585+
* or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
416586
* RFC 1521 and is suitable for encoding binary data and unformatted text.
417587
*
418588
* @param str

0 commit comments

Comments
 (0)