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 e22d95201b0f35a5e4b4b4ad61b05fe711d05c2d Author: Jinal Sarvaiya <[email protected]> AuthorDate: Thu Nov 5 14:55:02 2026 +0000 fix #100: eliminate quadratic scan paths and same-line bare correction regression --- .../camel/kit/ship/controller/ShipStageResult.java | 25 +++++++++++++--- .../kit/ship/controller/ShipStageResultTest.java | 35 ++++++++++++++++++---- 2 files changed, 50 insertions(+), 10 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 664125c7..23907b16 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 @@ -47,7 +47,6 @@ 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 BARE_JSON_START = Pattern.compile("(?:^|\\n)\\s*\\{"); private static final Comparator<RouteContract> ROUTE_ORDER = Comparator .comparing(RouteContract::routeId) .thenComparing(RouteContract::routePath) @@ -350,8 +349,7 @@ record ShipStageResult( close = afterClose; } if (!found) { - pos = contentStart; - continue; + break; } String content = text.substring(contentStart, close).strip(); @@ -383,7 +381,26 @@ record ShipStageResult( } private static boolean hasBareJsonStart(String fragment) { - return BARE_JSON_START.matcher(fragment).find(); + int i = 0; + int len = fragment.length(); + while (i < len) { + char c = fragment.charAt(i); + if (c == '`') { + int j = i + 1; + while (j < len && fragment.charAt(j) != '`' && fragment.charAt(j) != '\n') { + j++; + } + if (j < len && fragment.charAt(j) == '`') { + i = j + 1; + continue; + } + } + if (c == '{') { + return true; + } + i++; + } + return false; } static byte[] utf8(String value, int maximum, String label) throws IOException { 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 a756134b..bb0ec226 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 @@ -439,14 +439,24 @@ class ShipStageResultTest { @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'); + for (int i = 0; i < 90_000; i++) { + sb.append("```json\n{\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"); + assertTrue(input.length() > 800_000, "input must exceed 800 KB"); + + long start = System.nanoTime(); + assertRejected(Stage.DISCOVERY, input, "90k unfinished openers"); + long elapsedMs = (System.nanoTime() - start) / 1_000_000; + assertTrue(elapsedMs < 2000, + "parse took " + elapsedMs + " ms — expected under 2000 ms"); + } + + @Test + void blankLinePrefixDoesNotCauseQuadraticScan() throws Exception { + ObjectNode valid = result("149-local-controller", null); + String prefix = "\n".repeat(100_000); + String input = prefix + "```json\n" + JSON.writeValueAsString(valid) + "\n```\n"; long start = System.nanoTime(); ShipStageResult parsed = ShipStageResult.parse(Stage.DISCOVERY, input); @@ -484,6 +494,19 @@ class ShipStageResultTest { assertRejected(Stage.DESIGN, bareThenFenced, "bare then fenced envelope"); } + @Test + void rejectsSameLineBareCorrection() throws Exception { + ObjectNode fenced = result(null, null); + ObjectNode bare = result(null, null); + bare.put("materialAmbiguity", true); + bare.withArray("unansweredQuestions").addObject() + .put("question", "Which retry limit?").putNull("defaultApplied"); + String fencedThenSameLineBare = "```json\n" + JSON.writeValueAsString(fenced) + + "\n```\nCorrection: " + JSON.writeValueAsString(bare); + assertRejected(Stage.DESIGN, fencedThenSameLineBare, + "fenced then same-line bare correction"); + } + @Test void rejectsArrayWrappedEnvelope() throws Exception { ObjectNode valid = result("149-local-controller", null);
