Skip to content

Commit 7e8c20b

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

3 files changed

Lines changed: 216 additions & 65 deletions

File tree

src/changes/changes.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
<!-- <release version="2.0" date="TBA" description="Feature and fix release."> <action dev="ggregory" type="fix" issue="CODEC-126"> Make
2626
org.apache.commons.codec.net.URLCodec charset field final. </action> </release>
2727
-->
28+
<release version="1.6.1" date="TBD" description="Feature and fix release.">
29+
<action dev="ggregory" type="fix" issue="CODEC-121" due-to="javajohn">
30+
QuotedPrintableCodec does not support soft line break per the 'quoted-printable' example on Wikipedia
31+
</action>
32+
</release>
2833
<release version="1.6" date="20 November 2011" description="Feature and fix release.">
2934
<action dev="ggregory" type="fix" issue="CODEC-129" due-to="ggregory">
3035
Use standard Maven directory layout.

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

Lines changed: 129 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,10 @@
4242
* to ensure the integrity of the data should the message pass through a character- translating, and/or line-wrapping
4343
* gateway.
4444
* </p>
45-
*
46-
* <p>
47-
* Note:
48-
* </p>
49-
* <p>
50-
* Rules #3, #4, and #5 of the quoted-printable spec are not implemented yet because the complete quoted-printable spec
51-
* does not lend itself well into the byte[] oriented codec framework. Complete the codec once the streamable codec
52-
* framework is ready. The motivation behind providing the codec in a partial form is that it can already come in handy
53-
* for those applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
54-
* </p>
55-
*
45+
*
5646
* @see <a href="http://www.ietf.org/rfc/rfc1521.txt"> RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
5747
* Mechanisms for Specifying and Describing the Format of Internet Message Bodies </a>
58-
*
48+
*
5949
* @author Apache Software Foundation
6050
* @since 1.3
6151
* @version $Id$
@@ -76,6 +66,14 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
7666
private static final byte TAB = 9;
7767

7868
private static final byte SPACE = 32;
69+
70+
private static final byte CR = 13;
71+
72+
private static final byte LF = 10;
73+
74+
/** Safe line length for quoted printable encoded text. */
75+
private static final int SAFE_LENGTH = 73;
76+
7977
// Static initializer for printable chars collection
8078
static {
8179
// alpha characters
@@ -111,26 +109,78 @@ public QuotedPrintableCodec(String charset) {
111109
* Encodes byte into its quoted-printable representation.
112110
*
113111
* @param b
114-
* byte to encode
112+
* byte to encode
115113
* @param buffer
116-
* the buffer to write to
114+
* the buffer to write to
115+
* @return The number of bytes written to the <code>buffer</code>
117116
*/
118-
private static final void encodeQuotedPrintable(int b, ByteArrayOutputStream buffer) {
117+
private static final int encodeQuotedPrintable(int b, ByteArrayOutputStream buffer) {
119118
buffer.write(ESCAPE_CHAR);
120119
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
121120
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
122121
buffer.write(hex1);
123122
buffer.write(hex2);
123+
return 3;
124+
}
125+
126+
/**
127+
* Return the byte at position <code>index</code> of the byte array and
128+
* make sure it is unsigned.
129+
*
130+
* @param index
131+
* position in the array
132+
* @param bytes
133+
* the byte array
134+
* @return the unsigned octet at position <code>index</code> from the array
135+
*/
136+
private static int getUnsignedOctet(final int index, final byte[] bytes) {
137+
int b = bytes[index];
138+
if (b < 0) {
139+
b = 256 + b;
140+
}
141+
return b;
142+
}
143+
144+
/**
145+
* Write a byte to the buffer.
146+
*
147+
* @param b
148+
* byte to write
149+
* @param encode
150+
* indicates whether the octet shall be encoded
151+
* @param buffer
152+
* the buffer to write to
153+
* @return the number of bytes that have been written to the buffer
154+
*/
155+
private static int encodeByte(final int b, final boolean encode,
156+
final ByteArrayOutputStream buffer) {
157+
if (encode) {
158+
return encodeQuotedPrintable(b, buffer);
159+
} else {
160+
buffer.write(b);
161+
return 1;
162+
}
163+
}
164+
165+
/**
166+
* Checks whether the given byte is whitespace.
167+
*
168+
* @param b
169+
* byte to be checked
170+
* @return <code>true</code> if the byte is either a space or tab character
171+
*/
172+
private static boolean isWhitespace(final int b) {
173+
return b == SPACE || b == TAB;
124174
}
125175

126176
/**
127177
* Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
128-
*
178+
*
129179
* <p>
130-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
131-
* RFC 1521 and is suitable for encoding binary data and unformatted text.
180+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #5)
181+
* as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
132182
* </p>
133-
*
183+
*
134184
* @param printable
135185
* bitset of characters deemed quoted-printable
136186
* @param bytes
@@ -145,29 +195,59 @@ public static final byte[] encodeQuotedPrintable(BitSet printable, byte[] bytes)
145195
printable = PRINTABLE_CHARS;
146196
}
147197
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
148-
for (byte c : bytes) {
149-
int b = c;
150-
if (b < 0) {
151-
b = 256 + b;
152-
}
153-
if (printable.get(b)) {
154-
buffer.write(b);
198+
int pos = 1;
199+
// encode up to buffer.length - 3, the last three octets will be treated
200+
// separately for simplification of note #3
201+
for (int i = 0; i < bytes.length - 3; i++) {
202+
int b = getUnsignedOctet(i, bytes);
203+
if (pos < SAFE_LENGTH) {
204+
// up to this length it is safe to add any byte, encoded or not
205+
pos += encodeByte(b, !printable.get(b), buffer);
155206
} else {
156-
encodeQuotedPrintable(b, buffer);
207+
// rule #3: whitespace at the end of a line *must* be encoded
208+
encodeByte(b, !printable.get(b) || isWhitespace(b), buffer);
209+
210+
// rule #5: soft line break
211+
buffer.write(ESCAPE_CHAR);
212+
buffer.write(CR);
213+
buffer.write(LF);
214+
pos = 1;
157215
}
158216
}
217+
218+
// rule #3: whitespace at the end of a line *must* be encoded
219+
// if we would do a soft break line after this octet, encode whitespace
220+
int b = getUnsignedOctet(bytes.length - 3, bytes);
221+
boolean encode = !printable.get(b) || (isWhitespace(b) && pos > SAFE_LENGTH - 5);
222+
pos += encodeByte(b, encode, buffer);
223+
224+
// note #3: '=' *must not* be the ultimate or penultimate character
225+
// simplification: if < 6 bytes left, do a soft line break as we may need
226+
// exactly 6 bytes space for the last 2 bytes
227+
if (pos > SAFE_LENGTH - 2) {
228+
buffer.write(ESCAPE_CHAR);
229+
buffer.write(CR);
230+
buffer.write(LF);
231+
}
232+
for (int i = bytes.length - 2; i < bytes.length; i++) {
233+
b = getUnsignedOctet(i, bytes);
234+
// rule #3: trailing whitespace shall be encoded
235+
encode = !printable.get(b) || (i > bytes.length - 2 && isWhitespace(b));
236+
encodeByte(b, encode, buffer);
237+
}
238+
159239
return buffer.toByteArray();
160240
}
161241

162242
/**
163-
* Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted
164-
* back to their original representation.
165-
*
243+
* Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are
244+
* converted back to their original representation.
245+
*
166246
* <p>
167-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
168-
* RFC 1521.
247+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as
248+
* defined in RFC 1521.
169249
* </p>
170-
*
250+
*
171251
* @param bytes
172252
* array of quoted-printable characters
173253
* @return array of original bytes
@@ -180,16 +260,21 @@ public static final byte[] decodeQuotedPrintable(byte[] bytes) throws DecoderExc
180260
}
181261
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
182262
for (int i = 0; i < bytes.length; i++) {
183-
int b = bytes[i];
263+
final int b = bytes[i];
184264
if (b == ESCAPE_CHAR) {
185265
try {
186-
int u = Utils.digit16(bytes[++i]);
266+
// if the next octet is a CR we have found a soft line break
267+
if (bytes[++i] == CR) {
268+
continue;
269+
}
270+
int u = Utils.digit16(bytes[i]);
187271
int l = Utils.digit16(bytes[++i]);
188272
buffer.write((char) ((u << 4) + l));
189273
} catch (ArrayIndexOutOfBoundsException e) {
190274
throw new DecoderException("Invalid quoted-printable encoding", e);
191275
}
192-
} else {
276+
} else if (b != CR && b != LF) {
277+
// every other octet is appended except for CR & LF
193278
buffer.write(b);
194279
}
195280
}
@@ -200,8 +285,8 @@ public static final byte[] decodeQuotedPrintable(byte[] bytes) throws DecoderExc
200285
* Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
201286
*
202287
* <p>
203-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
204-
* RFC 1521 and is suitable for encoding binary data and unformatted text.
288+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #5)
289+
* as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
205290
* </p>
206291
*
207292
* @param bytes
@@ -217,8 +302,8 @@ public byte[] encode(byte[] bytes) {
217302
* back to their original representation.
218303
*
219304
* <p>
220-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
221-
* RFC 1521.
305+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #2)
306+
* as defined in RFC 1521.
222307
* </p>
223308
*
224309
* @param bytes
@@ -235,8 +320,8 @@ public byte[] decode(byte[] bytes) throws DecoderException {
235320
* Encodes a string into its quoted-printable form using the default string charset. Unsafe characters are escaped.
236321
*
237322
* <p>
238-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
239-
* RFC 1521 and is suitable for encoding binary data.
323+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #2)
324+
* as defined in RFC 1521 and is suitable for encoding binary data.
240325
* </p>
241326
*
242327
* @param pString
@@ -365,8 +450,8 @@ public String getDefaultCharset() {
365450
* Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
366451
*
367452
* <p>
368-
* This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
369-
* RFC 1521 and is suitable for encoding binary data and unformatted text.
453+
* This function fully implements the quoted-printable encoding specification (rule #1 through rule #2)
454+
* as defined in RFC 1521 and is suitable for encoding binary data and unformatted text.
370455
* </p>
371456
*
372457
* @param pString

src/test/java/org/apache/commons/codec/net/QuotedPrintableCodecTest.java

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.apache.commons.codec.CharEncoding;
2525
import org.apache.commons.codec.DecoderException;
2626
import org.apache.commons.codec.EncoderException;
27-
import org.junit.Ignore;
2827
import org.junit.Test;
2928

3029
/**
@@ -265,30 +264,92 @@ public void testDefaultEncoding() throws Exception {
265264
}
266265

267266
@Test
268-
@Ignore
269-
/**
270-
* The QuotedPrintableCodec documentation states that this is not supported.
271-
*
272-
* @throws Exception
273-
* @see <a href="https://issues.apache.org/jira/browse/CODEC-121">CODEC-121</a>
274-
*/
275267
public void testSoftLineBreakDecode() throws Exception {
276-
String qpdata = "If you believe that truth=3Dbeauty, then surely=20=\r\nmathematics is the most beautiful branch of philosophy.";
277-
String expected = "If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.";
278-
assertEquals(expected, new QuotedPrintableCodec().decode(qpdata));
268+
String qpdata = "If you believe that truth=3Dbeauty, then surely=20=\r\nmathematics " +
269+
"is the most beautiful branch of philosophy.";
270+
String expected = "If you believe that truth=beauty, then surely mathematics " +
271+
"is the most beautiful branch of philosophy.";
272+
273+
QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
274+
assertEquals(expected, qpcodec.decode(qpdata));
275+
276+
String encoded = qpcodec.encode(expected);
277+
assertEquals(expected, qpcodec.decode(encoded));
279278
}
280279

281280
@Test
282-
@Ignore
283-
/**
284-
* The QuotedPrintableCodec documentation states that this is not supported.
285-
*
286-
* @throws Exception
287-
* @see <a href="https://issues.apache.org/jira/browse/CODEC-121">CODEC-121</a>
288-
*/
289281
public void testSoftLineBreakEncode() throws Exception {
290-
String qpdata = "If you believe that truth=3Dbeauty, then surely=20=\r\nmathematics is the most beautiful branch of philosophy.";
291-
String expected = "If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.";
292-
assertEquals(qpdata, new QuotedPrintableCodec().encode(expected));
282+
String qpdata = "If you believe that truth=3Dbeauty, then surely mathematics is the most " +
283+
"b=\r\neautiful branch of philosophy.";
284+
String expected = "If you believe that truth=beauty, then surely mathematics is the most " +
285+
"beautiful branch of philosophy.";
286+
287+
QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
288+
assertEquals(qpdata, qpcodec.encode(expected));
289+
290+
String decoded = qpcodec.decode(qpdata);
291+
assertEquals(qpdata, qpcodec.encode(decoded));
292+
}
293+
294+
@Test
295+
public void testSkipNotEncodedCRLF() throws Exception {
296+
String qpdata = "CRLF in an\n encoded text should be=20=\r\n\rskipped in the\r decoding.";
297+
String expected = "CRLF in an encoded text should be skipped in the decoding.";
298+
299+
QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
300+
assertEquals(expected, qpcodec.decode(qpdata));
301+
302+
String encoded = qpcodec.encode(expected);
303+
assertEquals(expected, qpcodec.decode(encoded));
304+
}
305+
306+
@Test
307+
public void testTrailingSpecial() throws Exception {
308+
final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
309+
310+
String plain ="This is a example of a quoted-printable text file. This might contain sp=cial chars.";
311+
String expected = "This is a example of a quoted-printable text file. This might contain sp=3D=\r\ncial chars.";
312+
assertEquals(expected, qpcodec.encode(plain));
313+
314+
plain ="This is a example of a quoted-printable text file. This might contain ta\tbs as well.";
315+
expected = "This is a example of a quoted-printable text file. This might contain ta=09=\r\nbs as well.";
316+
assertEquals(expected, qpcodec.encode(plain));
293317
}
318+
319+
@Test
320+
public void testUltimateSoftBreak() throws Exception {
321+
final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
322+
323+
String plain ="This is a example of a quoted-printable text file. There is no end to it\t";
324+
String expected = "This is a example of a quoted-printable text file. There is no end to i=\r\nt=09";
325+
326+
assertEquals(expected, qpcodec.encode(plain));
327+
328+
plain ="This is a example of a quoted-printable text file. There is no end to it ";
329+
expected = "This is a example of a quoted-printable text file. There is no end to i=\r\nt=20";
330+
331+
assertEquals(expected, qpcodec.encode(plain));
332+
333+
// whitespace before soft break
334+
plain ="This is a example of a quoted-printable text file. There is no end to ";
335+
expected = "This is a example of a quoted-printable text file. There is no end to=20=\r\n =20";
336+
337+
assertEquals(expected, qpcodec.encode(plain));
338+
339+
// non-printable character before soft break
340+
plain ="This is a example of a quoted-printable text file. There is no end to= ";
341+
expected = "This is a example of a quoted-printable text file. There is no end to=3D=\r\n =20";
342+
343+
assertEquals(expected, qpcodec.encode(plain));
344+
}
345+
346+
@Test
347+
public void testFinalBytes() throws Exception {
348+
// whitespace, but does not need to be encoded
349+
String plain ="This is a example of a quoted=printable text file. There is no tt";
350+
String expected = "This is a example of a quoted=3Dprintable text file. There is no tt";
351+
352+
assertEquals(expected, new QuotedPrintableCodec().encode(plain));
353+
}
354+
294355
}

0 commit comments

Comments
 (0)