This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit dd2671084a7543e3094d28997f6c251f54b8b7c8
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Nov 12 13:16:03 2026 +0000

    Decode quoted-printable soft line breaks without recursion (#3709)
    
    (cherry picked from commit e7f156f5c2c4ffedccef3201fccd8d3662728642)
---
 .../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)));
+    }
 }

Reply via email to