rns-hooks 0.1.11

Programmable hook system for the Reticulum Network Stack
Documentation
#![cfg(all(feature = "native", target_os = "linux"))]

use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

use rns_hooks::native::NativeProgram;
use rns_hooks::{HookBackend, HookContext, HookError, HookManager, NullEngine, Verdict};

const FIXTURE_PREAMBLE: &str = r#"
#include <stddef.h>
#include <stdint.h>

typedef struct HookResult {
    uint32_t verdict;
    uint32_t modified_data_offset;
    uint32_t modified_data_len;
    uint32_t inject_actions_offset;
    uint32_t inject_actions_count;
    uint32_t log_offset;
    uint32_t log_len;
} HookResult;

typedef struct RnsNativeHostApi RnsNativeHostApi;
"#;

const VALID_HOOK: &str = r#"
int rns_hook_abi_version(void) { return 1; }

int rns_hook_on_call(
    const uint8_t *ctx,
    size_t ctx_len,
    const RnsNativeHostApi *host_api,
    HookResult *result
) {
    (void)host_api;
    if (ctx == NULL || ctx_len < 4 || result == NULL) {
        return -1;
    }
    uint32_t ctx_type =
        ((uint32_t)ctx[0]) |
        ((uint32_t)ctx[1] << 8) |
        ((uint32_t)ctx[2] << 16) |
        ((uint32_t)ctx[3] << 24);
    if (ctx_type != 2) {
        return -2;
    }
    result->verdict = 0;
    return 0;
}
"#;

#[test]
fn native_hook_loads_and_executes() {
    let lib_path = build_native_fixture("valid", VALID_HOOK);
    let manager = HookManager::new().expect("manager");
    let mut program = manager
        .load_file_backend("native-test".into(), &lib_path, 0, HookBackend::Native)
        .expect("load native hook");

    assert_eq!(program.backend_name(), "native");
    fs::remove_file(&lib_path).expect("unlink loaded fixture");

    let exec = manager
        .execute_program(&mut program, &HookContext::Tick, &NullEngine, 0.0, None)
        .expect("native hook result");
    let result = exec.hook_result.expect("hook result");
    assert_eq!(Verdict::from_u32(result.verdict), Some(Verdict::Continue));
    assert!(exec.injected_actions.is_empty());
    assert!(exec.provider_events.is_empty());
    assert!(exec.modified_data.is_none());
}

#[test]
fn native_hook_reports_missing_library() {
    let path = std::env::temp_dir().join(format!(
        "rns-hooks-missing-{}-{}.so",
        std::process::id(),
        next_fixture_id()
    ));
    let error = NativeProgram::load(&path).err().expect("load must fail");
    assert!(matches!(error, HookError::CompileError(_)));
    assert!(error.to_string().contains("failed to load native hook"));
}

#[test]
fn native_hook_requires_abi_version_symbol() {
    let path = build_native_fixture(
        "missing-abi",
        r#"
int rns_hook_on_call(const uint8_t *ctx, size_t len, const RnsNativeHostApi *api, HookResult *result) {
    (void)ctx; (void)len; (void)api; (void)result; return 0;
}
"#,
    );
    let error = NativeProgram::load(&path).err().expect("load must fail");
    assert!(matches!(error, HookError::CompileError(_)));
    assert!(error.to_string().contains("missing rns_hook_abi_version"));
}

#[test]
fn native_hook_rejects_wrong_abi_version() {
    let path = build_native_fixture(
        "wrong-abi",
        r#"
int rns_hook_abi_version(void) { return 2; }
int rns_hook_on_call(const uint8_t *ctx, size_t len, const RnsNativeHostApi *api, HookResult *result) {
    (void)ctx; (void)len; (void)api; (void)result; return 0;
}
"#,
    );
    let error = NativeProgram::load(&path).err().expect("load must fail");
    assert!(matches!(
        error,
        HookError::AbiVersionMismatch {
            expected: 1,
            found: Some(2),
            ..
        }
    ));
}

#[test]
fn native_hook_requires_call_symbol() {
    let path = build_native_fixture(
        "missing-call",
        "int rns_hook_abi_version(void) { return 1; }",
    );
    let error = NativeProgram::load(&path).err().expect("load must fail");
    assert!(matches!(error, HookError::CompileError(_)));
    assert!(error.to_string().contains("missing rns_hook_on_call"));
}

#[test]
fn native_hook_propagates_nonzero_return_code() {
    let error = execute_fixture(
        "return-error",
        r#"
int rns_hook_abi_version(void) { return 1; }
int rns_hook_on_call(const uint8_t *ctx, size_t len, const RnsNativeHostApi *api, HookResult *result) {
    (void)ctx; (void)len; (void)api; (void)result; return 37;
}
"#,
    )
    .expect_err("execution must fail");
    assert!(matches!(error, HookError::Trap(_)));
    assert!(error.to_string().contains("error code 37"));
}

#[test]
fn native_hook_rejects_invalid_verdict() {
    let error = execute_fixture(
        "invalid-verdict",
        r#"
int rns_hook_abi_version(void) { return 1; }
int rns_hook_on_call(const uint8_t *ctx, size_t len, const RnsNativeHostApi *api, HookResult *result) {
    (void)ctx; (void)len; (void)api; result->verdict = 99; return 0;
}
"#,
    )
    .expect_err("execution must fail");
    assert!(matches!(error, HookError::InvalidResult(_)));
    assert!(error.to_string().contains("invalid verdict value: 99"));
}

fn execute_fixture(name: &str, source: &str) -> Result<rns_hooks::ExecuteResult, HookError> {
    let path = build_native_fixture(name, source);
    let program = NativeProgram::load(&path)?;
    program.execute(&[2, 0, 0, 0], &NullEngine, false)
}

fn next_fixture_id() -> u64 {
    static NEXT_ID: AtomicU64 = AtomicU64::new(0);
    NEXT_ID.fetch_add(1, Ordering::Relaxed)
}

fn build_native_fixture(name: &str, implementation: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "rns-hooks-native-test-{}-{}-{}",
        std::process::id(),
        next_fixture_id(),
        name
    ));
    fs::create_dir_all(&dir).expect("create fixture dir");

    let source = dir.join("native_hook.c");
    let lib_path = dir.join(format!(
        "libnative_hook.{}",
        std::env::consts::DLL_EXTENSION
    ));
    fs::write(&source, format!("{FIXTURE_PREAMBLE}\n{implementation}"))
        .expect("write fixture source");

    let output = Command::new("cc")
        .arg("-shared")
        .arg("-fPIC")
        .arg(&source)
        .arg("-o")
        .arg(&lib_path)
        .output()
        .expect("run cc");
    assert!(
        output.status.success(),
        "cc failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    lib_path
}