2026-05-12 02:20:55 -04:00
//! Go harness emitter.
//!
//! Generates a Go `main` package that:
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
//! 2. Imports the entry package from `./entry/` and calls the entry function.
//! 3. Uses `runtime.Caller`-style wrapping in fixtures for sink-reachability
//! probes (fixtures explicitly emit `__NYX_SINK_HIT__` before the sink).
//!
//! Build step: `prepare_go()` in `build_sandbox.rs` runs `go build -o nyx_harness .`
//! in the workdir. The harness command is updated to the compiled binary path.
//!
//! File layout in workdir:
//! ```text
//! main.go ← harness entry point (generated)
//! go.mod ← module definition (generated)
//! entry/
//! entry.go ← entry function (copied from project; must have `package entry`)
//! ```
//!
//! Payload slot support:
//! - `PayloadSlot::Param(0)` — pass payload as `string` first argument.
//! - `PayloadSlot::EnvVar(name)` — set env var 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-go:{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 Go. Method bodies delegate to the
/// existing free functions in this module.
pub struct GoEmitter ;
/// Entry kinds the Go emitter currently understands. Extended in Phase 15
/// (Track B Go vertical) to include `HttpRoute` (`net/http`, gin) and CLI
/// (`flag.Parse`) shapes.
const SUPPORTED : & [ EntryKind ] = & [ EntryKind ::Function ] ;
impl LangEmitter for GoEmitter {
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! (
" go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add net/http, gin, flag.Parse shapes in phase 15 "
)
}
}
2026-05-14 05:35:28 -05:00
/// Source of the `__nyx_probe` shim for the Go harness (Phase 06 —
/// Track C.1). Variadic over `string` so callers can pass any number of
/// captured args at the sink site.
pub fn probe_shim ( ) -> & 'static str {
r #"
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
func __nyx_probe ( sinkCallee string , args .. . string ) {
p := os . Getenv ( " NYX_PROBE_PATH " )
if p = = " " {
return
}
serArgs := make ( [ ] map [ string ] interface { } , 0 , len ( args ) )
for _ , a := range args {
serArgs = append ( serArgs , map [ string ] interface { } {
" kind " : " String " ,
" value " : a ,
} )
}
rec := map [ string ] interface { } {
" sink_callee " : sinkCallee ,
" args " : serArgs ,
" captured_at_ns " : uint64 ( time . Now ( ) . UnixNano ( ) ) ,
" payload_id " : os . Getenv ( " NYX_PAYLOAD_ID " ) ,
}
b , err := json . Marshal ( rec )
if err ! = nil {
return
}
f , err := os . OpenFile ( p , os . O_APPEND | os . O_CREATE | os . O_WRONLY , 0644 )
if err ! = nil {
return
}
defer f . Close ( )
f . Write ( b )
f . Write ( [ ] byte ( " \n " ) )
}
" #
}
2026-05-12 02:20:55 -04:00
/// Emit a Go 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 main_go = generate_main_go ( spec ) ;
let go_mod = generate_go_mod ( ) ;
Ok ( HarnessSource {
source : main_go ,
filename : " main.go " . to_owned ( ) ,
command : vec ! [ " ./nyx_harness " . to_owned ( ) ] ,
extra_files : vec ! [ ( " go.mod " . to_owned ( ) , go_mod ) ] ,
entry_subpath : Some ( " entry/entry.go " . to_owned ( ) ) ,
} )
}
fn generate_main_go ( spec : & HarnessSpec ) -> String {
let entry_fn = capitalize_first ( & spec . entry_name ) ;
let ( pre_call , call_expr ) = build_call ( spec , & entry_fn ) ;
// Determine which imports are needed.
let env_import = if matches! ( & spec . payload_slot , PayloadSlot ::EnvVar ( _ ) ) {
" "
} else {
" "
} ;
let _ = env_import ;
format! (
r #" // Nyx dynamic harness — auto-generated, do not edit.
package main
import (
" encoding/base64 "
" fmt "
" os "
" nyx-harness/entry "
)
func main ( ) { {
payload := nyxPayload ( )
{ pre_call } { call_expr }
_ = fmt . Sprintf ( " " ) // suppress unused import if call_expr uses fmt directly
_ = os . Stderr // suppress unused import
} }
func nyxPayload ( ) string { {
if v := os . Getenv ( " NYX_PAYLOAD " ) ; v ! = " " { {
return v
} }
if b64 := os . Getenv ( " NYX_PAYLOAD_B64 " ) ; b64 ! = " " { {
if data , err := base64 . StdEncoding . DecodeString ( b64 ) ; err = = nil { {
return string ( data )
} }
} }
return " "
} }
" #,
pre_call = pre_call ,
call_expr = call_expr ,
)
}
fn generate_go_mod ( ) -> String {
" module nyx-harness \n \n go 1.21 \n " . to_owned ( )
}
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
fn build_call ( spec : & HarnessSpec , entry_fn : & str ) -> ( String , String ) {
match & spec . payload_slot {
PayloadSlot ::Param ( 0 ) = > {
let pre = String ::new ( ) ;
let call = format! ( " entry. {entry_fn} (payload) " ) ;
( pre , call )
}
PayloadSlot ::EnvVar ( name ) = > {
let pre = format! ( " \t os.Setenv( {name:?} , payload) \n " ) ;
let call = format! ( " entry. {entry_fn} () " ) ;
( pre , call )
}
_ = > {
let pre = String ::new ( ) ;
let call = format! ( " entry. {entry_fn} (payload) " ) ;
( pre , call )
}
}
}
/// Capitalize the first character of a string (Go exported names must start uppercase).
pub fn capitalize_first ( s : & str ) -> String {
let mut c = s . chars ( ) ;
match c . next ( ) {
None = > String ::new ( ) ,
Some ( f ) = > f . to_uppercase ( ) . collect ::< String > ( ) + c . as_str ( ) ,
}
}
#[ 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 : " go0000000000001 " . into ( ) ,
entry_file : " cmd/server/main.go " . into ( ) ,
entry_name : " handleRequest " . into ( ) ,
entry_kind : EntryKind ::Function ,
lang : Lang ::Go ,
toolchain_id : " go-stable " . into ( ) ,
payload_slot ,
expected_cap : Cap ::SQL_QUERY ,
constraint_hints : vec ! [ ] ,
sink_file : " cmd/server/main.go " . into ( ) ,
sink_line : 20 ,
spec_hash : " go0000000000001 " . 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 ( " nyx-harness/entry " ) ) ;
assert! ( harness . source . contains ( " nyxPayload() " ) ) ;
assert! ( harness . source . contains ( " entry.HandleRequest(payload) " ) ) ;
assert_eq! ( harness . filename , " main.go " ) ;
assert_eq! ( harness . command , vec! [ " ./nyx_harness " ] ) ;
}
#[ test ]
fn emit_includes_go_mod_in_extra_files ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 0 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
let go_mod = harness . extra_files . iter ( ) . find ( | ( n , _ ) | n = = " go.mod " ) ;
assert! ( go_mod . is_some ( ) , " go.mod must be in extra_files " ) ;
assert! ( go_mod . unwrap ( ) . 1. contains ( " module nyx-harness " ) ) ;
}
#[ test ]
fn emit_entry_subpath_is_entry_go ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 0 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert_eq! ( harness . entry_subpath , Some ( " entry/entry.go " . to_owned ( ) ) ) ;
}
#[ test ]
fn emit_env_var_slot ( ) {
let spec = make_spec ( PayloadSlot ::EnvVar ( " DB_USER " . into ( ) ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " os.Setenv " ) ) ;
assert! ( harness . source . contains ( " \" DB_USER \" " ) ) ;
}
#[ 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! ( ! GoEmitter . entry_kinds_supported ( ) . is_empty ( ) ) ;
assert! ( GoEmitter . entry_kinds_supported ( ) . contains ( & EntryKind ::Function ) ) ;
}
#[ test ]
fn entry_kind_hint_names_attempted_and_phase ( ) {
let hint = GoEmitter . entry_kind_hint ( EntryKind ::HttpRoute ) ;
assert! ( hint . contains ( " HttpRoute " ) ) ;
assert! ( hint . contains ( " phase 15 " ) ) ;
}
2026-05-12 02:20:55 -04:00
#[ test ]
fn capitalize_first_handles_lowercase ( ) {
assert_eq! ( capitalize_first ( " handleRequest " ) , " HandleRequest " ) ;
assert_eq! ( capitalize_first ( " run " ) , " Run " ) ;
assert_eq! ( capitalize_first ( " " ) , " " ) ;
assert_eq! ( capitalize_first ( " A " ) , " A " ) ;
}
#[ test ]
fn go_mod_has_correct_module ( ) {
let go_mod = generate_go_mod ( ) ;
assert! ( go_mod . contains ( " module nyx-harness " ) ) ;
assert! ( go_mod . contains ( " go 1.21 " ) ) ;
}
}