refactor(dynamic): replace PHP route stubs with framework-aware route replay logic for Laravel and Symfony, enhance helper functions, and update related test fixtures

This commit is contained in:
elipeter 2026-05-26 14:19:01 -05:00
parent aaf49acefb
commit ed398e2834
14 changed files with 835 additions and 345 deletions

View file

@ -544,10 +544,14 @@ impl RustShape {
let has_actix_strong = source.contains("use actix_web")
|| source.contains("actix_web::")
|| source.contains("// nyx-shape: actix");
let has_axum_strong = source.contains("use axum::")
let has_axum_import = source.contains("use axum::")
|| source.contains("axum::Router")
|| source.contains("axum::routing");
let has_axum_route = source.contains("axum::Router")
|| source.contains("Router::new")
|| source.contains("axum::routing")
|| source.contains("// nyx-shape: axum");
|| source.contains("// nyx-shape: axum")
|| source.contains("// nyx-shape: axum-route");
let has_attribute_route = source.contains("#[get(")
|| source.contains("#[post(")
|| source.contains("#[put(")
@ -578,13 +582,14 @@ impl RustShape {
Self::ActixWebRoute
};
}
if has_axum_strong {
if has_axum_route {
return Self::AxumRoute;
}
// Legacy weak detectors: HttpResponse / IntoResponse may
// appear in code that does not import a known framework.
let has_actix_weak = source.contains("HttpResponse") || source.contains("HttpRequest");
let has_axum_weak = source.contains("IntoResponse")
let has_axum_weak = has_axum_import
|| source.contains("IntoResponse")
|| source.contains("Json(")
|| source.contains("Query(");
if has_axum_weak {
@ -2018,7 +2023,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
}
let cargo_toml = generate_cargo_toml(spec.expected_cap);
let cargo_toml = generate_cargo_toml_for_shape(spec.expected_cap, shape);
let main_rs = generate_main_rs(spec, shape);
Ok(HarnessSource {
@ -2045,6 +2050,7 @@ fn emit_class_method_harness(spec: &HarnessSpec, class: &str, method: &str) -> H
let entry_label = format!("{class}::{method}");
let entry_src = read_entry_source(&spec.entry_file);
let receiver_expr = rust_receiver_expr(&entry_src, class, 3);
let method_invocation = rust_class_method_invocation(&entry_src, class, method);
let body = format!(
r#"//! Nyx dynamic harness — class method (Phase 19 / Track M.1).
mod entry;
@ -2054,7 +2060,7 @@ fn main() {{
let _ = &payload;
__nyx_install_crash_guard("{entry_label}");
let instance = {receiver_expr};
let _ = instance.{method}(&payload);
{method_invocation}
println!("__NYX_SINK_HIT__");
}}
@ -2100,9 +2106,9 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
Some(out)
}}
"#,
method = method,
entry_label = entry_label,
receiver_expr = receiver_expr,
method_invocation = method_invocation,
);
HarnessSource {
source: body,
@ -2147,6 +2153,76 @@ fn class_has_new(entry_src: &str, class: &str) -> bool {
}
}
fn rust_class_method_invocation(entry_src: &str, class: &str, method: &str) -> String {
if rust_method_returns_printable_stdout(entry_src, class, method) {
format!(
"let __nyx_result = instance.{method}(&payload);\n print!(\"{{}}\", __nyx_result);"
)
} else {
format!("let _ = instance.{method}(&payload);")
}
}
fn rust_method_returns_printable_stdout(entry_src: &str, class: &str, method: &str) -> bool {
let impl_marker = format!("impl {class}");
let mut search_from = 0usize;
while let Some(rel) = entry_src[search_from..].find(&impl_marker) {
let impl_start = search_from + rel;
let after_class = impl_start + impl_marker.len();
if entry_src[after_class..]
.chars()
.next()
.is_some_and(is_ident_char)
{
search_from = after_class;
continue;
}
let Some(open_rel) = entry_src[after_class..].find('{') else {
return false;
};
let block_start = after_class + open_rel;
let Some(block) = balanced_block(&entry_src[block_start..]) else {
return false;
};
if method_body_returns_printable_stdout(&block[1..block.len() - 1], method) {
return true;
}
search_from = block_start + block.len();
}
false
}
fn method_body_returns_printable_stdout(impl_body: &str, method: &str) -> bool {
let method_marker = format!("fn {method}");
let mut search_from = 0usize;
while let Some(rel) = impl_body[search_from..].find(&method_marker) {
let method_start = search_from + rel;
let after_name = method_start + method_marker.len();
if impl_body[after_name..]
.chars()
.next()
.is_some_and(is_ident_char)
{
search_from = after_name;
continue;
}
let Some(body_open_rel) = impl_body[after_name..].find('{') else {
return false;
};
let sig = &impl_body[after_name..after_name + body_open_rel];
if let Some(ret) = sig.split("->").nth(1) {
let ret = ret.trim();
return ret.starts_with("String")
|| ret.starts_with("std::string::String")
|| ret.starts_with("&str")
|| ret.starts_with("&'static str")
|| ret.starts_with("& 'static str");
}
search_from = after_name;
}
false
}
fn rust_struct_literal(entry_src: &str, class: &str, depth: usize) -> Option<String> {
if depth == 0 {
return None;
@ -2456,6 +2532,10 @@ fn word_in_text(text: &str, kw: &str) -> bool {
false
}
fn is_ident_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}
/// Generate `Cargo.toml` for the harness crate.
///
/// Dependencies are driven by `expected_cap`:
@ -2470,6 +2550,27 @@ pub fn generate_cargo_toml(cap: Cap) -> String {
generate_cargo_toml_with_extras(cap, false)
}
fn generate_cargo_toml_for_shape(cap: Cap, shape: RustShape) -> String {
let mut cargo = generate_cargo_toml_with_extras(cap, false);
let deps = match shape {
RustShape::AxumRoute => Some(
"axum = \"0.7\"\nserde = { version = \"1\", features = [\"derive\"] }\ntokio = { version = \"1\", features = [\"full\"] }\ntower = { version = \"0.5\", features = [\"util\"] }\n",
),
RustShape::ActixRoute => {
Some("actix-web = \"4\"\nserde = { version = \"1\", features = [\"derive\"] }\n")
}
RustShape::RocketRoute => Some("rocket = \"0.5\"\n"),
RustShape::WarpRoute => Some(
"serde = { version = \"1\", features = [\"derive\"] }\ntokio = { version = \"1\", features = [\"full\"] }\nwarp = \"0.3\"\n",
),
_ => None,
};
if let Some(deps) = deps {
cargo.push_str(deps);
}
cargo
}
/// Variant of [`generate_cargo_toml`] that conditionally pulls in
/// `percent-encoding` for the HEADER_INJECTION benign control fixture
/// (it routes the value through `utf8_percent_encode` to land CRLF as
@ -2548,6 +2649,44 @@ fn nyx_payload() -> String {{
String::new()
}}
fn nyx_route_uri(path: &str, query: Option<&str>, payload: &str) -> String {{
let mut uri = if path.starts_with('/') {{
path.to_owned()
}} else {{
format!("/{{path}}")
}};
if let Some(name) = query {{
uri.push('?');
uri.push_str(name);
uri.push('=');
uri.push_str(&nyx_url_encode(payload));
}}
uri
}}
fn nyx_route_payload(payload: &str) -> String {{
let trimmed = payload.trim_start();
if trimmed.starts_with(';') || trimmed.starts_with("&&") || trimmed.starts_with("||") {{
format!("true {{payload}}")
}} else {{
payload.to_owned()
}}
}}
fn nyx_url_encode(input: &str) -> String {{
let mut out = String::with_capacity(input.len());
const HEX: &[u8; 16] = b"0123456789ABCDEF";
for b in input.bytes() {{
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {{
out.push(b as char);
}} else {{
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}}
}}
out
}}
/// Minimal base64 decoder (no external deps).
fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
const TABLE: [u8; 128] = {{
@ -2604,25 +2743,119 @@ fn build_call(spec: &HarnessSpec, func: &str, shape: RustShape) -> (String, Stri
}
RustShape::ActixWebRoute => actix_invocation(spec, func),
RustShape::AxumHandler => axum_invocation(spec, func),
// Phase 17 framework dispatchers. Each shape prints the
// matching toolchain marker before invoking the entry under
// the same reflective shim used by [`Self::ActixWebRoute`] /
// [`Self::AxumHandler`]. Real-framework bootstrap (full
// `Router` mount, `App::new`, `rocket::build`, `warp::serve`)
// is deferred behind the per-shape harness real-engine
// follow-up — see `.pitboss/play/deferred.md`.
RustShape::ActixRoute => framework_route_invocation(spec, func, "NYX_ACTIX_TEST=1"),
RustShape::AxumRoute => framework_route_invocation(spec, func, "NYX_AXUM_TEST=1"),
RustShape::RocketRoute => framework_route_invocation(spec, func, "NYX_ROCKET_TEST=1"),
RustShape::WarpRoute => framework_route_invocation(spec, func, "NYX_WARP_TEST=1"),
RustShape::ActixRoute => actix_route_invocation(spec, func),
RustShape::AxumRoute => axum_route_invocation(spec, func),
RustShape::RocketRoute => rocket_route_invocation(spec, func),
RustShape::WarpRoute => warp_route_invocation(spec, func),
RustShape::ClapCli => clap_invocation(spec, func),
}
}
fn framework_route_invocation(spec: &HarnessSpec, func: &str, marker: &str) -> (String, String) {
let pre = format!(" println!(\"{marker}\");\n");
let (inner_pre, call) = actix_invocation(spec, func);
(format!("{pre}{inner_pre}"), call)
fn framework_route_uri_expr(spec: &HarnessSpec) -> String {
let path = spec
.framework
.as_ref()
.and_then(|binding| binding.route.as_ref())
.map(|route| route.path.as_str())
.unwrap_or("/run");
match &spec.payload_slot {
PayloadSlot::QueryParam(name) => {
let payload_expr = if spec.expected_cap.contains(Cap::CODE_EXEC) {
"&nyx_route_payload(&payload)"
} else {
"&payload"
};
format!(
"nyx_route_uri({}, Some({}), {})",
rust_string_literal(path),
rust_string_literal(name),
payload_expr
)
}
_ => format!(
"nyx_route_uri({}, None, &payload)",
rust_string_literal(path)
),
}
}
fn axum_route_invocation(spec: &HarnessSpec, _func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_AXUM_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("tokio runtime");
__nyx_rt.block_on(async {{
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
let app = entry::build();
let uri = {uri};
let request = Request::builder()
.method("GET")
.uri(uri)
.body(Body::empty())
.expect("axum request");
let _ = app.oneshot(request).await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn actix_route_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_ACTIX_TEST=1\");\n".to_owned(),
format!(
r#"actix_web::rt::System::new().block_on(async {{
let app = actix_web::test::init_service(actix_web::App::new().service(entry::{func})).await;
let uri = {uri};
let req = actix_web::test::TestRequest::get().uri(&uri).to_request();
let _ = actix_web::test::call_service(&app, req).await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn rocket_route_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_ROCKET_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = rocket::tokio::runtime::Builder::new_current_thread().enable_all().build().expect("rocket runtime");
__nyx_rt.block_on(async {{
let rocket = rocket::build().mount("/", rocket::routes![entry::{func}]);
let client = rocket::local::asynchronous::Client::tracked(rocket)
.await
.expect("rocket client");
let uri = {uri};
let _ = client.get(uri).dispatch().await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn warp_route_invocation(spec: &HarnessSpec, _func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_WARP_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("tokio runtime");
__nyx_rt.block_on(async {{
let filter = entry::build();
let uri = {uri};
let _ = warp::test::request()
.method("GET")
.path(&uri)
.reply(&filter)
.await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn actix_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
@ -2787,6 +3020,35 @@ mod tests {
assert!(!class_derives_default(src, "UserService"));
}
#[test]
fn class_method_string_return_is_printed_for_oracle_visibility() {
let src = r#"
pub struct UserService;
impl UserService {
pub fn run(&self, input: &str) -> String {
input.to_owned()
}
}
"#;
let invocation = rust_class_method_invocation(src, "UserService", "run");
assert!(invocation.contains("let __nyx_result = instance.run(&payload);"));
assert!(invocation.contains("print!(\"{}\", __nyx_result);"));
}
#[test]
fn class_method_void_return_keeps_direct_invocation() {
let src = r#"
pub struct UserService;
impl UserService {
pub fn run(&self, input: &str) {
let _ = input;
}
}
"#;
let invocation = rust_class_method_invocation(src, "UserService", "run");
assert_eq!(invocation, "let _ = instance.run(&payload);");
}
#[test]
fn emit_env_var_slot() {
let spec = make_spec(PayloadSlot::EnvVar("NYX_INPUT".into()));
@ -2838,14 +3100,19 @@ mod tests {
#[test]
fn shape_detect_axum_handler() {
// Phase 17 — Track L.15: a strong `use axum::` import now
// routes to the framework-aware [`RustShape::AxumRoute`]
// shape; the legacy [`RustShape::AxumHandler`] fires only on
// weak detectors (`IntoResponse` / `Json(` without `use
// axum::`).
// Importing an extractor is a handler hint, not proof that the
// fixture exports an app builder. Native Axum request replay
// requires a router shape.
let src =
"use axum::extract::Query; pub fn handler(payload: &str) -> String { String::new() }";
let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
assert_eq!(RustShape::detect(&spec, src), RustShape::AxumHandler);
}
#[test]
fn shape_detect_axum_router() {
let src = "use axum::Router; use axum::routing::get; pub async fn run() -> String { String::new() } pub fn build() -> Router { Router::new().route(\"/run\", get(run)) }";
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
assert_eq!(RustShape::detect(&spec, src), RustShape::AxumRoute);
}
@ -2953,6 +3220,7 @@ mod tests {
src.contains("NYX_AXUM_TEST=1"),
"AxumRoute must print NYX_AXUM_TEST=1 marker, got: {src}",
);
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
@ -2960,6 +3228,19 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::ActixRoute);
assert!(src.contains("NYX_ACTIX_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
fn code_exec_query_routes_through_shell_safe_payload_helper() {
let mut spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
spec.expected_cap = Cap::CODE_EXEC;
spec.payload_slot = PayloadSlot::QueryParam("cmd".to_owned());
let src = generate_main_rs(&spec, RustShape::AxumRoute);
assert!(
src.contains("nyx_route_uri(\"/run\", Some(\"cmd\"), &nyx_route_payload(&payload))")
);
assert!(src.contains("fn nyx_route_payload(payload: &str) -> String"));
}
#[test]
@ -2967,6 +3248,7 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::RocketRoute);
assert!(src.contains("NYX_ROCKET_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
@ -2974,6 +3256,7 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::WarpRoute);
assert!(src.contains("NYX_WARP_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]