js_sandbox/
script.rs

1// Copyright (c) 2020-2021 Jan Haller. zlib/libpng license.
2
3use std::borrow::Cow;
4use std::path::Path;
5use std::rc::Rc;
6
7use deno_core::{JsRuntime, OpState, RuntimeOptions, ZeroCopyBuf};
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use crate::{AnyError, JsValue};
12
13/// Represents a single JavaScript file that can be executed.
14///
15/// The code can be loaded from a file or from a string in memory.
16/// A typical usage pattern is to load a file with one or more JS function definitions, and then call those functions from Rust.
17pub struct Script {
18	runtime: JsRuntime,
19	last_rid: u32,
20}
21
22impl Script {
23	const DEFAULT_FILENAME: &'static str = "sandboxed.js";
24
25	/// Initialize a script with the given JavaScript source code
26	///
27	/// Returns a new object on success, and an error in case of syntax or initialization error with the code.
28	pub fn from_string(js_code: &str) -> Result<Self, AnyError> {
29		// console.log() is not available by default -- add the most basic version with single argument (and no warn/info/... variants)
30		let all_code = "const console = { log: function(expr) { Deno.core.print(expr + '\\n', false); } };".to_string() + js_code;
31
32		Self::create_script(&all_code, Self::DEFAULT_FILENAME)
33	}
34
35	/// Initialize a script by loading it from a .js file
36	///
37	/// Returns a new object on success. Fails if the file cannot be opened or in case of syntax or initialization error with the code.
38	pub fn from_file(file: impl AsRef<Path>) -> Result<Self, AnyError> {
39		let filename = file
40			.as_ref()
41			.file_name()
42			.and_then(|s| s.to_str())
43			.unwrap_or(Self::DEFAULT_FILENAME)
44			.to_owned();
45
46		match std::fs::read_to_string(file) {
47			Ok(js_code) => Self::create_script(&js_code, &filename),
48			Err(e) => Err(AnyError::from(e)),
49		}
50	}
51
52	/// Invokes a JavaScript function.
53	///
54	/// Passes a single argument `args` to JS by serializing it to JSON (using serde_json).
55	/// Multiple arguments are currently not supported, but can easily be emulated using a `Vec` to work as a JSON array.
56	pub fn call<P, R>(&mut self, fn_name: &str, args: &P) -> Result<R, AnyError>
57	where
58		P: Serialize,
59		R: DeserializeOwned,
60	{
61		let json_args = serde_json::to_value(args)?;
62		let json_result = self.call_json(fn_name, &json_args)?;
63		let result: R = serde_json::from_value(json_result)?;
64
65		Ok(result)
66	}
67
68	pub(crate) fn call_json(&mut self, fn_name: &str, args: &JsValue) -> Result<JsValue, AnyError> {
69		// Note: ops() is required to initialize internal state
70		// Wrap everything in scoped block
71
72		// undefined will cause JSON serialization error, so it needs to be treated as null
73		let js_code = format!("{{
74			let __rust_result = {f}({a});
75			if (typeof __rust_result === 'undefined')
76				__rust_result = null;
77
78			Deno.core.ops();
79			Deno.core.opSync(\"__rust_return\", __rust_result);\
80		}}", f = fn_name, a = args);
81
82		self.runtime.execute(Self::DEFAULT_FILENAME, &js_code)?;
83
84		let state_rc = self.runtime.op_state();
85		let mut state = state_rc.borrow_mut();
86		let table = &mut state.resource_table;
87
88		// Get resource, and free slot (no longer needed)
89		let entry: Rc<ResultResource> = table.take(self.last_rid).expect("Resource entry must be present");
90		let extracted = Rc::try_unwrap(entry).expect("Rc must hold single strong ref to resource entry");
91		self.last_rid += 1;
92
93		Ok(extracted.json_value)
94	}
95
96	fn create_script(js_code: &str, js_filename: &str) -> Result<Self, AnyError> {
97		let options = RuntimeOptions::default();
98
99		let mut runtime = JsRuntime::new(options);
100		runtime.execute(js_filename, &js_code)?;
101		runtime.register_op("__rust_return", deno_core::op_sync(Self::op_return));
102
103		Ok(Script { runtime, last_rid: 0 })
104	}
105
106	fn op_return(
107		state: &mut OpState,
108		args: JsValue,
109		_buf: Option<ZeroCopyBuf>,
110	) -> Result<JsValue, AnyError> {
111		let entry = ResultResource { json_value: args };
112		let resource_table = &mut state.resource_table;
113		let _rid = resource_table.add(entry);
114		//assert_eq!(rid, self.last_rid);
115
116		Ok(serde_json::Value::Null)
117	}
118}
119
120#[derive(Debug)]
121struct ResultResource {
122	json_value: JsValue
123}
124
125// Type that is stored inside Deno's resource table
126impl deno_core::Resource for ResultResource {
127	fn name(&self) -> Cow<str> {
128		"__rust_Result".into()
129	}
130}