jsdet-core 0.1.1

Core WASM-sandboxed JavaScript detonation engine
Documentation
use jsdet_core::{Bridge, CompiledModule, EmptyBridge, Error, SandboxConfig, Value};
use std::sync::Arc;
use std::thread;

fn module() -> Result<CompiledModule, Box<dyn std::error::Error>> {
    let m = CompiledModule::new().map_err(|e| e.to_string())?;
    Ok(m)
}

fn config() -> SandboxConfig {
    SandboxConfig::default()
}

/// 1. OOM injection tests
/// What happens when allocation fails mid-operation?
#[test]
fn test_oom_mid_operation() -> Result<(), Box<dyn std::error::Error>> {
    let m = module()?;
    let mut cfg = config();
    // QuickJS needs ~4MB for bootstrap. Give it just enough, then allocate hard.
    cfg.max_memory_bytes = 4 * 1024 * 1024;
    cfg.timeout_ms = 5000;

    // Allocate huge array
    let script = "
        let arr = [];
        for (let i = 0; i < 1000000; i++) {
            arr.push('x'.repeat(10000));
        }
    "
    .to_string();

    let result = m.execute(&[script], Arc::new(EmptyBridge), &cfg);

    // According to SQLite-grade testing principles, it must either error with MemoryExceeded
    // or return a result containing JS-level memory error observations. It should not hang or succeed.
    match result {
        Ok(res) => {
            // If it managed to complete, it should have JS errors describing the OOM
            assert!(
                !res.errors.is_empty(),
                "OOM should result in a JS-level error if not a trap"
            );
        }
        Err(e) => {
            // If it trapped, it should be due to memory or trap
            assert!(
                matches!(e, Error::Trap(_) | Error::MemoryExceeded { .. }),
                "Expected Trap or MemoryExceeded, got: {:?}",
                e
            );
        }
    }

    // Verify state consistency by running a normal script after OOM attempt
    let normal_script = "1 + 1".to_string();
    let result_normal = m.execute(&[normal_script], Arc::new(EmptyBridge), &config());
    let ok_res =
        result_normal.expect("Module must remain usable after an OOM in a sandboxed instance");
    assert!(
        ok_res.errors.is_empty(),
        "Normal script must run clean after an OOM failure in a prior instance"
    );

    Ok(())
}

/// 2. IO error injection tests
/// What happens when read/write/mmap fails? Are partial writes handled correctly?
#[test]
fn test_io_error_injection_cached_module() -> Result<(), Box<dyn std::error::Error>> {
    // Empty input
    let result_empty = CompiledModule::load_cached(&[]);
    match result_empty {
        Ok(_) => panic!("Empty cache load must fail with WasmInit error"),
        Err(e) => assert!(
            matches!(e, Error::WasmInit(_)),
            "Expected WasmInit error, got {:?}",
            e
        ),
    }

    // Single byte
    let result_single = CompiledModule::load_cached(&[0x00]);
    match result_single {
        Ok(_) => panic!("Single byte cache load must fail with WasmInit error"),
        Err(e) => assert!(
            matches!(e, Error::WasmInit(_)),
            "Expected WasmInit error, got {:?}",
            e
        ),
    }

    // Truncated valid cache
    let m = module()?;
    let valid_bytes = m.serialize().map_err(|e| e.to_string())?;
    assert!(
        !valid_bytes.is_empty(),
        "Serialized bytes must not be empty"
    );

    if valid_bytes.len() > 100 {
        let truncated = &valid_bytes[0..valid_bytes.len() / 2];
        let result_trunc = CompiledModule::load_cached(truncated);
        match result_trunc {
            Ok(_) => panic!("Truncated cache load must fail with WasmInit error"),
            Err(e) => assert!(
                matches!(e, Error::WasmInit(_)),
                "Expected WasmInit error, got {:?}",
                e
            ),
        }
    }

    Ok(())
}

/// 3. Concurrent stress tests
/// 32 threads hammering the same API simultaneously.
#[test]
fn test_concurrent_stress_32_threads() -> Result<(), Box<dyn std::error::Error>> {
    let m = Arc::new(module()?);
    let mut handles = vec![];

    for i in 0..32 {
        let module_clone = Arc::clone(&m);
        handles.push(thread::spawn(move || {
            let cfg = config();
            let script = format!("var a = {}; a + a;", i);
            module_clone.execute(&[script], Arc::new(EmptyBridge), &cfg)
        }));
    }

    for (i, handle) in handles.into_iter().enumerate() {
        let result = handle
            .join()
            .map_err(|_| format!("Thread {} panicked", i))?;
        let res = result.unwrap_or_else(|e| panic!("Thread {} execution failed: {:?}", i, e));
        assert!(
            res.errors.is_empty(),
            "Thread {} produced JS execution errors",
            i
        );
    }

    Ok(())
}

/// 4. Adversarial input at EVERY boundary
/// Empty input, single byte, all-zero, all-0xFF, alternating patterns, hash collision inputs.
#[test]
fn test_adversarial_input_boundaries() -> Result<(), Box<dyn std::error::Error>> {
    let m = module()?;
    let cfg = config();

    // All-zero bytes to cache
    let zeros = vec![0u8; 1024];
    let res_zeros = CompiledModule::load_cached(&zeros);
    match res_zeros {
        Ok(_) => panic!("All-zero bytes must result in WasmInit error"),
        Err(e) => assert!(
            matches!(e, Error::WasmInit(_)),
            "Expected WasmInit error, got {:?}",
            e
        ),
    }

    // All-0xFF bytes to cache
    let ffs = vec![0xFFu8; 1024];
    let res_ffs = CompiledModule::load_cached(&ffs);
    match res_ffs {
        Ok(_) => panic!("All-0xFF bytes must result in WasmInit error"),
        Err(e) => assert!(
            matches!(e, Error::WasmInit(_)),
            "Expected WasmInit error, got {:?}",
            e
        ),
    }

    // Alternating patterns
    let mut alt = vec![0u8; 1024];
    for i in 0..1024 {
        alt[i] = if i % 2 == 0 { 0xAA } else { 0x55 };
    }
    let res_alt = CompiledModule::load_cached(&alt);
    match res_alt {
        Ok(_) => panic!("Alternating bytes must result in WasmInit error"),
        Err(e) => assert!(
            matches!(e, Error::WasmInit(_)),
            "Expected WasmInit error, got {:?}",
            e
        ),
    }

    // Execution with empty script
    let res_empty_script = m.execute(&["".to_string()], Arc::new(EmptyBridge), &cfg)?;
    assert!(
        res_empty_script.errors.is_empty(),
        "Empty script must run without internal errors"
    );

    // Script with massive null bytes
    let null_script = String::from_utf8(vec![0x00; 100_000]).unwrap_or_default();
    let res_null = m.execute(&[null_script], Arc::new(EmptyBridge), &cfg);
    // Null byte scripts should either parse correctly or trigger a syntax error gracefully, but not panic
    match res_null {
        Ok(res) => assert!(
            !res.errors.is_empty(),
            "Null bytes script should result in a parsing error"
        ),
        Err(e) => assert!(
            matches!(e, Error::Trap(_)),
            "Null bytes script failing via trap is acceptable: {:?}",
            e
        ),
    }

    // Script that maximizes hash collisions
    let collision_script = "
        let obj = {};
        for(let i=0; i<10000; i++) {
            obj['a' + i] = i;
        }
    "
    .to_string();
    let res_collision = m.execute(&[collision_script], Arc::new(EmptyBridge), &cfg)?;
    assert!(
        res_collision.errors.is_empty(),
        "Hash collision script failed with JS errors"
    );

    Ok(())
}

/// 5. Integer overflow probes
/// Inputs sized to trigger u32 truncation, limit counts.
#[test]
fn test_integer_overflow_probes() -> Result<(), Box<dyn std::error::Error>> {
    let m = module()?;

    let mut cfg = config();
    // Test extreme boundaries for config (could truncate from u64 to u32 internally)
    cfg.max_scripts = usize::MAX;
    cfg.max_script_bytes = usize::MAX;
    cfg.max_total_script_bytes = usize::MAX;
    cfg.max_observations = usize::MAX;
    cfg.max_fuel = u64::MAX;

    // Normal script with extreme config should run cleanly, without fuel exhaustion triggering immediately due to u32 wrap-around
    let res_extreme = m.execute(&["var a = 1;".to_string()], Arc::new(EmptyBridge), &cfg)?;
    assert!(
        res_extreme.errors.is_empty(),
        "Extreme configuration caused script execution to fail"
    );

    // Pattern counts at exact limits
    let mut scripts = Vec::new();
    for _ in 0..256 {
        scripts.push("1;".to_string());
    }
    let res_256 = m.execute(&scripts, Arc::new(EmptyBridge), &config())?;
    assert!(
        res_256.errors.is_empty(),
        "Executing 256 scripts resulted in internal errors"
    );

    // Match counts that overflow internal buffers (massive number of observations generated)
    let overflow_observations = "
        for(let i=0; i<100000; i++) {
            // Force bridge calls to generate observations
            try { fetch('http://example.com/' + i); } catch(e) {}
        }
    "
    .to_string();
    let res_overflow = m.execute(&[overflow_observations], Arc::new(EmptyBridge), &config())?;
    // We expect observations to cap out gracefully rather than crashing or overflowing and panicking
    assert!(
        res_overflow.observations.len() <= config().max_observations,
        "Observations exceeded maximum limit"
    );

    Ok(())
}

/// Extra: Bridge adversarial inputs
struct MaliciousBridge;
impl Bridge for MaliciousBridge {
    fn call(&self, _api: &str, _args: &[Value]) -> Result<Value, String> {
        // Attack the engine by returning huge/invalid values
        Ok(Value::Null)
    }
    fn get_property(&self, _object: &str, _property: &str) -> Result<Value, String> {
        Ok(Value::Null)
    }
    fn set_property(&self, _object: &str, _property: &str, _value: &Value) -> Result<(), String> {
        Ok(())
    }
    fn provided_globals(&self) -> Vec<String> {
        vec!["fetch".to_string()]
    }
    fn bootstrap_js(&self) -> String {
        "function fetch() { Bridge.call('fetch', Array.prototype.slice.call(arguments)); }"
            .to_string()
    }
}

#[test]
fn test_bridge_adversarial() -> Result<(), Box<dyn std::error::Error>> {
    let m = module()?;
    let cfg = config();
    let script = "
        try {
            fetch('http://example.com');
        } catch(e) {}
    "
    .to_string();
    let res = m.execute(&[script], Arc::new(MaliciousBridge), &cfg)?;
    // The sandbox shouldn't crash, and no internal errors should be leaked.
    assert!(
        res.errors.is_empty(),
        "Malicious Bridge call caused JS errors"
    );
    Ok(())
}