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

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kit.git

commit 4b5290805f0f95e2a906f1d2edfa6625c067099a
Author: Jinal Sarvaiya <[email protected]>
AuthorDate: Thu Nov 5 12:30:47 2026 +0000

    fix #100: bound fence search to linear scan and reject inline-brace false 
positives
---
 .../camel/kit/ship/controller/ShipStageResult.java | 101 +++++++++++++++++----
 .../kit/ship/controller/ShipStageResultTest.java   |  30 ++++++
 2 files changed, 114 insertions(+), 17 deletions(-)

diff --git 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
index 7de329df..664125c7 100644
--- 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
+++ 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
@@ -10,7 +10,6 @@ import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.kit.ship.artifact.ArtifactManifest.JavaPolicy;
@@ -48,8 +47,7 @@ record ShipStageResult(
     private static final int MAX_ASSISTANT_BYTES = 1024 * 1024;
     private static final int MAX_REPORT_BYTES = MAX_ASSISTANT_BYTES;
     private static final Pattern ROUTE_ID = 
Pattern.compile("[A-Za-z0-9][A-Za-z0-9_.-]{0,127}");
-    private static final Pattern JSON_FENCE = Pattern.compile(
-            "```(?:json)?\\s*\\n(\\{.*?\\})\\s*\\n```", Pattern.DOTALL);
+    private static final Pattern BARE_JSON_START = 
Pattern.compile("(?:^|\\n)\\s*\\{");
     private static final Comparator<RouteContract> ROUTE_ORDER = Comparator
             .comparing(RouteContract::routeId)
             .thenComparing(RouteContract::routePath)
@@ -292,20 +290,9 @@ record ShipStageResult(
         if (stripped.startsWith("{")) {
             return stripped;
         }
-        Matcher m = JSON_FENCE.matcher(text);
-        if (m.find()) {
-            String first = m.group(1).strip();
-            int fenceStart = m.start();
-            int fenceEnd = m.end();
-            if (m.find()) {
-                throw new IOException("Pi stage result contains conflicting 
fenced JSON envelopes");
-            }
-            String before = text.substring(0, fenceStart);
-            String after = text.substring(fenceEnd);
-            if (before.indexOf('{') >= 0 || after.indexOf('{') >= 0) {
-                throw new IOException("Pi stage result contains conflicting 
JSON envelopes");
-            }
-            return first;
+        String fenced = extractFencedJson(text);
+        if (fenced != null) {
+            return fenced;
         }
         int start = text.indexOf('{');
         int end = text.lastIndexOf('}');
@@ -319,6 +306,86 @@ record ShipStageResult(
         return stripped;
     }
 
+    private static String extractFencedJson(String text) throws IOException {
+        String result = null;
+        int regionStart = -1;
+        int regionEnd = -1;
+        int pos = 0;
+        int len = text.length();
+
+        while (pos < len) {
+            int tick = text.indexOf("```", pos);
+            if (tick < 0) {
+                break;
+            }
+            if (tick > 0 && text.charAt(tick - 1) != '\n') {
+                pos = tick + 3;
+                continue;
+            }
+            int cur = tick + 3;
+            if (cur + 4 <= len && text.startsWith("json", cur)) {
+                cur += 4;
+            }
+            while (cur < len && (text.charAt(cur) == ' ' || text.charAt(cur) 
== '\t')) {
+                cur++;
+            }
+            if (cur >= len || text.charAt(cur) != '\n') {
+                pos = tick + 3;
+                continue;
+            }
+            int contentStart = cur + 1;
+
+            int close = contentStart;
+            boolean found = false;
+            while (true) {
+                close = text.indexOf("\n```", close);
+                if (close < 0) {
+                    break;
+                }
+                int afterClose = close + 4;
+                if (afterClose >= len || text.charAt(afterClose) == '\n') {
+                    found = true;
+                    break;
+                }
+                close = afterClose;
+            }
+            if (!found) {
+                pos = contentStart;
+                continue;
+            }
+
+            String content = text.substring(contentStart, close).strip();
+            int end = close + 4;
+            if (end < len && text.charAt(end) == '\n') {
+                end++;
+            }
+
+            if (content.startsWith("{") && content.endsWith("}")) {
+                if (result != null) {
+                    throw new IOException("Pi stage result contains 
conflicting fenced JSON envelopes");
+                }
+                result = content;
+                regionStart = tick;
+                regionEnd = end;
+            }
+            pos = end;
+        }
+
+        if (result == null) {
+            return null;
+        }
+        String before = text.substring(0, regionStart);
+        String after = text.substring(regionEnd);
+        if (hasBareJsonStart(before) || hasBareJsonStart(after)) {
+            throw new IOException("Pi stage result contains conflicting JSON 
envelopes");
+        }
+        return result;
+    }
+
+    private static boolean hasBareJsonStart(String fragment) {
+        return BARE_JSON_START.matcher(fragment).find();
+    }
+
     static byte[] utf8(String value, int maximum, String label) throws 
IOException {
         if (value == null || value.length() > maximum) {
             throw new IOException("Pi " + label + " exceeds its size limit");
diff --git 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
index 40100ada..a756134b 100644
--- 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
+++ 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
@@ -427,6 +427,36 @@ class ShipStageResultTest {
         assertEquals("149-local-controller", parsed.pipelineId());
     }
 
+    @Test
+    void acceptsFencedResponseWithInlineBracesInProse() throws Exception {
+        ObjectNode valid = result("149-local-controller", null);
+        String fenced = "Verified: POST returned `{\"status\":\"ok\"}` and 
logged the body.\n\n"
+                        + "```json\n" + JSON.writeValueAsString(valid) + 
"\n```\n";
+        ShipStageResult parsed = ShipStageResult.parse(Stage.DISCOVERY, 
fenced);
+        assertEquals("149-local-controller", parsed.pipelineId());
+    }
+
+    @Test
+    void malformedFenceOpenersDoNotCauseQuadraticScan() throws Exception {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < 4000; i++) {
+            sb.append("```json\n");
+            sb.append("Not valid JSON, just prose line 
").append(i).append('\n');
+        }
+        ObjectNode valid = result("149-local-controller", null);
+        
sb.append("```json\n").append(JSON.writeValueAsString(valid)).append("\n```\n");
+        String input = sb.toString();
+        assertTrue(input.length() > 150_000, "input must exceed 150 KB");
+
+        long start = System.nanoTime();
+        ShipStageResult parsed = ShipStageResult.parse(Stage.DISCOVERY, input);
+        long elapsedMs = (System.nanoTime() - start) / 1_000_000;
+
+        assertEquals("149-local-controller", parsed.pipelineId());
+        assertTrue(elapsedMs < 2000,
+                "parse took " + elapsedMs + " ms — expected under 2000 ms");
+    }
+
     @Test
     void rejectsConflictingFencedEnvelopes() throws Exception {
         ObjectNode first = result("149-local-controller", null);

Reply via email to