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);
