This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new e7f156f5c2c Decode quoted-printable soft line breaks without recursion
(#3709)
e7f156f5c2c is described below
commit e7f156f5c2c4ffedccef3201fccd8d3662728642
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Nov 12 13:16:03 2026 +0000
Decode quoted-printable soft line breaks without recursion (#3709)
---
.../attachment/QuotedPrintableDecoderStream.java | 85 +++++++++++++---------
.../QuotedPrintableDecoderStreamTest.java | 25 +++++++
2 files changed, 75 insertions(+), 35 deletions(-)
diff --git
a/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java
b/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java
index 6e4569d8ce0..950edb22b7f 100644
---
a/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java
+++
b/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java
@@ -23,6 +23,9 @@ import java.io.InputStream;
import java.util.HexFormat;
public class QuotedPrintableDecoderStream extends InputStream {
+ // Returned by decodeNonspaceChar for a soft line break, which decodes to
nothing
+ private static final int SOFT_LINE_BREAK = -2;
+
private int deferredWhitespace;
private int cachedCharacter = -1;
private final InputStream in;
@@ -45,9 +48,10 @@ public class QuotedPrintableDecoderStream extends
InputStream {
if (b[1] != '\n') {
throw new IOException("Invalid quoted printable encoding");
}
- // this was a soft linebreak inserted by the encoding. We just
toss this away
- // on decode. We need to return something, so recurse and decode
the next.
- return read();
+ // this was a soft linebreak inserted by the encoding. We just
toss this away on decode.
+ // Don't recurse here to decode the next character, as a long run
of soft linebreaks
+ // would overflow the stack.
+ return SOFT_LINE_BREAK;
}
// this is a hex pair we need to convert back to a single byte.
return (decodeHexDigit(b[0]) << 4) | decodeHexDigit(b[1]);
@@ -64,39 +68,50 @@ public class QuotedPrintableDecoderStream extends
InputStream {
@Override
public int read() throws IOException {
- // we potentially need to scan over spans of whitespace characters to
determine if they're real
- // we just return blanks until the count goes to zero.
- if (deferredWhitespace > 0) {
- deferredWhitespace--;
+ while (true) {
+ // we potentially need to scan over spans of whitespace characters
to determine if they're real
+ // we just return blanks until the count goes to zero.
+ if (deferredWhitespace > 0) {
+ deferredWhitespace--;
+ return ' ';
+ }
+ // we may have needed to scan ahead to find the first non-blank
character, which we would store
+ // here. hand that back once we're done with the blanks.
+ if (cachedCharacter != -1) {
+ int result = cachedCharacter;
+ cachedCharacter = -1;
+ return result;
+ }
+ int ch = in.read();
+ if (ch != ' ') {
+ int decoded = decodeNonspaceChar(ch);
+ if (decoded == SOFT_LINE_BREAK) {
+ // nothing to return for a soft linebreak, so move on to
the next character
+ continue;
+ }
+ return decoded;
+ }
+ // space characters are a pain. We need to scan ahead until we
find a non-space character.
+ // if the character is a line terminator, we need to discard the
blanks.
+ // scan forward, counting the characters.
+ while ((ch = in.read()) == ' ') {
+ deferredWhitespace++;
+ }
+ // is this a lineend at the current location?
+ if (ch == -1 || ch == '\r' || ch == '\n') {
+ // those blanks we so zealously counted up don't really exist.
Clear out the counter.
+ deferredWhitespace = 0;
+ // return the real significant character now.
+ return ch;
+ }
+ int decoded = decodeNonspaceChar(ch);
+ if (decoded != SOFT_LINE_BREAK) {
+ // remember this character for later, after we've used up the
deferred blanks.
+ cachedCharacter = decoded;
+ }
+ // return this space. We did not include this one in the deferred
count, so we're right in sync.
+ // after a soft linebreak, the deferred blanks are returned next
and then decoding carries on.
return ' ';
}
- // we may have needed to scan ahead to find the first non-blank
character, which we would store here.
- // hand that back once we're done with the blanks.
- if (cachedCharacter != -1) {
- int result = cachedCharacter;
- cachedCharacter = -1;
- return result;
- }
- int ch = in.read();
- if (ch != ' ') {
- return decodeNonspaceChar(ch);
- }
- // space characters are a pain. We need to scan ahead until we find a
non-space character.
- // if the character is a line terminator, we need to discard the
blanks.
- // scan forward, counting the characters.
- while ((ch = in.read()) == ' ') {
- deferredWhitespace++;
- }
- // is this a lineend at the current location?
- if (ch == -1 || ch == '\r' || ch == '\n') {
- // those blanks we so zealously counted up don't really exist.
Clear out the counter.
- deferredWhitespace = 0;
- // return the real significant character now.
- return ch;
- }
- // remember this character for later, after we've used up the deferred
blanks.
- cachedCharacter = decodeNonspaceChar(ch);
- // return this space. We did not include this one in the deferred
count, so we're right in sync.
- return ' ';
}
}
diff --git
a/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java
b/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java
index 9e2ea016bbc..9207187dc74 100644
---
a/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java
+++
b/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java
@@ -64,4 +64,29 @@ public class QuotedPrintableDecoderStreamTest {
// a byte >= 0x80 following the '=' marker must not index the decode
table negatively
assertThrows(IOException.class, () -> decode(new byte[] {'=',
(byte)0xff, 'A'}));
}
+
+ @Test
+ public void testSoftLineBreak() throws Exception {
+ assertArrayEquals("hello world".getBytes(StandardCharsets.US_ASCII),
+ decode("hello
=\r\nworld".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ @Test
+ public void testSpaceBeforeSoftLineBreak() throws Exception {
+ // the character after a space and a soft line break must not be lost
+ assertArrayEquals(" a".getBytes(StandardCharsets.US_ASCII),
+ decode(" =\r\n a
".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ @Test
+ public void testManySoftLineBreaks() throws Exception {
+ // a long run of soft line breaks must not overflow the stack
+ String encoded = "=\r\n".repeat(200_000) + "X";
+ assertArrayEquals("X".getBytes(StandardCharsets.US_ASCII),
+ decode(encoded.getBytes(StandardCharsets.US_ASCII)));
+
+ encoded = " =\r\n".repeat(200_000) + "X";
+ assertArrayEquals((" ".repeat(200_000) +
"X").getBytes(StandardCharsets.US_ASCII),
+ decode(encoded.getBytes(StandardCharsets.US_ASCII)));
+ }
}