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

gnodet pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 5f0c66b4773b [backport camel-4.18.x] CAMEL-26481: camel-syslog - 
handle fragmented length fields in Rfc5425FrameDecoder (#28870)
5f0c66b4773b is described below

commit 5f0c66b4773bdeed08150d4781c0a89c35e978c6
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 15:21:36 2026 +0100

    [backport camel-4.18.x] CAMEL-26481: camel-syslog - handle fragmented 
length fields in Rfc5425FrameDecoder (#28870)
    
    CAMEL-26481: camel-syslog - handle fragmented length fields in 
Rfc5425FrameDecoder (#28846)
    
    
    (cherry picked from commit 6e68019c2c5a93c3fbd4f1b9ce626a99c8d5549e)
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../syslog/netty/Rfc5425FrameDecoder.java          |  74 +++++-------
 .../syslog/netty/Rfc5425FrameDecoderTest.java      | 130 +++++++++++++++++++++
 2 files changed, 158 insertions(+), 46 deletions(-)

diff --git 
a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
 
b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
index 36a628c072d7..afe81c44a841 100644
--- 
a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
+++ 
b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
@@ -19,7 +19,6 @@ package org.apache.camel.component.syslog.netty;
 import java.util.List;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
@@ -33,33 +32,34 @@ public class Rfc5425FrameDecoder extends 
ByteToMessageDecoder implements Channel
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> 
out) throws Exception {
         if (currentFramelength == null) {
             // find index of the first space, it should be after the length 
field
-            int index = indexOf(in, Unpooled.wrappedBuffer(new byte[] { ' ' 
}));
-
-            // Read part until the first space, if we have found one
-            StringBuilder lengthbuffer = new StringBuilder();
-            if (index > -1) {
-                ByteBuf byteBuf = in.readBytes(index);
-                byte[] dest = new byte[byteBuf.readableBytes()];
-                byteBuf.readBytes(dest);
-                lengthbuffer.append(new String(dest));
+            int spaceIndex = in.indexOf(in.readerIndex(), in.writerIndex(), 
(byte) ' ');
+            if (spaceIndex < 0) {
+                // The length delimiter has not arrived yet, wait for more 
data without modifying reader index
+                return;
             }
 
-            int length;
-            try {
-                // add one because we have to take in account the space after
-                // the length field
-                length = Integer.parseInt(lengthbuffer.toString()) + 1;
-            } catch (NumberFormatException e) {
-                length = -1;
+            int lengthDigits = spaceIndex - in.readerIndex();
+            if (lengthDigits == 0) {
+                throw invalidFrame(in, "empty length field");
             }
 
-            // We have not found the length field, reset the buffer so we can
-            // retry next time
-            if (length < 0) {
-                in.resetReaderIndex();
-                return;
+            int length = 0;
+            for (int i = 0; i < lengthDigits; i++) {
+                byte b = in.getByte(in.readerIndex() + i);
+                if (b < '0' || b > '9') {
+                    throw invalidFrame(in, "non-digit character in length 
field");
+                }
+                // the frame is read as length + 1 bytes (the space after the 
length field), so the field must leave
+                // room for that in an int; Integer.parseInt used to reject 
such a field, this loop must not wrap it
+                if (length > (Integer.MAX_VALUE - 1 - (b - '0')) / 10) {
+                    throw invalidFrame(in, "length field does not fit an int");
+                }
+                length = length * 10 + (b - '0');
             }
-            currentFramelength = length;
+
+            // add one because we have to take in account the space after the 
length field
+            currentFramelength = length + 1;
+            in.skipBytes(lengthDigits);
         }
 
         // Buffer does not contain enough data yet, wait until it does
@@ -74,30 +74,12 @@ public class Rfc5425FrameDecoder extends 
ByteToMessageDecoder implements Channel
     }
 
     /**
-     * Borrowed from the DelimiterBasedFrameDecoder Returns the number of 
bytes between the readerIndex of the haystack
-     * and the first needle found in the haystack. -1 is returned if no needle 
is found in the haystack.
+     * A malformed length field leaves no way to find the next frame, so the 
buffered bytes are dropped before the
+     * exception is raised: the connection is closed on it, and the bytes must 
not be decoded again on the way out.
      */
-    private static int indexOf(ByteBuf haystack, ByteBuf needle) {
-        for (int i = haystack.readerIndex(); i < haystack.writerIndex(); i++) {
-            int haystackIndex = i;
-            int needleIndex;
-            for (needleIndex = 0; needleIndex < needle.capacity(); 
needleIndex++) {
-                if (haystack.getByte(haystackIndex) != 
needle.getByte(needleIndex)) {
-                    break;
-                } else {
-                    haystackIndex++;
-                    if (haystackIndex == haystack.writerIndex() && needleIndex 
!= needle.capacity() - 1) {
-                        return -1;
-                    }
-                }
-            }
-
-            if (needleIndex == needle.capacity()) {
-                // Found the needle from the haystack!
-                return i - haystack.readerIndex();
-            }
-        }
-        return -1;
+    private static IllegalArgumentException invalidFrame(ByteBuf in, String 
reason) {
+        in.skipBytes(in.readableBytes());
+        return new IllegalArgumentException("Invalid RFC 5425 frame: " + 
reason);
     }
 
     @Override
diff --git 
a/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
 
b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
new file mode 100644
index 000000000000..1a90ce388c00
--- /dev/null
+++ 
b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.syslog.netty;
+
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.DecoderException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class Rfc5425FrameDecoderTest {
+
+    @Test
+    void testFragmentedLengthFieldDoesNotDuplicateFrames() {
+        EmbeddedChannel channel = new EmbeddedChannel(new 
Rfc5425FrameDecoder());
+
+        // Write first message and partial length of next message: "3 one5"
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("3 one5", 
StandardCharsets.UTF_8)));
+
+        // Exactly one frame (" one") should be produced
+        ByteBuf frame1 = channel.readInbound();
+        assertNotNull(frame1);
+        assertEquals(" one", frame1.toString(StandardCharsets.UTF_8));
+        frame1.release();
+
+        // No duplicate frame should be produced
+        assertNull(channel.readInbound());
+
+        // Write remainder of second message: " three"
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(" three", 
StandardCharsets.UTF_8)));
+
+        // Second frame (" three") should be produced
+        ByteBuf frame2 = channel.readInbound();
+        assertNotNull(frame2);
+        assertEquals(" three", frame2.toString(StandardCharsets.UTF_8));
+        frame2.release();
+
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    void testFragmentedLengthDigitsAcrossPackets() {
+        EmbeddedChannel channel = new EmbeddedChannel(new 
Rfc5425FrameDecoder());
+
+        // A complete frame followed by the first digit of the next length 
field: "3 one1". Before the fix the
+        // decoder reset the reader index when the space had not arrived, 
replaying " one" on the next packet.
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("3 one1", 
StandardCharsets.UTF_8)));
+        ByteBuf frame1 = channel.readInbound();
+        assertNotNull(frame1);
+        assertEquals(" one", frame1.toString(StandardCharsets.UTF_8));
+        frame1.release();
+        assertNull(channel.readInbound());
+
+        // The second digit, the space and the body arrive in the next packet: 
"1 hello world"
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("1 hello world", 
StandardCharsets.UTF_8)));
+        ByteBuf frame2 = channel.readInbound();
+        assertNotNull(frame2);
+        assertEquals(" hello world", frame2.toString(StandardCharsets.UTF_8));
+        frame2.release();
+
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    void testEmptyLengthFieldIsRejected() {
+        assertRejected(" hello", "empty length field");
+    }
+
+    @Test
+    void testNonDigitLengthFieldIsRejected() {
+        assertRejected("1a hello", "non-digit character in length field");
+    }
+
+    @Test
+    void testLengthFieldWrappingAnIntIsRejected() {
+        // 4294967296 wraps a 32-bit int to 0, which would make the decoder 
emit one-byte frames
+        assertRejected("4294967296 hello", "length field does not fit an int");
+    }
+
+    @Test
+    void testLengthFieldAtIntegerMaxIsRejected() {
+        // the frame is read as length + 1 bytes, so Integer.MAX_VALUE itself 
does not fit
+        assertRejected("2147483647 hello", "length field does not fit an int");
+    }
+
+    @Test
+    void testTenDigitLengthFieldThatFitsIsAccepted() {
+        EmbeddedChannel channel = new EmbeddedChannel(new 
Rfc5425FrameDecoder());
+
+        // the largest field this decoder accepts; the frame itself never 
arrives, so nothing is produced
+        assertFalse(channel.writeInbound(Unpooled.copiedBuffer("2147483646 x", 
StandardCharsets.UTF_8)));
+        assertNull(channel.readInbound());
+        channel.finishAndReleaseAll();
+    }
+
+    private static void assertRejected(String input, String reason) {
+        EmbeddedChannel channel = new EmbeddedChannel(new 
Rfc5425FrameDecoder());
+
+        // ByteToMessageDecoder wraps the decoder's exception, and 
EmbeddedChannel rethrows it from writeInbound
+        DecoderException e = assertThrows(DecoderException.class,
+                () -> channel.writeInbound(Unpooled.copiedBuffer(input, 
StandardCharsets.UTF_8)));
+        assertTrue(e.getMessage().contains(reason), e.getMessage());
+        channel.finishAndReleaseAll();
+    }
+}

Reply via email to