//! PHP harness emitter. //! //! Generates a PHP script that: //! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars. //! 2. Includes the entry file (`entry.php`) from the workdir. //! 3. Calls the entry function with the payload routed to the correct slot. //! 4. Catches all Throwables to prevent harness crashes from masking results. //! //! Sink-reachability probe: fixtures explicitly emit `__NYX_SINK_HIT__` before //! the actual sink call (same pattern as Rust / JS fixtures). //! //! Payload slot support: //! - `PayloadSlot::Param(n)` — n-th positional argument. //! - `PayloadSlot::EnvVar(name)` — set `$_ENV`/`putenv()` before calling. //! - `PayloadSlot::Stdin` — wrap `STDIN` with the payload. //! - Other slots produce `UnsupportedReason::EntryKindUnsupported`. //! //! Build: no compilation step. Command is `php harness.php`. //! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1). use crate::dynamic::lang::HarnessSource; use crate::dynamic::spec::{HarnessSpec, PayloadSlot}; use crate::evidence::UnsupportedReason; /// Emit a PHP harness for `spec`. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::Stdin => {} _ => return Err(UnsupportedReason::EntryKindUnsupported), } let source = generate_source(spec); Ok(HarnessSource { source, filename: "harness.php".to_owned(), command: vec!["php".to_owned(), "harness.php".to_owned()], extra_files: vec![], entry_subpath: Some("entry.php".to_owned()), }) } fn generate_source(spec: &HarnessSpec) -> String { let entry_fn = &spec.entry_name; let (pre_call, call_expr) = build_call(spec, entry_fn); format!( r#"getMessage() . "\n"); exit(77); }} // ── Pre-call setup ───────────────────────────────────────────────────────────── {pre_call} // ── Call entry point ────────────────────────────────────────────────────────── try {{ $result = {call_expr}; if ($result !== null) {{ echo $result . "\n"; }} }} catch (Throwable $e) {{ fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n"); }} "#, pre_call = pre_call, call_expr = call_expr, ) } /// Build `(pre_call_setup, call_expression)` for the chosen payload slot. fn build_call(spec: &HarnessSpec, func: &str) -> (String, String) { match &spec.payload_slot { PayloadSlot::Param(idx) => { let pre = String::new(); let call = if *idx == 0 { format!("{func}($payload)") } else { let pads = (0..*idx).map(|_| "''").collect::>().join(", "); format!("{func}({pads}, $payload)") }; (pre, call) } PayloadSlot::EnvVar(name) => { let pre = format!("putenv({name:?} . '=' . $payload);\n$_ENV[{name:?}] = $payload;\n"); let call = format!("{func}()"); (pre, call) } PayloadSlot::Stdin => { // Replace STDIN with an in-memory stream containing the payload. let pre = "if (defined('STDIN')) {\n $stream = fopen('php://memory', 'r+');\n fwrite($stream, $payload);\n rewind($stream);\n // Note: STDIN reassignment is not portable; fixture reads via fgets(STDIN).\n}\n".to_owned(); let call = format!("{func}()"); (pre, call) } _ => { let pre = String::new(); let call = format!("{func}($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: "php0000000000001".into(), entry_file: "src/login.php".into(), entry_name: "login".into(), entry_kind: EntryKind::Function, lang: Lang::Php, toolchain_id: "php-8".into(), payload_slot, expected_cap: Cap::SQL_QUERY, constraint_hints: vec![], sink_file: "src/login.php".into(), sink_line: 10, spec_hash: "php0000000000001".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.starts_with("