1use std::cell::RefCell;
36use std::rc::Rc;
37
38use boa_engine::object::ObjectInitializer;
39use boa_engine::property::Attribute;
40use boa_engine::{js_string, Context, JsError, JsResult, JsString, JsValue, NativeFunction, Source};
41use serde_json::Value;
42
43pub use boa_engine;
47
48pub const LOOP_ITERATION_LIMIT: u64 = 10_000_000;
50
51pub const RECURSION_LIMIT: usize = 512;
53
54pub type Logs = Rc<RefCell<Vec<String>>>;
57
58#[derive(Debug, Clone)]
60pub struct Success {
61 pub value: Value,
63 pub logs: Vec<String>,
64}
65
66#[derive(Debug, Clone)]
70pub struct Failure {
71 pub message: String,
72 pub logs: Vec<String>,
73}
74
75pub fn call(
81 source: &str,
82 file: &str,
83 function: &str,
84 input: &Value,
85 install: &dyn Fn(&mut Context, &Logs) -> JsResult<()>,
86) -> Result<Success, Failure> {
87 let mut context = Context::default();
88 let limits = context.runtime_limits_mut();
89 limits.set_loop_iteration_limit(LOOP_ITERATION_LIMIT);
90 limits.set_recursion_limit(RECURSION_LIMIT);
91
92 let logs: Logs = Rc::new(RefCell::new(Vec::new()));
93 let fail = |message: String, logs: &Logs| Failure {
94 message,
95 logs: logs.borrow().clone(),
96 };
97
98 if let Err(error) = install_console(&mut context, &logs).and_then(|()| install(&mut context, &logs)) {
99 return Err(fail(format!("setting up the script host failed: {}", describe(error, &mut context)), &logs));
100 }
101
102 let path = std::path::Path::new(file);
103 if let Err(error) = context.eval(Source::from_bytes(source).with_path(path)) {
104 let message = describe(error, &mut context);
105 return Err(fail(format!("{file}: {message}"), &logs));
106 }
107
108 let global = context.global_object();
109 let callee = match global.get(JsString::from(function), &mut context) {
110 Ok(value) => value,
111 Err(error) => return Err(fail(describe(error, &mut context), &logs)),
112 };
113 let Some(callee) = callee.as_callable() else {
114 return Err(fail(format!("{file} defines no function {function}()"), &logs));
115 };
116
117 let argument = match JsValue::from_json(input, &mut context) {
118 Ok(value) => value,
119 Err(error) => return Err(fail(describe(error, &mut context), &logs)),
120 };
121 let returned = match callee.call(&JsValue::undefined(), &[argument], &mut context) {
122 Ok(value) => value,
123 Err(error) => return Err(fail(describe(error, &mut context), &logs)),
124 };
125 let _ = context.run_jobs();
128
129 let value = match returned.to_json(&mut context) {
130 Ok(json) => json.unwrap_or(Value::Null),
131 Err(error) => {
132 return Err(fail(
133 format!("{function}() returned a value that is not JSON: {}", describe(error, &mut context)),
134 &logs,
135 ))
136 }
137 };
138 let logs = logs.borrow().clone();
139 Ok(Success { value, logs })
140}
141
142pub fn describe(error: JsError, context: &mut Context) -> String {
144 if let Some(value) = error.as_opaque() {
145 if let Some(text) = value.as_string() {
146 return text.to_std_string_escaped();
147 }
148 }
149 match error.try_native(context) {
150 Ok(native) if !native.message().is_empty() => native.message().to_string(),
151 Ok(native) => native.to_string(),
152 Err(_) => {
153 if let Some(value) = error.as_opaque() {
155 if let Ok(Some(json)) = value.to_json(context) {
156 return json.to_string();
157 }
158 }
159 error.to_string()
160 }
161 }
162}
163
164pub fn text_of(value: &JsValue, context: &mut Context) -> String {
167 if let Some(text) = value.as_string() {
168 return text.to_std_string_escaped();
169 }
170 match value.to_json(context) {
171 Ok(Some(json)) => json.to_string(),
172 Ok(None) => "undefined".to_string(),
173 Err(_) => value.display().to_string(),
174 }
175}
176
177fn install_console(context: &mut Context, logs: &Logs) -> JsResult<()> {
179 let make = |prefix: &'static str| {
180 let logs = logs.clone();
181 unsafe {
185 NativeFunction::from_closure(move |_, args, context| {
186 let line = args
187 .iter()
188 .map(|value| text_of(value, context))
189 .collect::<Vec<_>>()
190 .join(" ");
191 logs.borrow_mut().push(format!("{prefix}{line}"));
192 Ok(JsValue::undefined())
193 })
194 }
195 };
196 let console = ObjectInitializer::new(context)
197 .function(make(""), js_string!("log"), 0)
198 .function(make(""), js_string!("info"), 0)
199 .function(make("warn: "), js_string!("warn"), 0)
200 .function(make("error: "), js_string!("error"), 0)
201 .build();
202 context.register_global_property(js_string!("console"), console, Attribute::all())
203}
204