use std::sync::Arc;
use jsdet_core::{CompiledModule, EmptyBridge, ExecutionResult, Observation, SandboxConfig};
use proptest::prelude::*;
fn module() -> CompiledModule {
CompiledModule::new().expect("Fix: CompiledModule::new() should succeed in test setup")
}
fn default_config() -> SandboxConfig {
SandboxConfig {
timeout_ms: 5000,
max_fuel: 100_000_000,
max_memory_bytes: 16 * 1024 * 1024,
max_scripts: 100,
max_script_bytes: 1024 * 1024,
max_total_script_bytes: 5 * 1024 * 1024,
..SandboxConfig::default()
}
}
fn assert_no_panic<T>(result: &Result<T, jsdet_core::Error>) {
assert!(
result.is_ok() || result.is_err(),
"Fix: Execution must not panic"
);
}
fn assert_resource_bounded(result: &ExecutionResult) {
let resource_limited = result
.observations
.iter()
.any(|o| matches!(o, Observation::ResourceLimit { .. }));
assert!(
result.scripts_executed >= 1 || resource_limited,
"Fix: Should execute at least one script or hit resource limit"
);
}
fn valid_package_name() -> impl Strategy<Value = String> {
prop_oneof![
"[a-z][a-z0-9-]{0,50}".prop_map(|s| s.to_string()),
"@[a-z][a-z0-9-]{0,20}/[a-z][a-z0-9-]{0,50}".prop_map(|s| s.to_string()),
]
}
fn valid_semver() -> impl Strategy<Value = String> {
prop_oneof![
"[0-9].[0-9].[0-9]".prop_map(|s| s.to_string()),
"~[0-9].[0-9].[0-9]".prop_map(|s| s.to_string()),
"\\^[0-9].[0-9].[0-9]".prop_map(|s| s.to_string()),
">=[0-9].[0-9].[0-9]".prop_map(|s| s.to_string()),
]
}
fn valid_package_json() -> impl Strategy<Value = String> {
(
valid_package_name(),
valid_semver(),
prop::option::of(valid_semver()),
prop::bool::ANY,
)
.prop_map(|(name, version, dep_version, has_scripts)| {
let deps = match dep_version {
Some(v) => format!(r#", "dependencies": {{ "dep-1": "{}" }}"#, v),
None => String::new(),
};
let scripts = if has_scripts {
r#", "scripts": { "test": "jest", "build": "tsc" }"#.to_string()
} else {
String::new()
};
format!(
r#"{{"name": "{}", "version": "{}"{}{}}}"#,
name, version, deps, scripts
)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn package_json_parses_without_panic(pkg_json in valid_package_json()) {
let script = format!("var pkg = {};", pkg_json);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
if let Ok(r) = result {
assert_eq!(r.scripts_executed, 1, "Fix: Valid JSON should execute");
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn malformed_json_does_not_crash(jsonish in "\\{[^}]{0,1000}\\}") {
let script = format!("try {{ var x = {}; }} catch (e) {{}}", jsonish);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
fn arbitrary_js_expression() -> impl Strategy<Value = String> {
prop_oneof![
"null".prop_map(|s| s.to_string()),
"undefined".prop_map(|s| s.to_string()),
"true".prop_map(|s| s.to_string()),
"false".prop_map(|s| s.to_string()),
"[0-9]{1,10}".prop_map(|s| s.to_string()),
"\"[a-zA-Z0-9]{0,50}\"".prop_map(|s| s.to_string()),
"[a-z][a-z0-9]{0,20}".prop_map(|s| s.to_string()),
]
}
fn arbitrary_js_statement() -> impl Strategy<Value = String> {
prop_oneof![
(arbitrary_js_expression(), arbitrary_js_expression())
.prop_map(|(name, val)| format!("var {} = {};", name, val)),
arbitrary_js_expression().prop_map(|f| format!("{}();", f)),
(arbitrary_js_expression(), "[a-z][a-z0-9]{0,20}")
.prop_map(|(obj, prop)| format!("{}.{};", obj, prop)),
arbitrary_js_expression().prop_map(|e| format!("try {{ {} }} catch (e) {{}}", e)),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn arbitrary_js_executes_safely(statement in arbitrary_js_statement()) {
let result = module().execute(
&[statement],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
if let Ok(r) = result {
assert_resource_bounded(&r);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn concatenated_js_executes_safely(statements in prop::collection::vec(arbitrary_js_statement(), 1..20)) {
let script = statements.join("\n");
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn string_processing_is_safe(input in "\\PC{0,1000}") {
let escaped = input.replace('\\', "\\\\").replace('"', "\\\"");
let script = format!(
r#"var s = "{}"; var upper = s.toUpperCase(); var lower = s.toLowerCase(); var len = s.length;"#,
escaped
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
if let Ok(r) = result {
assert_eq!(r.scripts_executed, 1);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn string_concatenation_is_safe(
parts in prop::collection::vec("[a-zA-Z0-9]{0,100}", 1..50)
) {
let json_array = serde_json::to_string(&parts).unwrap();
let script = format!(
r#"var parts = {}; var result = ''; for (var i = 0; i < parts.length; i++) {{ result += parts[i]; }}"#,
json_array
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn numeric_operations_are_safe(a in -1e15f64..1e15f64, b in -1e15f64..1e15f64) {
let script = format!(
r#"var a = {}; var b = {}; var sum = a + b; var diff = a - b; var prod = a * b; var quot = b !== 0 ? a / b : 0;"#,
a, b
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn extreme_numeric_values(num in prop::num::f64::ANY) {
let script = format!(
r#"var n = {}; var isFinite = Number.isFinite(n); var isNaN = Number.isNaN(n); var str = n.toString();"#,
num
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn array_operations_are_safe(size in 0usize..1000usize) {
let script = format!(
r#"var arr = new Array({}); for (var i = 0; i < arr.length; i++) {{ arr[i] = i; }} var mapped = arr.map(function(x) {{ return x * 2; }});"#,
size
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn object_operations_are_safe(
keys in prop::collection::vec("[a-z][a-z0-9]{0,20}", 0..50)
) {
let obj_lit = keys
.iter()
.enumerate()
.map(|(i, k)| format!("{}: {}", k, i))
.collect::<Vec<_>>()
.join(", ");
let script = format!(
r#"var obj = {{ {} }}; var keys = Object.keys(obj); var vals = Object.values(obj); var json = JSON.stringify(obj); var parsed = JSON.parse(json);"#,
obj_lit
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn execution_isolation_is_maintained(
first_script in arbitrary_js_statement(),
second_script in arbitrary_js_statement()
) {
let m = module();
let bridge = Arc::new(EmptyBridge);
let config = default_config();
let r1 = m.execute(&[first_script], bridge.clone(), &config);
assert_no_panic(&r1);
let r2 = m.execute(
&[second_script],
bridge,
&config,
);
assert_no_panic(&r2);
if let Ok(r) = r2 {
assert!(r.scripts_executed >= 1, "Fix: Second execution should run");
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn unicode_handling_is_safe(
input in prop::collection::vec(prop::num::u32::ANY, 0..100)
) {
let chars: String = input
.iter()
.filter(|&&cp| {
!(0xD800..=0xDFFF).contains(&cp)
})
.filter_map(|&cp| char::from_u32(cp))
.collect();
let escaped: String = chars
.chars()
.flat_map(|c| {
match c {
'\\' => vec!['\\', '\\'],
'"' => vec!['\\', '"'],
c => vec![c],
}
})
.collect();
let script = format!(
r#"var s = "{}"; var len = s.length; var upper = s.toUpperCase();"#,
escaped
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn nested_objects_are_safe(depth in 0usize..100usize) {
let mut obj = "null".to_string();
for _ in 0..depth {
obj = format!("{{nested: {}}}", obj);
}
let script = format!(
r#"var obj = {}; var d = 0; var c = obj; while (c && c.nested) {{ d++; c = c.nested; }}"#,
obj
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn nested_arrays_are_safe(depth in 0usize..100usize) {
let mut arr = "1".to_string();
for _ in 0..depth {
arr = format!("[{}]", arr);
}
let script = format!(
r#"var arr = {}; var f = arr.flat(Infinity);"#,
arr
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn infinite_loops_are_terminated(
max_fuel in 1_000_000u64..50_000_000u64
) {
let config = SandboxConfig {
max_fuel,
timeout_ms: 1000,
..default_config()
};
let result = module().execute(
&["while(true) { var x = 1; }".into()],
Arc::new(EmptyBridge),
&config,
);
match result {
Ok(r) => {
let resource_limited = r.observations.iter().any(|o| {
matches!(o, Observation::ResourceLimit { .. })
});
assert!(
resource_limited || r.timed_out,
"Fix: Infinite loop must be resource-limited"
);
}
Err(jsdet_core::Error::FuelExhausted { .. }) => {}
Err(jsdet_core::Error::Timeout { .. }) => {}
Err(e) => panic!("Fix: Unexpected error: {}", e),
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn json_roundtrip_is_safe(
num in -1000000i64..1000000i64,
str in "[a-zA-Z0-9]{0,50}"
) {
let json_str = format!(r#"{{"num": {}, "str": "{}"}}"#, num, str);
let script = format!(
r#"var orig = {}; var s = JSON.stringify(orig); var parsed = JSON.parse(s); var dbl = JSON.parse(JSON.stringify(parsed));"#,
json_str
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
if let Ok(r) = result {
assert_eq!(r.scripts_executed, 1);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn regex_operations_are_safe(
pattern in "[a-zA-Z0-9.*+?]{0,20}",
input in "[a-zA-Z0-9]{0,100}"
) {
let pattern_json = serde_json::to_string(&pattern).unwrap();
let input_json = serde_json::to_string(&input).unwrap();
let script = format!(
"try {{ var re = new RegExp({}); var r = re.test({}); var m = {}.match(re); }} catch (e) {{}}",
pattern_json, input_json, input_json
);
let result = module().execute(
&[script],
Arc::new(EmptyBridge),
&default_config(),
);
assert_no_panic(&result);
}
}