2026-05-12 02:20:55 -04:00
//! 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.
2026-05-14 03:45:51 -05:00
//! - Other slots produce `UnsupportedReason::PayloadSlotUnsupported`.
2026-05-12 02:20:55 -04:00
//!
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
2026-05-14 03:22:30 -05:00
use crate ::dynamic ::lang ::{ HarnessSource , LangEmitter } ;
use crate ::dynamic ::spec ::{ EntryKind , HarnessSpec , PayloadSlot } ;
2026-05-12 02:20:55 -04:00
use crate ::evidence ::UnsupportedReason ;
2026-05-14 03:22:30 -05:00
/// 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 < HarnessSource , UnsupportedReason > {
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 "
)
}
}
2026-05-14 05:35:28 -05:00
/// Source of the `__nyx_probe` shim for the Java harness (Phase 06 —
/// Track C.1).
///
/// Splices into the generated harness class as a `static void __nyx_probe(...)`
/// method. Hand-rolled JSON keeps the shim free of org.json / jackson
/// dependencies; matches the
/// [`crate::dynamic::probe::SinkProbe`] wire format.
pub fn probe_shim ( ) -> & 'static str {
r #"
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
static void __nyx_probe ( String sinkCallee , String .. . args ) {
String p = System . getenv ( " NYX_PROBE_PATH " ) ;
if ( p = = null | | p . isEmpty ( ) ) {
return ;
}
long now = System . nanoTime ( ) ;
String payloadId = System . getenv ( " NYX_PAYLOAD_ID " ) ;
if ( payloadId = = null ) payloadId = " " ;
StringBuilder line = new StringBuilder ( 128 ) ;
line . append ( " { \" sink_callee \" : \" " ) ;
nyxJsonEscape ( sinkCallee , line ) ;
line . append ( " \" , \" args \" :[ " ) ;
for ( int i = 0 ; i < args . length ; i + + ) {
if ( i > 0 ) line . append ( ',' ) ;
line . append ( " { \" kind \" : \" String \" , \" value \" : \" " ) ;
nyxJsonEscape ( args [ i ] = = null ? " " : args [ i ] , line ) ;
line . append ( " \" } " ) ;
}
line . append ( " ], \" captured_at_ns \" : " ) . append ( now ) . append ( " , \" payload_id \" : \" " ) ;
nyxJsonEscape ( payloadId , line ) ;
line . append ( " \" } \n " ) ;
try ( java . io . FileWriter fw = new java . io . FileWriter ( p , true ) ) {
fw . write ( line . toString ( ) ) ;
} catch ( java . io . IOException e ) {
// best-effort
}
}
private static void nyxJsonEscape ( String s , StringBuilder out ) {
for ( int i = 0 ; i < s . length ( ) ; i + + ) {
char c = s . charAt ( i ) ;
switch ( c ) {
case '"' : out . append ( " \\ \" " ) ; break ;
case '\\' : out . append ( " \\ \\ " ) ; break ;
case '\n' : out . append ( " \\ n " ) ; break ;
case '\r' : out . append ( " \\ r " ) ; break ;
case '\t' : out . append ( " \\ t " ) ; break ;
default :
if ( c < 0x20 ) {
out . append ( String . format ( " \\ u%04x " , ( int ) c ) ) ;
} else {
out . append ( c ) ;
}
}
}
}
" #
}
2026-05-12 02:20:55 -04:00
/// Emit a Java harness for `spec`.
pub fn emit ( spec : & HarnessSpec ) -> Result < HarnessSource , UnsupportedReason > {
match & spec . payload_slot {
PayloadSlot ::Param ( 0 ) | PayloadSlot ::EnvVar ( _ ) = > { }
2026-05-14 03:45:51 -05:00
_ = > return Err ( UnsupportedReason ::PayloadSlotUnsupported ) ,
2026-05-12 02:20:55 -04:00
}
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 ( ) ,
2026-05-13 13:03:44 -04:00
derivation : crate ::dynamic ::spec ::SpecDerivationStrategy ::FromFlowSteps ,
2026-05-12 02:20:55 -04:00
}
}
#[ 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 ( ) ;
2026-05-14 03:45:51 -05:00
assert_eq! ( err , UnsupportedReason ::PayloadSlotUnsupported ) ;
2026-05-12 02:20:55 -04:00
}
#[ test ]
fn emit_stdin_is_unsupported ( ) {
let spec = make_spec ( PayloadSlot ::Stdin ) ;
let err = emit ( & spec ) . unwrap_err ( ) ;
2026-05-14 03:45:51 -05:00
assert_eq! ( err , UnsupportedReason ::PayloadSlotUnsupported ) ;
2026-05-12 02:20:55 -04:00
}
2026-05-14 03:22:30 -05:00
#[ 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 " ) ) ;
}
2026-05-12 02:20:55 -04:00
#[ 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 " ) ) ;
}
}