mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
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:
parent
aaf49acefb
commit
ed398e2834
14 changed files with 835 additions and 345 deletions
|
|
@ -433,8 +433,13 @@ fn visit_laravel_routes<'a>(
|
|||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
if node.kind() == "scoped_call_expression"
|
||||
&& let Some(found) = try_laravel_route(node, bytes, target, controller)
|
||||
if node.kind() == "scoped_call_expression" {
|
||||
if let Some(found) = try_laravel_route(node, bytes, target, controller) {
|
||||
*out = Some(found);
|
||||
return;
|
||||
}
|
||||
} else if node.kind() == "member_call_expression"
|
||||
&& let Some(found) = try_laravel_member_route(node, bytes, target, controller)
|
||||
{
|
||||
*out = Some(found);
|
||||
return;
|
||||
|
|
@ -470,6 +475,30 @@ fn try_laravel_route<'a>(
|
|||
})
|
||||
}
|
||||
|
||||
fn try_laravel_member_route<'a>(
|
||||
call: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
controller: Option<&str>,
|
||||
) -> Option<RouteShape> {
|
||||
let object = call.child_by_field_name("object")?.utf8_text(bytes).ok()?;
|
||||
if object.trim_start_matches('$').trim() != "router" {
|
||||
return None;
|
||||
}
|
||||
let verb_node = call.child_by_field_name("name")?.utf8_text(bytes).ok()?;
|
||||
let args = call.child_by_field_name("arguments")?;
|
||||
let methods = laravel_route_methods(verb_node, args, bytes)?;
|
||||
let path = laravel_route_path(verb_node, args, bytes)?;
|
||||
if !laravel_callable_matches(verb_node, args, bytes, target, controller) {
|
||||
return None;
|
||||
}
|
||||
Some(if methods.len() > 1 {
|
||||
RouteShape::multi(methods, path)
|
||||
} else {
|
||||
RouteShape::single(methods[0], path)
|
||||
})
|
||||
}
|
||||
|
||||
fn laravel_route_methods(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<Vec<HttpMethod>> {
|
||||
match verb {
|
||||
"any" => Some(vec![
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ fn stage_harness(
|
|||
// Copy the entry file into the workdir so the harness can import/include it.
|
||||
copy_entry_file(spec, &workdir, harness_src.entry_subpath.as_deref());
|
||||
copy_java_sibling_sources(spec, &workdir);
|
||||
copy_php_project_manifests(spec, &workdir);
|
||||
|
||||
Ok(workdir)
|
||||
}
|
||||
|
|
@ -259,6 +260,26 @@ fn copy_java_sibling_sources(spec: &HarnessSpec, workdir: &Path) {
|
|||
}
|
||||
}
|
||||
|
||||
fn copy_php_project_manifests(spec: &HarnessSpec, workdir: &Path) {
|
||||
if spec.lang != crate::symbol::Lang::Php {
|
||||
return;
|
||||
}
|
||||
let entry = PathBuf::from(&spec.entry_file);
|
||||
let mut dir = entry.parent();
|
||||
while let Some(current) = dir {
|
||||
let composer_json = current.join("composer.json");
|
||||
if composer_json.exists() {
|
||||
let _ = fs::copy(&composer_json, workdir.join("composer.json"));
|
||||
let composer_lock = current.join("composer.lock");
|
||||
if composer_lock.exists() {
|
||||
let _ = fs::copy(composer_lock, workdir.join("composer.lock"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
dir = current.parent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the source of the entry file (for repro bundles). Best-effort.
|
||||
fn extract_entry_source(spec: &HarnessSpec) -> String {
|
||||
let candidates = [
|
||||
|
|
|
|||
|
|
@ -143,22 +143,21 @@ pub enum PhpShape {
|
|||
/// stub (query/body) and invokes the closure resolved from the
|
||||
/// global (which the entry file publishes during include).
|
||||
RouteClosure,
|
||||
/// Laravel route — `Route::get('/x', 'Controller@method')` or
|
||||
/// closure callable. Phase 16 v1 dispatches through the same
|
||||
/// `$GLOBALS['__nyx_route']` channel as `RouteClosure` but
|
||||
/// publishes a `NYX_LARAVEL_TEST=1` stdout marker so the
|
||||
/// verifier can confirm the framework toolchain knob propagated.
|
||||
/// Laravel route — `Route::get('/x', 'Controller@method')`,
|
||||
/// `$router->get('/x', [Controller::class, 'method'])`, or
|
||||
/// closure callable. The harness requires Composer autoload,
|
||||
/// registers the fixture routes against `Illuminate\Routing\Router`,
|
||||
/// and dispatches an `Illuminate\Http\Request`.
|
||||
LaravelRoute,
|
||||
/// Symfony route — `#[Route('/x')]` PHP attribute on a
|
||||
/// controller method or top-level function. Phase 16 v1
|
||||
/// dispatches via reflective invocation (the entry file's
|
||||
/// `entry.php` instantiates the controller class and the harness
|
||||
/// calls the method) plus an `NYX_SYMFONY_TEST=1` stdout marker.
|
||||
/// controller method or top-level function. The harness requires
|
||||
/// Composer autoload, registers the fixture routes into a
|
||||
/// `RouteCollection`, and drives `HttpKernel::handle`.
|
||||
SymfonyRoute,
|
||||
/// CodeIgniter route — `$routes->get('users/(:num)', ...)`
|
||||
/// published from `app/Config/Routes.php`. Phase 16 v1
|
||||
/// dispatches via the `$GLOBALS['__nyx_route']` channel plus a
|
||||
/// `NYX_CODEIGNITER_TEST=1` stdout marker.
|
||||
/// published from `app/Config/Routes.php`. The harness requires
|
||||
/// Composer autoload, registers routes against CodeIgniter's
|
||||
/// `RouteCollection`, and replays the matching route handler.
|
||||
CodeIgniterRoute,
|
||||
/// CLI script driven by `$argv`. Harness mutates `$argv` then
|
||||
/// includes the entry file (whose top-level body reads `$argv`),
|
||||
|
|
@ -1953,6 +1952,7 @@ fn generate_source(spec: &HarnessSpec, shape: PhpShape) -> String {
|
|||
let shim = probe_shim();
|
||||
let toolchain_marker = build_toolchain_marker(shape);
|
||||
let route_methods_fn = build_route_methods_fn(spec);
|
||||
let framework_helpers = build_framework_helpers(spec, shape);
|
||||
let crash_callee = if entry_fn.is_empty() {
|
||||
"main"
|
||||
} else {
|
||||
|
|
@ -1976,9 +1976,10 @@ function nyx_payload(): string {{
|
|||
return '';
|
||||
}}
|
||||
|
||||
{route_methods_fn}
|
||||
|
||||
$payload = nyx_payload();
|
||||
{route_methods_fn}
|
||||
{framework_helpers}
|
||||
|
||||
$payload = nyx_payload();
|
||||
|
||||
// Phase 08 sink-site signal handler: install AFTER payload decode so a crash
|
||||
// inside `nyx_payload` writes no Crash probe and routes the verifier to
|
||||
|
|
@ -2016,10 +2017,286 @@ foreach (__nyx_route_methods() as $__nyx_method) {{
|
|||
shim = shim,
|
||||
toolchain_marker = toolchain_marker,
|
||||
route_methods_fn = route_methods_fn,
|
||||
framework_helpers = framework_helpers,
|
||||
crash_callee = crash_callee,
|
||||
)
|
||||
}
|
||||
|
||||
fn route_path_for_spec(spec: &HarnessSpec) -> String {
|
||||
spec.framework
|
||||
.as_ref()
|
||||
.and_then(|binding| binding.route.as_ref())
|
||||
.map(|route| route.path.clone())
|
||||
.unwrap_or_else(|| {
|
||||
if matches!(spec.entry_kind, crate::evidence::EntryKind::HttpRoute) {
|
||||
"/run/{payload}".to_owned()
|
||||
} else {
|
||||
"/".to_owned()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn build_framework_helpers(spec: &HarnessSpec, shape: PhpShape) -> String {
|
||||
if !matches!(
|
||||
shape,
|
||||
PhpShape::LaravelRoute | PhpShape::SymfonyRoute | PhpShape::CodeIgniterRoute
|
||||
) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let route_path = php_string_literal(&route_path_for_spec(spec));
|
||||
let mut out = String::new();
|
||||
out.push_str("const __NYX_ROUTE_PATH = ");
|
||||
out.push_str(&route_path);
|
||||
out.push_str(";\n");
|
||||
out.push_str(
|
||||
r#"
|
||||
function __nyx_find_user_function(string $leaf): ?string {
|
||||
$funcs = get_defined_functions();
|
||||
foreach (($funcs['user'] ?? []) as $fn) {
|
||||
if ($fn === $leaf || str_ends_with($fn, '\\' . $leaf)) {
|
||||
return $fn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function __nyx_request_path(string $template, string $payload): string {
|
||||
$encoded = rawurlencode($payload);
|
||||
$path = $template;
|
||||
$replacements = [
|
||||
'{payload}' => $encoded,
|
||||
'{payload?}' => $encoded,
|
||||
'(:any)' => $encoded,
|
||||
'(:segment)' => $encoded,
|
||||
'(:alphanum)' => $encoded,
|
||||
'(:alpha)' => $encoded,
|
||||
'(:num)' => $encoded,
|
||||
'(:hash)' => $encoded,
|
||||
];
|
||||
foreach ($replacements as $needle => $value) {
|
||||
if (str_contains($path, $needle)) {
|
||||
$path = str_replace($needle, $value, $path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($path === '') {
|
||||
$path = '/';
|
||||
}
|
||||
if ($path[0] !== '/') {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
function __nyx_response_text($response): string {
|
||||
if ($response === null) {
|
||||
return '';
|
||||
}
|
||||
if (is_string($response)) {
|
||||
return $response;
|
||||
}
|
||||
if (is_object($response)) {
|
||||
if (method_exists($response, 'getContent')) {
|
||||
return (string) $response->getContent();
|
||||
}
|
||||
if (method_exists($response, 'getBody')) {
|
||||
return (string) $response->getBody();
|
||||
}
|
||||
if (method_exists($response, '__toString')) {
|
||||
return (string) $response;
|
||||
}
|
||||
}
|
||||
if (is_array($response)) {
|
||||
return json_encode($response) ?: '';
|
||||
}
|
||||
return (string) $response;
|
||||
}
|
||||
|
||||
function __nyx_require_registrar(): string {
|
||||
$registrar = __nyx_find_user_function('nyx_register_routes');
|
||||
if ($registrar === null) {
|
||||
throw new RuntimeException('NYX_ROUTE_REGISTRAR_MISSING: nyx_register_routes');
|
||||
}
|
||||
return $registrar;
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
match shape {
|
||||
PhpShape::LaravelRoute => out.push_str(
|
||||
r#"
|
||||
function __nyx_dispatch_laravel(string $payload, string $method) {
|
||||
$required = [
|
||||
'\Illuminate\Container\Container',
|
||||
'\Illuminate\Events\Dispatcher',
|
||||
'\Illuminate\Http\Request',
|
||||
'\Illuminate\Routing\Router',
|
||||
];
|
||||
foreach ($required as $class) {
|
||||
if (!class_exists($class)) {
|
||||
throw new RuntimeException('NYX_LARAVEL_CLASS_MISSING: ' . $class);
|
||||
}
|
||||
}
|
||||
$container = new \Illuminate\Container\Container();
|
||||
\Illuminate\Container\Container::setInstance($container);
|
||||
$events = new \Illuminate\Events\Dispatcher($container);
|
||||
$router = new \Illuminate\Routing\Router($events, $container);
|
||||
$registrar = __nyx_require_registrar();
|
||||
$registrar($router);
|
||||
$path = __nyx_request_path(__NYX_ROUTE_PATH, $payload);
|
||||
$request = \Illuminate\Http\Request::create($path, $method, ['payload' => $payload], [], [], [], $payload);
|
||||
$response = $router->dispatch($request);
|
||||
return __nyx_response_text($response);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
PhpShape::SymfonyRoute => out.push_str(
|
||||
r#"
|
||||
function __nyx_dispatch_symfony(string $payload, string $method) {
|
||||
$required = [
|
||||
'\Symfony\Component\EventDispatcher\EventDispatcher',
|
||||
'\Symfony\Component\HttpFoundation\Request',
|
||||
'\Symfony\Component\HttpFoundation\RequestStack',
|
||||
'\Symfony\Component\HttpKernel\Controller\ArgumentResolver',
|
||||
'\Symfony\Component\HttpKernel\Controller\ControllerResolver',
|
||||
'\Symfony\Component\HttpKernel\HttpKernel',
|
||||
'\Symfony\Component\Routing\Matcher\UrlMatcher',
|
||||
'\Symfony\Component\Routing\RequestContext',
|
||||
'\Symfony\Component\Routing\RouteCollection',
|
||||
];
|
||||
foreach ($required as $class) {
|
||||
if (!class_exists($class)) {
|
||||
throw new RuntimeException('NYX_SYMFONY_CLASS_MISSING: ' . $class);
|
||||
}
|
||||
}
|
||||
$routes = new \Symfony\Component\Routing\RouteCollection();
|
||||
$registrar = __nyx_require_registrar();
|
||||
$registrar($routes);
|
||||
$path = __nyx_request_path(__NYX_ROUTE_PATH, $payload);
|
||||
$request = \Symfony\Component\HttpFoundation\Request::create($path, $method, ['payload' => $payload], [], [], [], $payload);
|
||||
$context = new \Symfony\Component\Routing\RequestContext();
|
||||
$context->fromRequest($request);
|
||||
$matcher = new \Symfony\Component\Routing\Matcher\UrlMatcher($routes, $context);
|
||||
$request->attributes->add($matcher->match($request->getPathInfo()));
|
||||
$kernel = new \Symfony\Component\HttpKernel\HttpKernel(
|
||||
new \Symfony\Component\EventDispatcher\EventDispatcher(),
|
||||
new \Symfony\Component\HttpKernel\Controller\ControllerResolver(),
|
||||
new \Symfony\Component\HttpFoundation\RequestStack(),
|
||||
new \Symfony\Component\HttpKernel\Controller\ArgumentResolver()
|
||||
);
|
||||
$response = $kernel->handle($request);
|
||||
return __nyx_response_text($response);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
PhpShape::CodeIgniterRoute => out.push_str(
|
||||
r#"
|
||||
function __nyx_define_codeigniter_config(): void {
|
||||
if (!defined('ENVIRONMENT')) define('ENVIRONMENT', 'testing');
|
||||
if (!defined('ROOTPATH')) define('ROOTPATH', __DIR__ . DIRECTORY_SEPARATOR);
|
||||
if (!defined('APPPATH')) define('APPPATH', __DIR__ . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR);
|
||||
if (!defined('WRITEPATH')) define('WRITEPATH', __DIR__ . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR);
|
||||
if (!defined('SYSTEMPATH')) define('SYSTEMPATH', __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'codeigniter4' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR);
|
||||
if (!is_dir(APPPATH . 'Config')) @mkdir(APPPATH . 'Config', 0777, true);
|
||||
if (!is_dir(WRITEPATH)) @mkdir(WRITEPATH, 0777, true);
|
||||
if (!class_exists('Config\\Modules') && class_exists('\\CodeIgniter\\Config\\Modules')) {
|
||||
eval('namespace Config; class Modules extends \\CodeIgniter\\Config\\Modules {}');
|
||||
}
|
||||
if (!class_exists('Config\\Routing') && class_exists('\\CodeIgniter\\Config\\Routing')) {
|
||||
eval('namespace Config; class Routing extends \\CodeIgniter\\Config\\Routing {}');
|
||||
}
|
||||
if (!class_exists('Config\\App') && class_exists('\\CodeIgniter\\Config\\BaseConfig')) {
|
||||
eval('namespace Config; class App extends \\CodeIgniter\\Config\\BaseConfig { public string $baseURL = "http://localhost/"; public array $supportedLocales = ["en"]; }');
|
||||
}
|
||||
if (!class_exists('Config\\Services') && class_exists('\\CodeIgniter\\Config\\BaseService')) {
|
||||
eval('namespace Config; class Services extends \\CodeIgniter\\Config\\BaseService {}');
|
||||
}
|
||||
}
|
||||
|
||||
function __nyx_codeigniter_routes() {
|
||||
__nyx_define_codeigniter_config();
|
||||
if (!class_exists('\\CodeIgniter\\Router\\RouteCollection')) {
|
||||
throw new RuntimeException('NYX_CODEIGNITER_CLASS_MISSING: \\CodeIgniter\\Router\\RouteCollection');
|
||||
}
|
||||
if (class_exists('\\CodeIgniter\\Config\\Services')) {
|
||||
try {
|
||||
$routes = \CodeIgniter\Config\Services::routes(false);
|
||||
if ($routes !== null) {
|
||||
return $routes;
|
||||
}
|
||||
} catch (Throwable $_) {
|
||||
}
|
||||
}
|
||||
$modules = class_exists('Config\\Modules') ? new \Config\Modules() : null;
|
||||
$routing = class_exists('Config\\Routing') ? new \Config\Routing() : null;
|
||||
if ($routing !== null) {
|
||||
$routing->defaultNamespace = 'App\\Controllers';
|
||||
$routing->defaultController = 'Home';
|
||||
$routing->defaultMethod = 'index';
|
||||
$routing->translateURIDashes = false;
|
||||
$routing->autoRoute = false;
|
||||
$routing->routeFiles = [];
|
||||
}
|
||||
$locator = class_exists('\\CodeIgniter\\Autoloader\\FileLocator') && $modules !== null
|
||||
? new \CodeIgniter\Autoloader\FileLocator($modules)
|
||||
: null;
|
||||
return new \CodeIgniter\Router\RouteCollection($locator, $modules, $routing);
|
||||
}
|
||||
|
||||
function __nyx_ci_pattern_regex(string $pattern): string {
|
||||
$regex = str_replace(
|
||||
['(:any)', '(:segment)', '(:alphanum)', '(:alpha)', '(:num)', '(:hash)'],
|
||||
['(.+)', '([^/]+)', '([a-zA-Z0-9]+)', '([a-zA-Z]+)', '([0-9]+)', '([^/]+)'],
|
||||
$pattern
|
||||
);
|
||||
return '#^' . str_replace('#', '\\#', trim($regex, '/')) . '$#u';
|
||||
}
|
||||
|
||||
function __nyx_ci_invoke_handler($handler, array $args, string $payload) {
|
||||
if (is_callable($handler)) {
|
||||
return call_user_func_array($handler, $args ?: [$payload]);
|
||||
}
|
||||
if (!is_string($handler)) {
|
||||
throw new RuntimeException('NYX_CODEIGNITER_HANDLER_UNSUPPORTED');
|
||||
}
|
||||
[$target, $_tail] = array_pad(explode('/', $handler, 2), 2, '');
|
||||
if (!str_contains($target, '::')) {
|
||||
throw new RuntimeException('NYX_CODEIGNITER_HANDLER_BAD: ' . $handler);
|
||||
}
|
||||
[$class, $method] = explode('::', $target, 2);
|
||||
if (!class_exists($class) && class_exists('App\\Controllers\\' . ltrim($class, '\\'))) {
|
||||
$class = 'App\\Controllers\\' . ltrim($class, '\\');
|
||||
}
|
||||
$controller = new $class();
|
||||
return call_user_func_array([$controller, $method], $args ?: [$payload]);
|
||||
}
|
||||
|
||||
function __nyx_dispatch_codeigniter(string $payload, string $method) {
|
||||
$routes = __nyx_codeigniter_routes();
|
||||
$registrar = __nyx_require_registrar();
|
||||
$registrar($routes);
|
||||
if (method_exists($routes, 'setHTTPVerb')) {
|
||||
$routes->setHTTPVerb($method);
|
||||
}
|
||||
$path = ltrim(__nyx_request_path(__NYX_ROUTE_PATH, $payload), '/');
|
||||
$map = method_exists($routes, 'getRoutes') ? $routes->getRoutes($method) : [];
|
||||
foreach ($map as $pattern => $handler) {
|
||||
if (preg_match(__nyx_ci_pattern_regex((string) $pattern), $path, $matches)) {
|
||||
array_shift($matches);
|
||||
$decoded = array_map('rawurldecode', $matches);
|
||||
return __nyx_response_text(__nyx_ci_invoke_handler($handler, $decoded, $payload));
|
||||
}
|
||||
}
|
||||
throw new RuntimeException('NYX_CODEIGNITER_ROUTE_NOT_FOUND: ' . $method . ' ' . $path);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn build_route_methods_fn(spec: &HarnessSpec) -> String {
|
||||
let mut methods = spec
|
||||
.framework
|
||||
|
|
@ -2100,14 +2377,30 @@ fn build_pre_call(spec: &HarnessSpec, shape: PhpShape) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
fn build_entry_block(_shape: PhpShape) -> String {
|
||||
r#"try {
|
||||
fn build_entry_block(shape: PhpShape) -> String {
|
||||
let autoload = if matches!(
|
||||
shape,
|
||||
PhpShape::LaravelRoute | PhpShape::SymfonyRoute | PhpShape::CodeIgniterRoute
|
||||
) {
|
||||
r#"$__nyx_autoload = __DIR__ . '/vendor/autoload.php';
|
||||
if (!is_file($__nyx_autoload)) {
|
||||
fwrite(STDERR, 'NYX_IMPORT_ERROR: missing Composer autoload; run composer install for framework route replay' . "\n");
|
||||
exit(77);
|
||||
}
|
||||
require_once $__nyx_autoload;
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
r#"{autoload}try {{
|
||||
require_once __DIR__ . '/entry.php';
|
||||
} catch (Throwable $e) {
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_IMPORT_ERROR: ' . $e->getMessage() . "\n");
|
||||
exit(77);
|
||||
}"#
|
||||
.to_owned()
|
||||
}}"#,
|
||||
autoload = autoload
|
||||
)
|
||||
}
|
||||
|
||||
/// Phase 11 (Track J.9) CRYPTO harness for PHP.
|
||||
|
|
@ -2899,7 +3192,7 @@ fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
|
|||
"null".to_owned()
|
||||
}
|
||||
}
|
||||
PhpShape::RouteClosure | PhpShape::LaravelRoute | PhpShape::CodeIgniterRoute => {
|
||||
PhpShape::RouteClosure => {
|
||||
// Entry script publishes the route closure via
|
||||
// `$GLOBALS['__nyx_route']`. When the global is missing,
|
||||
// fall back to calling the named function directly.
|
||||
|
|
@ -2907,17 +3200,10 @@ fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
|
|||
"(isset($GLOBALS['__nyx_route']) && is_callable($GLOBALS['__nyx_route'])) ? call_user_func($GLOBALS['__nyx_route'], $payload) : (function_exists({func:?}) ? {func}($payload) : null)"
|
||||
)
|
||||
}
|
||||
PhpShape::SymfonyRoute => {
|
||||
// Symfony controllers are normally reached through
|
||||
// `HttpKernel::handle`. The Phase 16 v1 harness drives
|
||||
// the action directly: the entry file publishes a
|
||||
// controller instance via `$GLOBALS['__nyx_controller']`
|
||||
// and the harness reflectively invokes the action method.
|
||||
// Falls back to calling a bare function when no
|
||||
// controller class was published.
|
||||
format!(
|
||||
"(isset($GLOBALS['__nyx_controller']) && is_object($GLOBALS['__nyx_controller'])) ? $GLOBALS['__nyx_controller']->{func}($payload) : (function_exists({func:?}) ? {func}($payload) : null)"
|
||||
)
|
||||
PhpShape::LaravelRoute => "__nyx_dispatch_laravel($payload, $__nyx_method)".to_owned(),
|
||||
PhpShape::SymfonyRoute => "__nyx_dispatch_symfony($payload, $__nyx_method)".to_owned(),
|
||||
PhpShape::CodeIgniterRoute => {
|
||||
"__nyx_dispatch_codeigniter($payload, $__nyx_method)".to_owned()
|
||||
}
|
||||
PhpShape::Generic => build_generic_call(spec, func),
|
||||
}
|
||||
|
|
@ -3118,7 +3404,9 @@ mod tests {
|
|||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
|
||||
let src = generate_source(&spec, PhpShape::LaravelRoute);
|
||||
assert!(src.contains("NYX_LARAVEL_TEST=1"));
|
||||
assert!(src.contains("$GLOBALS['__nyx_route']"));
|
||||
assert!(src.contains("vendor/autoload.php"));
|
||||
assert!(src.contains("__nyx_dispatch_laravel($payload, $__nyx_method)"));
|
||||
assert!(!src.contains("$GLOBALS['__nyx_route']"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3146,8 +3434,10 @@ mod tests {
|
|||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
|
||||
let src = generate_source(&spec, PhpShape::SymfonyRoute);
|
||||
assert!(src.contains("NYX_SYMFONY_TEST=1"));
|
||||
assert!(src.contains("$GLOBALS['__nyx_controller']"));
|
||||
assert!(src.contains("->run($payload)"));
|
||||
assert!(src.contains("vendor/autoload.php"));
|
||||
assert!(src.contains("Symfony\\Component\\HttpKernel\\HttpKernel"));
|
||||
assert!(src.contains("__nyx_dispatch_symfony($payload, $__nyx_method)"));
|
||||
assert!(!src.contains("$GLOBALS['__nyx_controller']"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3155,7 +3445,10 @@ mod tests {
|
|||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
|
||||
let src = generate_source(&spec, PhpShape::CodeIgniterRoute);
|
||||
assert!(src.contains("NYX_CODEIGNITER_TEST=1"));
|
||||
assert!(src.contains("$GLOBALS['__nyx_route']"));
|
||||
assert!(src.contains("vendor/autoload.php"));
|
||||
assert!(src.contains("CodeIgniter\\Router\\RouteCollection"));
|
||||
assert!(src.contains("__nyx_dispatch_codeigniter($payload, $__nyx_method)"));
|
||||
assert!(!src.contains("$GLOBALS['__nyx_route']"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1,44 +1,24 @@
|
|||
<?php
|
||||
// Phase 16 — CodeIgniter-style route, benign sanitised payload.
|
||||
// CodeIgniter-style route, benign sanitised payload.
|
||||
|
||||
namespace CodeIgniter\Router {
|
||||
class RouteCollection
|
||||
{
|
||||
}
|
||||
}
|
||||
namespace App\Controllers;
|
||||
|
||||
namespace {
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
class BaseController
|
||||
function nyx_register_routes(RouteCollection $routes): void
|
||||
{
|
||||
$routes->get('run/(:any)', 'App\\Controllers\\UserController::run');
|
||||
}
|
||||
|
||||
class NyxRoutes extends RouteCollection
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function get(string $path, string $callable)
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
|
||||
[$class, $method] = explode('::', $callable, 2);
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$routes = new NyxRoutes();
|
||||
$routes->get('run', 'UserController::run');
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
public function run($payload)
|
||||
public function run(string $payload): string
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,24 @@
|
|||
<?php
|
||||
// Phase 16 — CodeIgniter-style route, vulnerable.
|
||||
// `$routes->get('run', 'UserController::run')` references the
|
||||
// controller method whose body shells out without sanitisation.
|
||||
// CodeIgniter-style route, vulnerable.
|
||||
|
||||
namespace CodeIgniter\Router {
|
||||
class RouteCollection
|
||||
{
|
||||
}
|
||||
}
|
||||
namespace App\Controllers;
|
||||
|
||||
namespace {
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
class BaseController
|
||||
function nyx_register_routes(RouteCollection $routes): void
|
||||
{
|
||||
$routes->get('run/(:any)', 'App\\Controllers\\UserController::run');
|
||||
}
|
||||
|
||||
class NyxRoutes extends RouteCollection
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function get(string $path, string $callable)
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
|
||||
[$class, $method] = explode('::', $callable, 2);
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$routes = new NyxRoutes();
|
||||
$routes->get('run', 'UserController::run');
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
public function run($payload)
|
||||
public function run(string $payload): string
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "echo hello " . $payload;
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,23 @@
|
|||
<?php
|
||||
// Phase 16 — Laravel-style route, benign sanitised payload.
|
||||
// Laravel-style route, benign sanitised payload.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function get(string $path, string $callable)
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
|
||||
[$class, $method] = preg_split('/@|::/', $callable);
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
function nyx_register_routes(Router $router): void
|
||||
{
|
||||
$router->get('/run/{payload}', [UserController::class, 'run']);
|
||||
}
|
||||
|
||||
namespace {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/run', 'UserController@run');
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run($payload)
|
||||
public function run(string $payload): string
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,23 @@
|
|||
<?php
|
||||
// Phase 16 — Laravel-style route, vulnerable.
|
||||
// `Route::get('/run', 'UserController@run')` references the
|
||||
// controller method whose body shells out without sanitisation.
|
||||
// Laravel-style route, vulnerable.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function get(string $path, string $callable)
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
|
||||
[$class, $method] = preg_split('/@|::/', $callable);
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
function nyx_register_routes(Router $router): void
|
||||
{
|
||||
$router->get('/run/{payload}', [UserController::class, 'run']);
|
||||
}
|
||||
|
||||
namespace {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/run', 'UserController@run');
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run($payload)
|
||||
public function run(string $payload): string
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "echo hello " . $payload;
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,53 +2,27 @@
|
|||
// Same route shape as vuln.php, but quotes the payload before invoking
|
||||
// the shell so the command-injection marker remains inert.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function match(array $methods, string $path, array $callable)
|
||||
{
|
||||
\NyxLaravelMultiVerb\register_route($methods, $callable);
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
function nyx_register_routes(Router $router): void
|
||||
{
|
||||
$router->match(['GET', 'POST'], '/run/{payload}', [UserController::class, 'run']);
|
||||
}
|
||||
|
||||
namespace NyxLaravelMultiVerb {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
function register_route(array $methods, array $callable): void
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload): ?string
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if (!in_array($requestMethod, $methods, true)) {
|
||||
return null;
|
||||
}
|
||||
[$class, $method] = $callable;
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
}
|
||||
|
||||
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload)
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd);
|
||||
echo $out;
|
||||
return $out;
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "nyx/fixture-laravel-multi-verb",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"laravel/framework": "^11.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,28 @@
|
|||
<?php
|
||||
// Laravel-style multi-verb route fixture. The vulnerable sink is gated
|
||||
// to POST so verifier runs that exercise only the representative GET
|
||||
// method miss the command injection.
|
||||
// to POST so verifier runs that exercise only GET miss the command injection.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function match(array $methods, string $path, array $callable)
|
||||
{
|
||||
\NyxLaravelMultiVerb\register_route($methods, $callable);
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
function nyx_register_routes(Router $router): void
|
||||
{
|
||||
$router->match(['GET', 'POST'], '/run/{payload}', [UserController::class, 'run']);
|
||||
}
|
||||
|
||||
namespace NyxLaravelMultiVerb {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
function register_route(array $methods, array $callable): void
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload): ?string
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if (!in_array($requestMethod, $methods, true)) {
|
||||
return null;
|
||||
}
|
||||
[$class, $method] = $callable;
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
}
|
||||
|
||||
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload)
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . $payload;
|
||||
$out = shell_exec($cmd);
|
||||
echo $out;
|
||||
return $out;
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . $payload;
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,35 @@
|
|||
<?php
|
||||
// Phase 16 — Symfony-style route via `#[Route]` attribute,
|
||||
// benign sanitised payload.
|
||||
// Symfony-style route via RouteCollection and HttpKernel, benign sanitised payload.
|
||||
|
||||
namespace Symfony\Component\HttpFoundation {
|
||||
class Response
|
||||
{
|
||||
public function __construct(private string $content)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Symfony\Component\Routing\Annotation {
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
|
||||
class Route
|
||||
{
|
||||
public function __construct(...$args)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Controller {
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Route as SymfonyRoute;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
function nyx_register_routes(RouteCollection $routes): void
|
||||
{
|
||||
$routes->add('nyx_run', new SymfonyRoute(
|
||||
'/run/{payload}',
|
||||
['_controller' => [new UserController(), 'run']],
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
[],
|
||||
['GET']
|
||||
));
|
||||
}
|
||||
|
||||
class UserController
|
||||
{
|
||||
#[Route('/run', methods: ['GET'])]
|
||||
public function run($payload)
|
||||
#[Route('/run/{payload}', methods: ['GET'])]
|
||||
public function run(string $payload): Response
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return new Response($out);
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['__nyx_controller'] = new UserController();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,35 @@
|
|||
<?php
|
||||
// Phase 16 — Symfony-style route via `#[Route]` attribute,
|
||||
// vulnerable.
|
||||
// Symfony-style route via RouteCollection and HttpKernel, vulnerable.
|
||||
|
||||
namespace Symfony\Component\HttpFoundation {
|
||||
class Response
|
||||
{
|
||||
public function __construct(private string $content)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Symfony\Component\Routing\Annotation {
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
|
||||
class Route
|
||||
{
|
||||
public function __construct(...$args)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Controller {
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Route as SymfonyRoute;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
function nyx_register_routes(RouteCollection $routes): void
|
||||
{
|
||||
$routes->add('nyx_run', new SymfonyRoute(
|
||||
'/run/{payload}',
|
||||
['_controller' => [new UserController(), 'run']],
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
[],
|
||||
['GET']
|
||||
));
|
||||
}
|
||||
|
||||
class UserController
|
||||
{
|
||||
#[Route('/run', methods: ['GET'])]
|
||||
public function run($payload)
|
||||
#[Route('/run/{payload}', methods: ['GET'])]
|
||||
public function run(string $payload): Response
|
||||
{
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "echo hello " . $payload;
|
||||
$out = shell_exec($cmd);
|
||||
$out = shell_exec($cmd) ?? '';
|
||||
echo $out;
|
||||
return new Response($out);
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['__nyx_controller'] = new UserController();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@ fn laravel_vuln_fixture_binds_route() {
|
|||
assert_eq!(binding.adapter, "php-laravel");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.path, "/run/{payload}");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
let payload = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "payload")
|
||||
.expect("payload formal");
|
||||
assert!(matches!(payload.source, ParamSource::QueryParam(_)));
|
||||
assert!(matches!(payload.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -68,7 +68,7 @@ fn laravel_benign_fixture_binds_same_route_shape() {
|
|||
.expect("laravel adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "php-laravel");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.path, "/run/{payload}");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ fn laravel_multi_verb_fixture_preserves_match_methods() {
|
|||
.expect("laravel adapter must bind multi-verb fixture");
|
||||
assert_eq!(binding.adapter, "php-laravel");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.path, "/run/{payload}");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
|
|
@ -101,7 +101,7 @@ fn symfony_vuln_fixture_binds_route_via_attribute() {
|
|||
assert_eq!(binding.adapter, "php-symfony");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.path, "/run/{payload}");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ fn symfony_benign_fixture_binds_same_route_shape() {
|
|||
.expect("symfony adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "php-symfony");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.path, "/run/{payload}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -153,7 +153,7 @@ fn codeigniter_vuln_fixture_binds_route() {
|
|||
assert_eq!(binding.adapter, "php-codeigniter");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "run");
|
||||
assert_eq!(route.path, "run/(:any)");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ fn codeigniter_benign_fixture_binds_same_route_shape() {
|
|||
.expect("codeigniter adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "php-codeigniter");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "run");
|
||||
assert_eq!(route.path, "run/(:any)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -270,6 +270,13 @@ mod e2e_phase_16_framework_dispatchers {
|
|||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join(file);
|
||||
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
|
||||
for manifest in ["composer.json", "composer.lock"] {
|
||||
let candidate = src.parent().expect("fixture parent").join(manifest);
|
||||
if candidate.exists() {
|
||||
std::fs::copy(&candidate, tmp.path().join(manifest))
|
||||
.expect("copy composer manifest into tempdir");
|
||||
}
|
||||
}
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let bytes = std::fs::read(&dst).expect("copied fixture readable");
|
||||
let tree = parse_php(&bytes);
|
||||
|
|
@ -425,6 +432,13 @@ mod e2e_phase_16_laravel_multi_verb {
|
|||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join(file);
|
||||
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
|
||||
for manifest in ["composer.json", "composer.lock"] {
|
||||
let candidate = src.parent().expect("fixture parent").join(manifest);
|
||||
if candidate.exists() {
|
||||
std::fs::copy(&candidate, tmp.path().join(manifest))
|
||||
.expect("copy composer manifest into tempdir");
|
||||
}
|
||||
}
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let bytes = std::fs::read(&dst).expect("copied fixture readable");
|
||||
let tree = parse_php(&bytes);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue