diff --git a/src/dynamic/corpus/json_parse/java.rs b/src/dynamic/corpus/json_parse/java.rs new file mode 100644 index 00000000..cdbd61a4 --- /dev/null +++ b/src/dynamic/corpus/json_parse/java.rs @@ -0,0 +1,59 @@ +//! Java `Cap::JSON_PARSE` payloads. +//! +//! The depth pair shares a single fixture; the payload tag +//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Java has +//! no prototype-pollution surface so the canary half of the slice is +//! intentionally omitted, matching the PHP / Go / Rust shape. +//! +//! Java has no stdlib JSON parser, so the harness ships a hand-rolled +//! iterative JSON walker as a sibling class (`NyxJsonProbe.java`); the +//! fixture calls `NyxJsonProbe.parse(text)` in place of any Jackson / +//! Gson dependency so the build path never reaches for an external jar. + +use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef}; +use crate::dynamic::oracle::ProbePredicate; + +const MAX_DEPTH: u32 = 64; + +pub const PAYLOADS: &[CuratedPayload] = &[ + CuratedPayload { + bytes: b"NYX_JSON_DEEP", + label: "json-parse-java-depth-bomb", + oracle: Oracle::SinkProbe { + predicates: &[ProbePredicate::JsonParseExcessiveDepth { + max_depth: MAX_DEPTH, + }], + }, + is_benign: false, + provenance: PayloadProvenance::Curated, + since_corpus_version: 15, + deprecated_at_corpus_version: None, + fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/java/Vuln.java"], + oob_nonce_slot: false, + probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth { + max_depth: MAX_DEPTH, + }], + benign_control: Some(PayloadRef { + label: "json-parse-java-depth-shallow", + }), + no_benign_control_rationale: None, + }, + CuratedPayload { + bytes: b"NYX_JSON_SHALLOW", + label: "json-parse-java-depth-shallow", + oracle: Oracle::SinkProbe { + predicates: &[ProbePredicate::JsonParseExcessiveDepth { + max_depth: MAX_DEPTH, + }], + }, + is_benign: true, + provenance: PayloadProvenance::Curated, + since_corpus_version: 15, + deprecated_at_corpus_version: None, + fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/java/Vuln.java"], + oob_nonce_slot: false, + probe_predicates: &[], + benign_control: None, + no_benign_control_rationale: None, + }, +]; diff --git a/src/dynamic/corpus/json_parse/mod.rs b/src/dynamic/corpus/json_parse/mod.rs index de0fba57..bd1b7c0b 100644 --- a/src/dynamic/corpus/json_parse/mod.rs +++ b/src/dynamic/corpus/json_parse/mod.rs @@ -17,6 +17,7 @@ //! regular property `data`, leaving the chain untouched. pub mod go; +pub mod java; pub mod javascript; pub mod php; pub mod python; diff --git a/src/dynamic/corpus/registry.rs b/src/dynamic/corpus/registry.rs index ab0a99c0..33fd000f 100644 --- a/src/dynamic/corpus/registry.rs +++ b/src/dynamic/corpus/registry.rs @@ -200,6 +200,7 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[ json_parse::javascript::PAYLOADS, ), (Cap::JSON_PARSE, Lang::Go, json_parse::go::PAYLOADS), + (Cap::JSON_PARSE, Lang::Java, json_parse::java::PAYLOADS), (Cap::JSON_PARSE, Lang::Php, json_parse::php::PAYLOADS), (Cap::JSON_PARSE, Lang::Python, json_parse::python::PAYLOADS), (Cap::JSON_PARSE, Lang::Ruby, json_parse::ruby::PAYLOADS), @@ -497,7 +498,15 @@ mod tests { ), ( Cap::JSON_PARSE, - &[Lang::JavaScript, Lang::Python, Lang::Ruby], + &[ + Lang::JavaScript, + Lang::Python, + Lang::Ruby, + Lang::Php, + Lang::Go, + Lang::Rust, + Lang::Java, + ], ), ( Cap::UNAUTHORIZED_ID, diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index d535ab17..d8831a8e 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -606,6 +606,18 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_crypto_harness(spec)); } + // Phase 11 (Track J.9): JSON_PARSE depth-bomb short-circuit. The + // Java harness reflectively loads the fixture class, invokes its + // declared method with the payload, walks the returned tree + // iteratively via `NyxJsonProbe.countDepth`, and emits a + // [`crate::dynamic::probe::ProbeKind::JsonParse`] probe. The + // hand-rolled `NyxJsonProbe` helper is shipped as a sibling + // `.java` file so the build path never reaches for Jackson / + // Gson. + if spec.expected_cap == crate::labels::Cap::JSON_PARSE { + return Ok(emit_json_parse_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. Routes through // the existing `invokeReflective` helper so the harness instantiates // the receiver via its no-arg constructor (or null-fills primitive @@ -2215,6 +2227,352 @@ public class NyxHarness {{ } } +/// Phase 11 (Track J.9) JSON_PARSE depth-bomb harness for Java. +/// +/// Reflectively loads the fixture's entry class, invokes the named +/// static method with the payload (signature `static Object +/// (String)`), then walks the returned tree iteratively via +/// `NyxJsonProbe.countDepth(Object)` to produce a +/// [`crate::dynamic::probe::ProbeKind::JsonParse`] record. +/// +/// Java has no stdlib JSON parser, so the harness ships +/// `NyxJsonProbe.java` as an `extra_files` sibling: a hand-rolled +/// iterative parser that returns a `java.util.List` / `java.util.Map` +/// tree without pulling Jackson / Gson onto the classpath. The +/// fixture calls `NyxJsonProbe.parse(text)` in place of any library +/// JSON parser. When the parser's own +/// [`NyxJsonProbe.NyxJsonDepthException`] fires (nesting above +/// `MAX_PARSE_DEPTH = 4096`) the harness emits a `JsonParse { depth: +/// 0, excessive_depth: true }` probe before continuing — matches the +/// PHP `JSON_ERROR_DEPTH` and Python `RecursionError` excess paths. +pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource { + let shim = probe_shim(); + let entry_source = read_entry_source(&spec.entry_file); + let entry_class = derive_entry_class(&entry_source); + let entry_fqn = derive_entry_qualifier(&entry_source, &entry_class); + let entry_method = if spec.entry_name.is_empty() { + "run".to_owned() + } else { + spec.entry_name.clone() + }; + + let source = format!( + r#"// Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9). +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class NyxHarness {{ +{shim} + + static void nyxJsonParseProbe(int depth, boolean excessive) {{ + String p = System.getenv("NYX_PROBE_PATH"); + if (p == null || p.isEmpty()) return; + long now = System.nanoTime(); + String pid = System.getenv("NYX_PAYLOAD_ID"); + if (pid == null) pid = ""; + StringBuilder line = new StringBuilder(192); + line.append("{{\"sink_callee\":\"NyxJsonProbe.parse\",\"args\":["); + line.append("{{\"kind\":\"Int\",\"value\":").append(depth).append("}}],"); + line.append("\"captured_at_ns\":").append(now).append(','); + line.append("\"payload_id\":\""); + nyxJsonEscape(pid, line); + line.append("\",\"kind\":{{\"kind\":\"JsonParse\",\"depth\":").append(depth); + line.append(",\"excessive_depth\":").append(excessive).append("}},"); + line.append("\"witness\":"); + line.append(nyxWitnessJson("NyxJsonProbe.parse", new String[]{{Integer.toString(depth)}})); + line.append("}}\n"); + try (FileWriter fw = new FileWriter(p, true)) {{ + fw.write(line.toString()); + }} catch (IOException e) {{ + // best-effort + }} + }} + + public static void main(String[] args) {{ + String payload = System.getenv("NYX_PAYLOAD"); + if (payload == null) payload = ""; + int depth = 0; + boolean excessive = false; + boolean fixtureInvoked = false; + try {{ + Class entry = Class.forName("{entry_fqn}"); + Method m = entry.getDeclaredMethod("{entry_method}", String.class); + m.setAccessible(true); + Object produced = m.invoke(null, payload); + depth = NyxJsonProbe.countDepth(produced); + excessive = depth > 64; + fixtureInvoked = true; + }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ + // Fall through to fallback probe. + }} catch (InvocationTargetException ite) {{ + Throwable cause = ite.getCause(); + if (cause instanceof NyxJsonProbe.NyxJsonDepthException) {{ + depth = 0; + excessive = true; + fixtureInvoked = true; + }} else if (cause instanceof NyxJsonProbe.NyxJsonParseException) {{ + // Malformed JSON — payload survived the harness path, + // record the parse attempt without claiming depth. + fixtureInvoked = true; + }} + }} + nyxJsonParseProbe(depth, excessive); + System.out.println("__NYX_SINK_HIT__"); + if (!fixtureInvoked) {{ + System.out.println("__NYX_JSON_PARSE_FALLBACK__"); + }} + }} +}} +"# + ); + HarnessSource { + source, + filename: "NyxHarness.java".to_owned(), + command: vec![ + "java".to_owned(), + "-cp".to_owned(), + ".".to_owned(), + "NyxHarness".to_owned(), + ], + extra_files: vec![("NyxJsonProbe.java".to_owned(), nyx_json_probe_source().to_owned())], + entry_subpath: Some(format!("{entry_class}.java")), + } +} + +/// Hand-rolled iterative JSON parser shipped alongside the harness. +/// +/// Phase 11 (Track J.9) cannot reach for Jackson / Gson because the +/// build container does not yet bundle either jar. The walker returns +/// a `java.util.List` / `java.util.Map` / `String` / `Long` / `Double` +/// / `Boolean` / null tree the harness then iterates over via an +/// explicit stack to compute the observed max nesting depth. +fn nyx_json_probe_source() -> &'static str { + r#"// Auto-generated by nyx_scanner::dynamic::lang::java::emit_json_parse_harness. +// Hand-rolled iterative JSON parser so the Phase 11 JSON_PARSE harness +// can run without a Jackson / Gson classpath dep. + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NyxJsonProbe { + public static final int MAX_PARSE_DEPTH = 4096; + public static final int MAX_WALK = 4096; + + public static class NyxJsonDepthException extends RuntimeException { + public NyxJsonDepthException(String msg) { super(msg); } + } + + public static class NyxJsonParseException extends RuntimeException { + public NyxJsonParseException(String msg) { super(msg); } + } + + public static Object parse(String s) { + if (s == null) return null; + State st = new State(s); + st.skipWs(); + Object v = parseValue(st, 1); + st.skipWs(); + return v; + } + + private static Object parseValue(State st, int depth) { + if (depth > MAX_PARSE_DEPTH) { + throw new NyxJsonDepthException("max depth " + MAX_PARSE_DEPTH + " exceeded"); + } + st.skipWs(); + if (st.pos >= st.src.length()) { + throw new NyxJsonParseException("unexpected EOF"); + } + char c = st.src.charAt(st.pos); + if (c == '[') { + st.pos++; + List arr = new ArrayList<>(); + st.skipWs(); + if (st.pos < st.src.length() && st.src.charAt(st.pos) == ']') { + st.pos++; + return arr; + } + while (true) { + arr.add(parseValue(st, depth + 1)); + st.skipWs(); + if (st.pos >= st.src.length()) { + throw new NyxJsonParseException("unterminated array"); + } + char d = st.src.charAt(st.pos); + if (d == ',') { + st.pos++; + continue; + } + if (d == ']') { + st.pos++; + return arr; + } + throw new NyxJsonParseException("expected , or ] in array"); + } + } + if (c == '{') { + st.pos++; + Map obj = new HashMap<>(); + st.skipWs(); + if (st.pos < st.src.length() && st.src.charAt(st.pos) == '}') { + st.pos++; + return obj; + } + while (true) { + st.skipWs(); + String key = parseString(st); + st.skipWs(); + if (st.pos >= st.src.length() || st.src.charAt(st.pos) != ':') { + throw new NyxJsonParseException("expected : in object"); + } + st.pos++; + Object v = parseValue(st, depth + 1); + obj.put(key, v); + st.skipWs(); + if (st.pos >= st.src.length()) { + throw new NyxJsonParseException("unterminated object"); + } + char d = st.src.charAt(st.pos); + if (d == ',') { + st.pos++; + continue; + } + if (d == '}') { + st.pos++; + return obj; + } + throw new NyxJsonParseException("expected , or } in object"); + } + } + if (c == '"') return parseString(st); + if (c == 't' || c == 'f' || c == 'n') return parseLiteral(st); + if (c == '-' || (c >= '0' && c <= '9')) return parseNumber(st); + throw new NyxJsonParseException("unexpected char " + c + " at " + st.pos); + } + + private static String parseString(State st) { + if (st.pos >= st.src.length() || st.src.charAt(st.pos) != '"') { + throw new NyxJsonParseException("expected string"); + } + st.pos++; + StringBuilder sb = new StringBuilder(); + while (st.pos < st.src.length()) { + char c = st.src.charAt(st.pos++); + if (c == '"') return sb.toString(); + if (c == '\\') { + if (st.pos >= st.src.length()) { + throw new NyxJsonParseException("trailing escape"); + } + char e = st.src.charAt(st.pos++); + switch (e) { + case '"': sb.append('"'); break; + case '\\': sb.append('\\'); break; + case '/': sb.append('/'); break; + case 'n': sb.append('\n'); break; + case 't': sb.append('\t'); break; + case 'r': sb.append('\r'); break; + case 'b': sb.append('\b'); break; + case 'f': sb.append('\f'); break; + case 'u': + if (st.pos + 4 > st.src.length()) { + throw new NyxJsonParseException("bad unicode escape"); + } + int code = Integer.parseInt(st.src.substring(st.pos, st.pos + 4), 16); + sb.append((char) code); + st.pos += 4; + break; + default: + sb.append(e); + } + } else { + sb.append(c); + } + } + throw new NyxJsonParseException("unterminated string"); + } + + private static Object parseLiteral(State st) { + if (st.src.startsWith("true", st.pos)) { st.pos += 4; return Boolean.TRUE; } + if (st.src.startsWith("false", st.pos)) { st.pos += 5; return Boolean.FALSE; } + if (st.src.startsWith("null", st.pos)) { st.pos += 4; return null; } + throw new NyxJsonParseException("bad literal at " + st.pos); + } + + private static Object parseNumber(State st) { + int start = st.pos; + if (st.src.charAt(st.pos) == '-') st.pos++; + boolean isFloat = false; + while (st.pos < st.src.length()) { + char c = st.src.charAt(st.pos); + if ((c >= '0' && c <= '9') || c == '+' || c == '-') { + st.pos++; + } else if (c == '.' || c == 'e' || c == 'E') { + isFloat = true; + st.pos++; + } else { + break; + } + } + String num = st.src.substring(start, st.pos); + try { + if (isFloat) return Double.parseDouble(num); + return Long.parseLong(num); + } catch (NumberFormatException e) { + throw new NyxJsonParseException("bad number: " + num); + } + } + + public static int countDepth(Object parsed) { + if (parsed == null) return 0; + ArrayDeque stack = new ArrayDeque<>(); + stack.push(new Frame(parsed, 1)); + int maxDepth = 0; + int visited = 0; + while (!stack.isEmpty()) { + Frame f = stack.pop(); + visited++; + if (visited > MAX_WALK) break; + if (f.depth > maxDepth) maxDepth = f.depth; + if (f.value instanceof List) { + for (Object child : (List) f.value) { + stack.push(new Frame(child, f.depth + 1)); + } + } else if (f.value instanceof Map) { + for (Object child : ((Map) f.value).values()) { + stack.push(new Frame(child, f.depth + 1)); + } + } + } + return maxDepth; + } + + private static final class State { + final String src; + int pos; + State(String s) { this.src = s; this.pos = 0; } + void skipWs() { + while (pos < src.length()) { + char c = src.charAt(pos); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++; + else break; + } + } + } + + private static final class Frame { + final Object value; + final int depth; + Frame(Object v, int d) { this.value = v; this.depth = d; } + } +} +"# +} + /// Stage the `javax.servlet.*` / `jakarta.servlet.*` stub bundle when /// the entry source imports either namespace. Phase 08 / 09 fixtures /// (`HttpServletResponse.setHeader` / `.sendRedirect`) carry the @@ -4318,4 +4676,141 @@ mod tests { "Java CRYPTO harness must catch the reflective lookup exceptions and route to the fallback", ); } + + // ── Phase 11 (Track J.9) Java JSON_PARSE emitter tests ──────────────────── + + fn make_json_parse_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::JSON_PARSE; + spec.entry_file = entry_file.to_owned(); + spec.entry_name = entry_name.to_owned(); + spec + } + + #[test] + fn emit_dispatches_to_json_parse_harness_when_cap_is_json_parse() { + let h = emit(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )) + .unwrap(); + assert!( + h.source.contains("nyxJsonParseProbe"), + "dispatcher must short-circuit Cap::JSON_PARSE into emit_json_parse_harness so the depth probe shim is present", + ); + assert!( + h.source.contains("\\\"kind\\\":\\\"JsonParse\\\""), + "Java JSON_PARSE harness must record probes with kind: JsonParse so the JsonParseExcessiveDepth predicate fires", + ); + } + + #[test] + fn emit_json_parse_harness_ships_nyx_json_probe_extra_file() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )); + assert!( + h.extra_files + .iter() + .any(|(name, _)| name == "NyxJsonProbe.java"), + "Java JSON_PARSE harness must stage NyxJsonProbe.java as a sibling extra file so the fixture can resolve the helper at javac time without Jackson / Gson", + ); + let (_, probe_src) = h + .extra_files + .iter() + .find(|(name, _)| name == "NyxJsonProbe.java") + .unwrap(); + assert!( + probe_src.contains("public class NyxJsonProbe"), + "NyxJsonProbe.java extra file must declare the helper class", + ); + assert!( + probe_src.contains("public static Object parse(String s)"), + "NyxJsonProbe must expose a String -> Object parse helper", + ); + assert!( + probe_src.contains("public static int countDepth(Object parsed)"), + "NyxJsonProbe must expose an iterative countDepth walker", + ); + assert!( + probe_src.contains("ArrayDeque"), + "NyxJsonProbe.countDepth must walk iteratively to dodge the JVM stack-frame budget", + ); + } + + #[test] + fn emit_json_parse_harness_routes_through_reflective_entry_invocation() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("Class.forName(\"Vuln\")"), + "Java JSON_PARSE harness must reflectively load the fixture entry class by its derived FQN: {}", + h.source + ); + assert!( + h.source + .contains("getDeclaredMethod(\"run\", String.class)"), + "Java JSON_PARSE harness must look up the entry method with a single String parameter", + ); + assert!( + h.source.contains("m.invoke(null, payload)"), + "Java JSON_PARSE harness must invoke the static method with the payload", + ); + assert!( + h.source.contains("NyxJsonProbe.countDepth(produced)"), + "Java JSON_PARSE harness must drive countDepth on the fixture's return value", + ); + assert_eq!( + h.filename, "NyxHarness.java", + "Java JSON_PARSE harness must emit a NyxHarness.java file", + ); + } + + #[test] + fn emit_json_parse_harness_emits_depth_fields() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("\\\"depth\\\":"), + "Java JSON_PARSE harness must serialise a depth field on the JsonParse probe record", + ); + assert!( + h.source.contains("\\\"excessive_depth\\\":"), + "Java JSON_PARSE harness must serialise an excessive_depth field on the JsonParse probe record", + ); + assert!( + h.source.contains("__NYX_SINK_HIT__"), + "Java JSON_PARSE harness must print the universal sink-hit sentinel", + ); + } + + #[test] + fn emit_json_parse_harness_handles_parser_depth_exception() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("NyxJsonProbe.NyxJsonDepthException"), + "Java JSON_PARSE harness must catch the parser's depth-budget exception so a guard-rail trip still emits a probe", + ); + } + + #[test] + fn emit_json_parse_harness_derives_entry_class_from_fixture() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/java/Vuln.java", + "run", + )); + assert!( + matches!(h.entry_subpath.as_deref(), Some(p) if p == "Vuln.java"), + "Java JSON_PARSE harness must stage the fixture under its public-class-derived filename so javac's filename invariant holds: got {:?}", + h.entry_subpath, + ); + } } diff --git a/tests/dynamic_fixtures/json_parse_depth/java/Vuln.java b/tests/dynamic_fixtures/json_parse_depth/java/Vuln.java new file mode 100644 index 00000000..bc01bf2f --- /dev/null +++ b/tests/dynamic_fixtures/json_parse_depth/java/Vuln.java @@ -0,0 +1,33 @@ +// Java JSON_PARSE depth-bomb vuln fixture. +// +// Models a config-driven JSON ingest endpoint that picks the parser +// input based on the request payload tag - `*_DEEP` routes through a +// deeply-nested array literal (256 levels) that drives the parser past +// the 64-level depth budget; `*_SHALLOW` routes through a flat `[]` +// parse that leaves the predicate clear. This shape is needed by the +// differential runner: the vuln-payload attempt and the benign-control +// attempt both load the same fixture, and only the payload-routed +// deep branch trips the `JsonParseExcessiveDepth` predicate. +// +// Java has no stdlib JSON parser. The harness ships a hand-rolled +// iterative `NyxJsonProbe.parse(String)` helper alongside `NyxHarness` +// so the fixture does not need to link Jackson / Gson at build time. +// The helper returns a `java.util.List` / `java.util.Map` tree the +// harness then walks via `NyxJsonProbe.countDepth(Object)` to produce +// the `ProbeKind::JsonParse { depth }` record. +public class Vuln { + public static Object run(String value) { + String text = value == null ? "" : value; + if (text.contains("DEEP")) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 256; i++) { + sb.append('['); + } + for (int i = 0; i < 256; i++) { + sb.append(']'); + } + return NyxJsonProbe.parse(sb.toString()); + } + return NyxJsonProbe.parse("[]"); + } +} diff --git a/tests/json_parse_corpus.rs b/tests/json_parse_corpus.rs index 5eb93f5b..ace85007 100644 --- a/tests/json_parse_corpus.rs +++ b/tests/json_parse_corpus.rs @@ -27,6 +27,7 @@ const LANGS: &[Lang] = &[ Lang::Php, Lang::Go, Lang::Rust, + Lang::Java, ]; /// Subset of [`LANGS`] whose JSON parser has a prototype-pollution @@ -181,7 +182,8 @@ mod e2e_json_parse_depth { Lang::Php => "php", Lang::Go => "go", Lang::Rust => "rust", - _ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust only"), + Lang::Java => "java", + _ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"), }) .join(fixture); let tmp = TempDir::new().expect("create tempdir"); @@ -227,7 +229,8 @@ mod e2e_json_parse_depth { Lang::Php => "php", Lang::Go => "go", Lang::Rust => "cargo", - _ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust only"), + Lang::Java => "javac", + _ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"), }; if !command_available(required) { eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}"); @@ -310,11 +313,19 @@ mod e2e_json_parse_depth { }; assert_confirmed(Lang::Rust, &outcome); } + + #[test] + fn java_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; + assert_confirmed(Lang::Java, &outcome); + } } #[test] fn json_parse_unsupported_for_other_langs() { - for lang in [Lang::C, Lang::Cpp, Lang::Java, Lang::TypeScript] { + for lang in [Lang::C, Lang::Cpp, Lang::TypeScript] { assert!( payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(), "JSON_PARSE has unexpected payloads for {lang:?}",