//! Java harness emitter. //! //! Generates a Java `NyxHarness.java` that: //! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars. //! 2. Calls `Entry.{entry_name}(payload)` from the co-located `Entry.java`. //! 3. Catches all exceptions to prevent harness crashes from masking results. //! //! Sink-reachability probe: fixtures explicitly emit `System.out.println("__NYX_SINK_HIT__")` //! before the actual sink call (same pattern as Rust and Go fixtures). //! //! Build step: `prepare_java()` in `build_sandbox.rs` runs `javac NyxHarness.java Entry.java` //! in the workdir. The compiled `.class` files land in the workdir. //! //! File layout in workdir: //! ```text //! NyxHarness.java ← harness main class (generated) //! Entry.java ← entry class (copied from project) //! NyxHarness.class ← compiled by prepare_java() //! Entry.class ← compiled by prepare_java() //! ``` //! //! Payload slot support: //! - `PayloadSlot::Param(0)` — pass payload as `String` first argument. //! - `PayloadSlot::EnvVar(name)` — set system property before calling entry. //! - Other slots produce `UnsupportedReason::EntryKindUnsupported`. //! //! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1). use crate::dynamic::lang::{HarnessSource, LangEmitter}; use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; use crate::evidence::UnsupportedReason; /// Zero-sized [`LangEmitter`] handle for Java. Method bodies delegate to the /// existing free functions in this module. pub struct JavaEmitter; /// Entry kinds the Java emitter currently understands. Extended in Phase 14 /// (Track B Java vertical) to include `HttpRoute` (servlet / Spring / /// Quarkus) and JUnit static-method shapes. const SUPPORTED: &[EntryKind] = &[EntryKind::Function]; impl LangEmitter for JavaEmitter { fn emit(&self, spec: &HarnessSpec) -> Result { emit(spec) } fn entry_kinds_supported(&self) -> &'static [EntryKind] { SUPPORTED } fn entry_kind_hint(&self, attempted: EntryKind) -> String { format!( "java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add servlet / Spring / Quarkus shapes in phase 14" ) } } /// Emit a Java harness for `spec`. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {} _ => return Err(UnsupportedReason::EntryKindUnsupported), } let source = generate_harness_java(spec); Ok(HarnessSource { source, filename: "NyxHarness.java".to_owned(), // Use absolute workdir classpath set by runner.rs after compilation. // Before runner.rs updates it, '.' works for process backend when run // from the workdir. command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![], entry_subpath: Some("Entry.java".to_owned()), }) } fn generate_harness_java(spec: &HarnessSpec) -> String { let entry_method = &spec.entry_name; let (pre_call, call_expr) = build_call(spec, entry_method); format!( r#"// Nyx dynamic harness — auto-generated, do not edit. public class NyxHarness {{ public static void main(String[] args) throws Exception {{ String payload = nyxPayload(); {pre_call} try {{ {call_expr} }} catch (Exception e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) {{ return v; }} String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, pre_call = pre_call, call_expr = call_expr, ) } /// Build `(pre_call_setup, call_expression)` for the chosen payload slot. fn build_call(spec: &HarnessSpec, method: &str) -> (String, String) { match &spec.payload_slot { PayloadSlot::Param(0) => { let pre = String::new(); let call = format!("Entry.{method}(payload);"); (pre, call) } PayloadSlot::EnvVar(name) => { // Use System.setProperty since env vars cannot be set post-JVM-launch // via standard Java APIs. Fixtures that read env vars must use // System.getProperty as a fallback, or read NYX_PAYLOAD_PROP_{name}. let pre = format!( " System.setProperty({name:?}, payload);\n" ); let call = format!("Entry.{method}();"); (pre, call) } _ => { let pre = String::new(); let call = format!("Entry.{method}(payload);"); (pre, call) } } } #[cfg(test)] mod tests { use super::*; use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; use crate::labels::Cap; use crate::symbol::Lang; fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec { HarnessSpec { finding_id: "java00000000001".into(), entry_file: "src/main/java/App.java".into(), entry_name: "processInput".into(), entry_kind: EntryKind::Function, lang: Lang::Java, toolchain_id: "java-21".into(), payload_slot, expected_cap: Cap::SQL_QUERY, constraint_hints: vec![], sink_file: "src/main/java/App.java".into(), sink_line: 25, spec_hash: "java00000000001".into(), derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps, } } #[test] fn emit_produces_source() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("public class NyxHarness")); assert!(harness.source.contains("nyxPayload()")); assert!(harness.source.contains("Entry.processInput(payload)")); assert_eq!(harness.filename, "NyxHarness.java"); assert_eq!(harness.command, vec!["java", "-cp", ".", "NyxHarness"]); } #[test] fn emit_entry_subpath_is_entry_java() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned())); } #[test] fn emit_env_var_slot() { let spec = make_spec(PayloadSlot::EnvVar("DB_PASSWORD".into())); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("System.setProperty")); assert!(harness.source.contains("\"DB_PASSWORD\"")); } #[test] fn emit_param_gt_0_is_unsupported() { let spec = make_spec(PayloadSlot::Param(1)); let err = emit(&spec).unwrap_err(); assert_eq!(err, UnsupportedReason::EntryKindUnsupported); } #[test] fn emit_stdin_is_unsupported() { let spec = make_spec(PayloadSlot::Stdin); let err = emit(&spec).unwrap_err(); assert_eq!(err, UnsupportedReason::EntryKindUnsupported); } #[test] fn entry_kinds_supported_is_non_empty() { assert!(!JavaEmitter.entry_kinds_supported().is_empty()); assert!(JavaEmitter .entry_kinds_supported() .contains(&EntryKind::Function)); } #[test] fn entry_kind_hint_names_attempted_and_phase() { let hint = JavaEmitter.entry_kind_hint(EntryKind::HttpRoute); assert!(hint.contains("HttpRoute")); assert!(hint.contains("phase 14")); } #[test] fn harness_has_base64_decoder() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("Base64.getDecoder()")); assert!(harness.source.contains("NYX_PAYLOAD_B64")); } }