This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/soft-line-breaks in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 2bd74a5fd06e12aa9cdb906daf713f35ebc6b5ac Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Nov 12 07:04:40 2026 +0000 Decode quoted-printable soft line breaks without recursion --- .../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))); + } }
