You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2026/06/02 20:23:06 UTC

(tomcat) branch main updated (8baa306386 -> a2ee8e625f)

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

markt-asf pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


    from 8baa306386 Fixes from code review
     new 774d854783 Follow-up to EOF handling fixes
     new a2ee8e625f Test cases co-authored with CoPilot/GPT-5.4

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 java/org/apache/tomcat/util/buf/B2CConverter.java  |  93 ++++---
 java/org/apache/tomcat/util/buf/ByteChunk.java     |   7 +
 .../apache/tomcat/util/buf/TestB2CConverter.java   | 269 +++++++++++++++++++++
 3 files changed, 337 insertions(+), 32 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 01/02: Follow-up to EOF handling fixes

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 774d8547834ad1e3309030d6a7d3470af269ae72
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Jun 2 21:15:55 2026 +0100

    Follow-up to EOF handling fixes
---
 java/org/apache/tomcat/util/buf/B2CConverter.java | 93 +++++++++++++++--------
 java/org/apache/tomcat/util/buf/ByteChunk.java    |  7 ++
 2 files changed, 68 insertions(+), 32 deletions(-)

diff --git a/java/org/apache/tomcat/util/buf/B2CConverter.java b/java/org/apache/tomcat/util/buf/B2CConverter.java
index e40f020606..ce3c761bef 100644
--- a/java/org/apache/tomcat/util/buf/B2CConverter.java
+++ b/java/org/apache/tomcat/util/buf/B2CConverter.java
@@ -24,6 +24,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
 import java.util.Locale;
 
 import org.apache.juli.logging.Log;
@@ -150,31 +151,42 @@ public class B2CConverter {
         CoderResult result;
         // Parse leftover if any are present
         if (leftovers.position() > 0) {
-            int pos = cb.position();
-            // Loop until one char is decoded or there is a decoder error
             do {
+                // Previous call may have triggered overflow in cb so try conversion first
+                leftovers.flip();
+                result = decoder.decode(leftovers, cb, false);
+                if (result.isOverflow()) {
+                    leftovers.position(leftovers.limit());
+                    leftovers.limit(leftovers.array().length);
+                    return;
+                } else if (result.isError()) {
+                    result.throwException();
+                } else if (leftovers.remaining() == 0) {
+                    // leftovers have been converted
+                    leftovers.clear();
+                    bb.position(bc.getStart());
+                    break;
+                } else {
+                    // leftovers is incomplete
+                    leftovers.position(leftovers.limit());
+                    leftovers.limit(leftovers.array().length);
+                }
+                // Try
                 int b = bc.subtract();
                 if (b < 0) {
-                    leftovers.flip();
-                    result = decoder.decode(leftovers, cb, endOfInput);
-                    break;
+                    if (endOfInput) {
+                        throw new MalformedInputException(leftovers.position());
+                    }
+                    // Underflow - need more bytes to complete the leftover character
+                    return;
                 }
                 leftovers.put((byte) b);
-                leftovers.flip();
-                result = decoder.decode(leftovers, cb, endOfInput);
-                leftovers.position(leftovers.limit());
-                leftovers.limit(leftovers.array().length);
-            } while (result.isUnderflow() && (cb.position() == pos));
-            if (result.isError() || result.isMalformed()) {
-                result.throwException();
-            }
-            bb.position(bc.getStart());
-            leftovers.position(0);
+            } while (true);
         }
         // Do the decoding and get the results into the byte chunk and the char
         // chunk
         result = decoder.decode(bb, cb, endOfInput);
-        if (result.isError() || result.isMalformed()) {
+        if (result.isError()) {
             result.throwException();
         } else if (result.isOverflow()) {
             // Propagate current positions to the byte chunk and char chunk, if
@@ -225,33 +237,50 @@ public class B2CConverter {
         CoderResult result;
         // Parse leftover if any are present
         if (leftovers.position() > 0) {
-            int pos = cb.position();
-            // Loop until one char is decoded or there is a decoder error
             do {
+                // Previous call may have triggered overflow in cb so try conversion first
+                leftovers.flip();
+                result = decoder.decode(leftovers, cb, false);
+                if (result.isOverflow()) {
+                    leftovers.position(leftovers.limit());
+                    leftovers.limit(leftovers.array().length);
+                    return;
+                } else if (result.isError()) {
+                    result.throwException();
+                } else if (leftovers.remaining() == 0) {
+                    // leftovers have been converted
+                    leftovers.clear();
+                    bb.limit(bc.limit());
+                    bb.position(bc.position());
+                    break;
+                } else {
+                    // leftovers is incomplete
+                    leftovers.position(leftovers.limit());
+                    leftovers.limit(leftovers.array().length);
+                }
+                // Try
                 if (bc.remaining() == 0) {
                     int n = ic.realReadBytes();
+                    if (ic.getByteBuffer() != bc) {
+                        // realReadBytes changed the ByteBuffer used by the caller
+                        bc = ic.getByteBuffer();
+                        bb = ByteBuffer.wrap(bc.array(), bc.arrayOffset() + bc.position(), bc.remaining());
+                    }
                     if (n < 0) {
-                        leftovers.flip();
-                        result = decoder.decode(leftovers, cb, endOfInput);
-                        break;
+                        if (endOfInput) {
+                            throw new MalformedInputException(leftovers.position());
+                        }
+                        // Underflow - need more bytes to complete the leftover character
+                        return;
                     }
                 }
                 leftovers.put(bc.get());
-                leftovers.flip();
-                result = decoder.decode(leftovers, cb, endOfInput);
-                leftovers.position(leftovers.limit());
-                leftovers.limit(leftovers.array().length);
-            } while (result.isUnderflow() && (cb.position() == pos));
-            if (result.isError() || result.isMalformed()) {
-                result.throwException();
-            }
-            bb.position(bc.position());
-            leftovers.position(0);
+            } while (true);
         }
         // Do the decoding and get the results into the byte chunk and the char
         // chunk
         result = decoder.decode(bb, cb, endOfInput);
-        if (result.isError() || result.isMalformed()) {
+        if (result.isError()) {
             result.throwException();
         } else if (result.isOverflow()) {
             // Propagate current positions to the byte chunk and char chunk, if
diff --git a/java/org/apache/tomcat/util/buf/ByteChunk.java b/java/org/apache/tomcat/util/buf/ByteChunk.java
index 3897a1755e..012ae264e7 100644
--- a/java/org/apache/tomcat/util/buf/ByteChunk.java
+++ b/java/org/apache/tomcat/util/buf/ByteChunk.java
@@ -68,6 +68,13 @@ public final class ByteChunk extends AbstractChunk {
          * @throws IOException If an I/O error occurs during reading
          */
         int realReadBytes() throws IOException;
+
+        /**
+         * Obtain the ByteBuffer this channel is targeting.
+         *
+         * @return the ByteBuffer this channel is targeting
+         */
+        ByteBuffer getByteBuffer();
     }
 
     /**


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 02/02: Test cases co-authored with CoPilot/GPT-5.4

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit a2ee8e625faa7b704fb0e76c6c75c5ce40423a25
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Jun 2 21:16:26 2026 +0100

    Test cases co-authored with CoPilot/GPT-5.4
---
 .../apache/tomcat/util/buf/TestB2CConverter.java   | 269 +++++++++++++++++++++
 1 file changed, 269 insertions(+)

diff --git a/test/org/apache/tomcat/util/buf/TestB2CConverter.java b/test/org/apache/tomcat/util/buf/TestB2CConverter.java
index 8c30591f44..a5e83fcfa6 100644
--- a/test/org/apache/tomcat/util/buf/TestB2CConverter.java
+++ b/test/org/apache/tomcat/util/buf/TestB2CConverter.java
@@ -16,6 +16,9 @@
  */
 package org.apache.tomcat.util.buf;
 
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.MalformedInputException;
 import java.nio.charset.StandardCharsets;
@@ -24,6 +27,8 @@ import java.util.Locale;
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.catalina.connector.InputBuffer;
+
 public class TestB2CConverter {
 
     private static final byte[] UTF16_MESSAGE =
@@ -140,4 +145,268 @@ public class TestB2CConverter {
         }
         Assert.assertNotNull(e);
     }
+
+
+    @Test
+    public void testLeftoverChunk() throws Exception {
+        // E2 8C A8 is the "keyboard" character
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteChunk bc = new ByteChunk();
+        CharChunk cc = new CharChunk();
+        cc.allocate(1, -1);
+
+        bc.append((byte) 0x41);
+
+        bc.append((byte) 0xE2);
+        conv.convert(bc, cc, false);
+        Assert.assertEquals(1, cc.getLength());
+
+        bc.append((byte) 0x8C);
+        conv.convert(bc, cc, false);
+        Assert.assertEquals(1, cc.getLength());
+
+        bc.append((byte) 0xA8);
+        conv.convert(bc, cc, true);
+        // Expect overflow so while all 3 bytes are present, they aren't converted
+        Assert.assertEquals(1, cc.getLength());
+
+        cc.setLimit(2);
+        cc.makeSpace(2);
+        conv.convert(bc, cc, true);
+        Assert.assertEquals("A\u2328", cc.toString());
+    }
+
+
+    @Test
+    public void testLeftoverBuffer() throws Exception {
+        // E2 8C A8 is the "keyboard" character
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteBuffer bb = ByteBuffer.allocate(8);
+        CharBuffer cb = newCharBuffer(1);
+        TesterInputBuffer ib = new TesterInputBuffer(bb);
+
+        bb.put((byte) 0x41);
+        bb.flip();
+
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A", cb);
+
+        // Will trigger overflow if there is a complete character
+        bb.clear();
+        bb.put((byte) 0xE2);
+        bb.flip();
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A", cb);
+
+        // NO-OP (underflow)
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A", cb);
+
+        // Adds second byte of an incomplete 3 byte character to leftovers
+        ib.setNextRealBytes(new byte[] { (byte) 0x8C });
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A", cb);
+
+        // Adds final byte of 3 byte character to leftovers. Not converted as output will overflow
+        ib.setNextRealBytes(new byte[] { (byte) 0xA8 });
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A", cb);
+
+        cb = newCharBuffer(2);
+        cb.limit(cb.capacity());
+        cb.put('A');
+        cb.limit(1);
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("A\u2328", cb);
+    }
+
+
+    @Test
+    public void testLeftoverChunkWithTrailingBytes() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteChunk bc = new ByteChunk();
+        CharChunk cc = new CharChunk();
+        cc.allocate(4, -1);
+
+        bc.append((byte) 0x41);
+        bc.append((byte) 0xE2);
+        conv.convert(bc, cc, false);
+
+        bc.append((byte) 0x8C);
+        conv.convert(bc, cc, false);
+
+        bc.append((byte) 0xA8);
+        bc.append((byte) 0x42);
+        conv.convert(bc, cc, true);
+
+        Assert.assertEquals("A\u2328B", cc.toString());
+    }
+
+
+    @Test
+    public void testLeftoverChunkMalformed() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8, true);
+        ByteChunk bc = new ByteChunk();
+        CharChunk cc = new CharChunk();
+        cc.allocate(4, -1);
+
+        bc.append((byte) 0x41);
+        bc.append((byte) 0xE2);
+        conv.convert(bc, cc, false);
+
+        bc.append((byte) 0x42);
+        conv.convert(bc, cc, true);
+
+        Assert.assertEquals("A\ufffdB", cc.toString());
+    }
+
+
+    @Test
+    public void testLeftoverChunkMalformedAtEnd() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8, true);
+        ByteChunk bc = new ByteChunk();
+        CharChunk cc = new CharChunk();
+        cc.allocate(4, -1);
+
+        bc.append((byte) 0x41);
+        bc.append((byte) 0xE2);
+        conv.convert(bc, cc, true);
+
+        Assert.assertEquals("A\ufffd", cc.toString());
+    }
+
+
+    @Test
+    public void testBug54602d() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteBuffer bb = ByteBuffer.allocate(4);
+        CharBuffer cb = newCharBuffer(4);
+        TesterInputBuffer ib = new TesterInputBuffer(bb);
+
+        bb.put(UTF8_PARTIAL);
+        bb.flip();
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("", cb);
+
+        conv.convert(bb, cb, ib, false);
+        assertCharBufferEquals("", cb);
+
+        Exception e = null;
+        try {
+            conv.convert(bb, cb, ib, true);
+        } catch (MalformedInputException mie) {
+            e = mie;
+        }
+        Assert.assertNotNull(e);
+    }
+
+
+    @Test(timeout = 1000)
+    public void testLeftoverBufferWithBufferedContinuationBytes() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteBuffer bb = ByteBuffer.wrap(new byte[] { (byte) 0xE2 });
+        CharBuffer cb = newCharBuffer(4);
+        TesterInputBuffer ib = new TesterInputBuffer(bb);
+
+        conv.convert(bb, cb, ib, false);
+
+        bb = ByteBuffer.wrap(new byte[] { (byte) 0x8C, (byte) 0xA8, (byte) 0x42 });
+        ib.setByteBuffer(bb);
+        conv.convert(bb, cb, ib, true);
+
+        assertCharBufferEquals("\u2328B", cb);
+    }
+
+
+    @Test
+    public void testLeftoverBufferRefillWithTrailingBytes() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteBuffer bb = ByteBuffer.allocate(8);
+        CharBuffer cb = newCharBuffer(4);
+        TesterInputBuffer ib = new TesterInputBuffer(bb);
+
+        bb.put((byte) 0xE2);
+        bb.flip();
+        conv.convert(bb, cb, ib, false);
+
+        ib.setNextRealBytes(new byte[] { (byte) 0x8C, (byte) 0xA8, (byte) 0x42 });
+        conv.convert(bb, cb, ib, true);
+
+        assertCharBufferEquals("\u2328B", cb);
+    }
+
+
+    @Test
+    public void testLeftoverBufferRefillWithReplacementBuffer() throws Exception {
+        B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8);
+        ByteBuffer bb = ByteBuffer.allocate(8);
+        CharBuffer cb = newCharBuffer(4);
+        TesterInputBuffer ib = new TesterInputBuffer(bb);
+
+        bb.put((byte) 0xE2);
+        bb.flip();
+        conv.convert(bb, cb, ib, false);
+
+        ib.replaceNextByteBuffer(new byte[] { (byte) 0x8C, (byte) 0xA8, (byte) 0x42 });
+        conv.convert(bb, cb, ib, true);
+
+        assertCharBufferEquals("\u2328B", cb);
+    }
+
+
+    private static class TesterInputBuffer extends InputBuffer {
+
+        private byte[] nextRealBytes = null;
+        private boolean replaceByteBuffer = false;
+
+        TesterInputBuffer(ByteBuffer byteBuffer) {
+            super(null);
+            setByteBuffer(byteBuffer);
+        }
+
+        @Override
+        public int realReadBytes() throws IOException {
+            if (nextRealBytes == null) {
+                return -1;
+            } else {
+                ByteBuffer byteBuffer;
+                if (replaceByteBuffer) {
+                    byteBuffer = ByteBuffer.allocate(Math.max(getByteBuffer().capacity(), nextRealBytes.length));
+                    setByteBuffer(byteBuffer);
+                } else {
+                    byteBuffer = getByteBuffer();
+                }
+                byteBuffer.clear();
+                byteBuffer.put(nextRealBytes);
+                byteBuffer.flip();
+                int result = nextRealBytes.length;
+                nextRealBytes = null;
+                replaceByteBuffer = false;
+                return result;
+            }
+        }
+
+        public void setNextRealBytes(byte[] nextRealBytes) {
+            this.nextRealBytes = nextRealBytes;
+        }
+
+        public void replaceNextByteBuffer(byte[] nextRealBytes) {
+            this.nextRealBytes = nextRealBytes;
+            replaceByteBuffer = true;
+        }
+    }
+
+
+    private static CharBuffer newCharBuffer(int capacity) {
+        CharBuffer charBuffer = CharBuffer.allocate(capacity);
+        charBuffer.limit(0);
+        return charBuffer;
+    }
+
+
+    private static void assertCharBufferEquals(String expected, CharBuffer actual) {
+        CharBuffer actualCopy = actual.asReadOnlyBuffer();
+        actualCopy.position(0);
+        Assert.assertEquals(expected, actualCopy.toString());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org