use serde_json::Value;
use super::error::BridgeError;
pub fn build_iife(body: &str) -> String {
format!("(function(){{ {body} }})()")
}
pub fn build_iife_with_args(body: &str, args: &[Value]) -> Result<String, BridgeError> {
let args_json = serde_json::to_string(args)
.map_err(|e| BridgeError::InvalidParams(format!("args serialization failed: {e}")))?;
Ok(format!(
"(function(){{ var __args={args_json}; return (function(){{ {body} }}).apply(null, __args); }})()"
))
}
#[allow(dead_code)]
pub fn build_iife_element(
body: &str,
backend_node_id: i64,
extra_args: &[Value],
) -> Result<String, BridgeError> {
let extra_json = serde_json::to_string(extra_args)
.map_err(|e| BridgeError::InvalidParams(format!("extra args serialization failed: {e}")))?;
Ok(format!(
"(function(){{ var __args={extra_json}; return (function(){{ var el=document.querySelector('[data-bao-backend-node-id=\"{backend_node_id}\"]'); if(!el){{throw new Error('element not found: {backend_node_id}');}} {body} }}).apply(null, __args); }})()"
))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn build_iife_wraps_body_in_iife() {
let s = build_iife("return document.title;");
assert!(s.starts_with("(function(){"));
assert!(s.ends_with("})()"));
assert!(s.contains("return document.title;"));
}
#[test]
fn build_iife_with_args_serializes_args_as_json() {
let s = build_iife_with_args("return __args[0];", &[json!("hello")]).unwrap();
assert!(s.contains("var __args=[\"hello\"];"));
assert!(s.contains("return __args[0];"));
assert!(s.contains(").apply(null, __args);"));
}
#[test]
fn build_iife_with_args_empty_array_is_safe() {
let s = build_iife_with_args("return document.title;", &[]).unwrap();
assert!(s.contains("var __args=[];"));
}
#[test]
fn injection_single_quote_in_string_is_escaped() {
let payload = "');alert('xss');//";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
let args_marker = "var __args=";
let args_pos = s.find(args_marker).unwrap();
let args_end_rel = s[args_pos..].find("];").unwrap();
let args_literal = &s[args_pos..args_pos + args_end_rel + 1];
assert!(args_literal.contains("\"');alert('xss');//\""));
assert!(s.contains("return __args[0];"));
let body_start = s.find("return (function(){").unwrap();
let body_end = s.find("}).apply(null, __args);").unwrap();
let body = &s[body_start..body_end];
assert!(!body.contains(&format!("return {payload};")));
}
#[test]
fn injection_double_quote_in_string_is_escaped() {
let payload = "\";alert(\"x\");//";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\\\""));
}
#[test]
fn injection_backslash_in_string_is_escaped() {
let payload = "\\;alert(1);//";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\\\\"));
}
#[test]
fn injection_script_close_tag_is_neutralized() {
let payload = "</script><script>alert(1)</script>";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\"</script>"));
assert!(!s.contains("return </script>"));
}
#[test]
fn injection_template_literal_syntax_is_neutralized() {
let payload = "${alert(1)}";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\"${alert(1)}\""));
assert!(!s.contains("return ${alert(1)};"));
}
#[test]
fn injection_newline_in_string_is_escaped() {
let payload = "a\nb";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\\n"));
assert!(!s.contains("\"a\nb\""));
}
#[test]
fn injection_unicode_control_chars_are_escaped() {
let payload = "\u{0000}\u{001B}";
let s = build_iife_with_args("return __args[0];", &[json!(payload)]).unwrap();
assert!(s.contains("\\u0000") || s.contains("\\u001b") || s.contains("\\u001B"));
}
#[test]
fn number_args_are_serialized_correctly() {
let s = build_iife_with_args("return __args[0]+__args[1];", &[json!(1), json!(2)]).unwrap();
assert!(s.contains("var __args=[1,2];"));
}
#[test]
fn bool_args_are_serialized_correctly() {
let s = build_iife_with_args("return __args[0];", &[json!(true)]).unwrap();
assert!(s.contains("var __args=[true];"));
}
#[test]
fn null_args_are_serialized_correctly() {
let s = build_iife_with_args("return __args[0];", &[Value::Null]).unwrap();
assert!(s.contains("var __args=[null];"));
}
#[test]
fn object_args_are_serialized_correctly() {
let s = build_iife_with_args("return __args[0].x;", &[json!({"x": 1, "y": "a"})]).unwrap();
assert!(s.contains("var __args=[{\"x\":1,\"y\":\"a\"}];"));
}
#[test]
fn array_args_are_serialized_correctly() {
let s =
build_iife_with_args("return __args[0].length;", &[json!(["a", "b", "c"])]).unwrap();
assert!(s.contains("var __args=[[\"a\",\"b\",\"c\"]]"));
}
#[test]
fn multiple_args_preserve_order() {
let s = build_iife_with_args(
"return [__args[0], __args[1], __args[2]];",
&[json!("first"), json!(42), json!(true)],
)
.unwrap();
assert!(s.contains("var __args=[\"first\",42,true]"));
}
#[test]
fn iife_starts_and_ends_correctly() {
let s = build_iife_with_args("return 1;", &[]).unwrap();
assert!(s.starts_with("(function(){"));
assert!(s.ends_with("})()"));
}
#[test]
fn iife_uses_apply_to_bind_args() {
let s = build_iife_with_args("return __args[0];", &[json!(1)]).unwrap();
assert!(s.contains("}).apply(null, __args);"));
}
#[test]
fn iife_body_appears_after_args_declaration() {
let s = build_iife_with_args("/*BODY*/", &[]).unwrap();
let args_pos = s.find("var __args").unwrap();
let body_pos = s.find("/*BODY*/").unwrap();
assert!(args_pos < body_pos);
}
#[test]
fn build_iife_element_inserts_backend_node_id() {
let s = build_iife_element("return el.textContent;", 42, &[]).unwrap();
assert!(s.contains("data-bao-backend-node-id=\"42\""));
assert!(s.contains("throw new Error('element not found: 42')"));
assert!(s.contains("return el.textContent;"));
}
}