#![deny(warnings)]
#![deny(missing_docs)]
mod bindings;
mod callback;
mod droppable_value;
mod value;
use std::{convert::TryFrom, error, fmt};
pub use callback::Callback;
pub use value::*;
#[derive(PartialEq, Debug)]
pub enum ExecutionError {
InputWithZeroBytes,
Conversion(ValueError),
Internal(String),
Exception(JsValue),
OutOfMemory,
#[doc(hidden)]
__NonExhaustive,
}
impl fmt::Display for ExecutionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ExecutionError::*;
match self {
InputWithZeroBytes => write!(f, "Invalid script input: code contains zero byte (\\0)"),
Conversion(e) => e.fmt(f),
Internal(e) => write!(f, "Internal error: {}", e),
Exception(e) => write!(f, "{:?}", e),
OutOfMemory => write!(f, "Out of memory: runtime memory limit exceeded"),
__NonExhaustive => unreachable!(),
}
}
}
impl error::Error for ExecutionError {}
impl From<ValueError> for ExecutionError {
fn from(v: ValueError) -> Self {
ExecutionError::Conversion(v)
}
}
#[derive(Debug)]
pub enum ContextError {
RuntimeCreationFailed,
ContextCreationFailed,
#[doc(hidden)]
__NonExhaustive,
}
impl fmt::Display for ContextError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ContextError::*;
match self {
RuntimeCreationFailed => write!(f, "Could not create runtime"),
ContextCreationFailed => write!(f, "Could not create context"),
__NonExhaustive => unreachable!(),
}
}
}
impl error::Error for ContextError {}
pub struct ContextBuilder {
memory_limit: Option<usize>,
}
impl ContextBuilder {
fn new() -> Self {
Self { memory_limit: None }
}
pub fn memory_limit(self, max_bytes: usize) -> Self {
let mut s = self;
s.memory_limit = Some(max_bytes);
s
}
pub fn build(self) -> Result<Context, ContextError> {
let wrapper = bindings::ContextWrapper::new(self.memory_limit)?;
Ok(Context::from_wrapper(wrapper))
}
}
pub struct Context {
wrapper: bindings::ContextWrapper,
}
impl Context {
fn from_wrapper(wrapper: bindings::ContextWrapper) -> Self {
Self { wrapper }
}
pub fn builder() -> ContextBuilder {
ContextBuilder::new()
}
pub fn new() -> Result<Self, ContextError> {
let wrapper = bindings::ContextWrapper::new(None)?;
Ok(Self::from_wrapper(wrapper))
}
pub fn reset(self) -> Result<Self, ContextError> {
let wrapper = self.wrapper.reset()?;
Ok(Self { wrapper })
}
pub fn eval(&self, code: &str) -> Result<JsValue, ExecutionError> {
let value_raw = self.wrapper.eval(code)?;
let value = value_raw.to_value()?;
Ok(value)
}
pub fn eval_as<R>(&self, code: &str) -> Result<R, ExecutionError>
where
R: TryFrom<JsValue>,
R::Error: Into<ValueError>,
{
let value_raw = self.wrapper.eval(code)?;
let value = value_raw.to_value()?;
let ret = R::try_from(value).map_err(|e| e.into())?;
Ok(ret)
}
pub fn call_function(
&self,
function_name: &str,
args: impl IntoIterator<Item = impl Into<JsValue>>,
) -> Result<JsValue, ExecutionError> {
let qargs = args
.into_iter()
.map(|arg| self.wrapper.serialize_value(arg.into()))
.collect::<Result<Vec<_>, _>>()?;
let global = self.wrapper.global()?;
let func_obj = global.property(function_name)?;
if !func_obj.is_object() {
return Err(ExecutionError::Internal(format!(
"Could not find function '{}' in global scope: does not exist, or not an object",
function_name
)));
}
let value = self.wrapper.call_function(func_obj, qargs)?.to_value()?;
Ok(value)
}
pub fn add_callback<F>(
&self,
name: &str,
callback: impl Callback<F> + 'static,
) -> Result<(), ExecutionError> {
self.wrapper.add_callback(name, callback)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn test_eval_pass() {
use std::iter::FromIterator;
let c = Context::new().unwrap();
let cases = vec![
("null", Ok(JsValue::Null)),
("true", Ok(JsValue::Bool(true))),
("2 > 10", Ok(JsValue::Bool(false))),
("1", Ok(JsValue::Int(1))),
("1 + 1", Ok(JsValue::Int(2))),
("1.1", Ok(JsValue::Float(1.1))),
("2.2 * 2 + 5", Ok(JsValue::Float(9.4))),
("\"abc\"", Ok(JsValue::String("abc".into()))),
(
"[1,2]",
Ok(JsValue::Array(vec![JsValue::Int(1), JsValue::Int(2)])),
),
];
for (code, res) in cases.into_iter() {
assert_eq!(c.eval(code), res,);
}
let obj_cases = vec![
(
r#" {"a": null} "#,
Ok(JsValue::Object(HashMap::from_iter(vec![(
"a".to_string(),
JsValue::Null,
)]))),
),
(
r#" {a: 1, b: true, c: {c1: false}} "#,
Ok(JsValue::Object(HashMap::from_iter(vec![
("a".to_string(), JsValue::Int(1)),
("b".to_string(), JsValue::Bool(true)),
(
"c".to_string(),
JsValue::Object(HashMap::from_iter(vec![(
"c1".to_string(),
JsValue::Bool(false),
)])),
),
]))),
),
];
for (index, (code, res)) in obj_cases.into_iter().enumerate() {
let full_code = format!(
"var v{index} = {code}; v{index}",
index = index,
code = code
);
assert_eq!(c.eval(&full_code), res,);
}
assert_eq!(c.eval_as::<bool>("true").unwrap(), true,);
assert_eq!(c.eval_as::<i32>("1 + 2").unwrap(), 3,);
let value: String = c.eval_as("var x = 44; x.toString()").unwrap();
assert_eq!(&value, "44");
}
#[test]
fn test_eval_syntax_error() {
let c = Context::new().unwrap();
assert_eq!(
c.eval(
r#"
!!!!
"#
),
Err(ExecutionError::Exception(
"SyntaxError: unexpected token in expression: \'\'".into()
))
);
}
#[test]
fn test_eval_exception() {
let c = Context::new().unwrap();
assert_eq!(
c.eval(
r#"
function f() {
throw new Error("My Error");
}
f();
"#
),
Err(ExecutionError::Exception("Error: My Error".into(),))
);
}
#[test]
fn eval_async() {
let c = Context::new().unwrap();
let value = c
.eval(
r#"
new Promise((resolve, _) => {
resolve(33);
})
"#,
)
.unwrap();
assert_eq!(value, JsValue::Int(33));
let res = c.eval(
r#"
new Promise((_resolve, reject) => {
reject("Failed...");
})
"#,
);
assert_eq!(
res,
Err(ExecutionError::Exception(JsValue::String(
"Failed...".into()
)))
);
}
#[test]
fn test_call() {
let c = Context::new().unwrap();
assert_eq!(
c.call_function("parseInt", vec!["22"]).unwrap(),
JsValue::Int(22),
);
c.eval(
r#"
function add(a, b) {
return a + b;
}
"#,
)
.unwrap();
assert_eq!(
c.call_function("add", vec![5, 7]).unwrap(),
JsValue::Int(12),
);
c.eval(
r#"
function sumArray(arr) {
let sum = 0;
for (const value of arr) {
sum += value;
}
return sum;
}
"#,
)
.unwrap();
assert_eq!(
c.call_function("sumArray", vec![vec![1, 2, 3]]).unwrap(),
JsValue::Int(6),
);
c.eval(
r#"
function addObject(obj) {
let sum = 0;
for (const key of Object.keys(obj)) {
sum += obj[key];
}
return sum;
}
"#,
)
.unwrap();
let mut obj = std::collections::HashMap::<String, i32>::new();
obj.insert("a".into(), 10);
obj.insert("b".into(), 20);
obj.insert("c".into(), 30);
assert_eq!(
c.call_function("addObject", vec![obj]).unwrap(),
JsValue::Int(60),
);
}
#[test]
fn test_call_large_string() {
let c = Context::new().unwrap();
c.eval(" function strLen(s) { return s.length; } ").unwrap();
let s = " ".repeat(200_000);
let v = c.call_function("strLen", vec![s]).unwrap();
assert_eq!(v, JsValue::Int(200_000));
}
#[test]
fn call_async() {
let c = Context::new().unwrap();
c.eval(
r#"
function asyncOk() {
return new Promise((resolve, _) => {
resolve(33);
});
}
function asyncErr() {
return new Promise((_resolve, reject) => {
reject("Failed...");
});
}
"#,
)
.unwrap();
let value = c.call_function("asyncOk", vec![true]).unwrap();
assert_eq!(value, JsValue::Int(33));
let res = c.call_function("asyncErr", vec![true]);
assert_eq!(
res,
Err(ExecutionError::Exception(JsValue::String(
"Failed...".into()
)))
);
}
#[test]
fn test_callback() {
let c = Context::new().unwrap();
c.add_callback("cb1", |flag: bool| !flag).unwrap();
assert_eq!(c.eval("cb1(true)").unwrap(), JsValue::Bool(false),);
c.add_callback("concat2", |a: String, b: String| format!("{}{}", a, b))
.unwrap();
assert_eq!(
c.eval(r#"concat2("abc", "def")"#).unwrap(),
JsValue::String("abcdef".into()),
);
c.add_callback("add2", |a: i32, b: i32| -> i32 { a + b })
.unwrap();
assert_eq!(c.eval("add2(5, 11)").unwrap(), JsValue::Int(16),);
}
#[test]
fn test_callback_argn_variants() {
macro_rules! callback_argn_tests {
[
$(
$len:literal : ( $( $argn:ident : $argv:literal ),* ),
)*
] => {
$(
{
let name = format!("cb{}", $len);
let c = Context::new().unwrap();
c.add_callback(&name, | $( $argn : i32 ),*| -> i32 {
$( $argn + )* 0
}).unwrap();
let code = format!("{}( {} )", name, "1,".repeat($len));
let v = c.eval(&code).unwrap();
assert_eq!(v, JsValue::Int($len));
let name = format!("cbres{}", $len);
c.add_callback(&name, | $( $argn : i32 ),*| -> Result<i32, String> {
Ok($( $argn + )* 0)
}).unwrap();
let code = format!("{}( {} )", name, "1,".repeat($len));
let v = c.eval(&code).unwrap();
assert_eq!(v, JsValue::Int($len));
let name = format!("cbreserr{}", $len);
c.add_callback(&name, #[allow(unused_variables)] | $( $argn : i32 ),*| -> Result<i32, String> {
Err("error".into())
}).unwrap();
let code = format!("{}( {} )", name, "1,".repeat($len));
let res = c.eval(&code);
assert_eq!(res, Err(ExecutionError::Exception("error".into())));
}
)*
}
}
callback_argn_tests![
1: (a : 1),
]
}
#[test]
fn test_callback_invalid_argcount() {
let c = Context::new().unwrap();
c.add_callback("cb", |a: i32, b: i32| a + b).unwrap();
assert_eq!(
c.eval(" cb(5) "),
Err(ExecutionError::Exception(
"Invalid argument count: Expected 2, got 1".into()
)),
);
}
#[test]
fn memory_limit_exceeded() {
let c = Context::builder().memory_limit(100_000).build().unwrap();
assert_eq!(
c.eval(" 'abc'.repeat(200_000) "),
Err(ExecutionError::OutOfMemory),
);
}
#[test]
fn context_reset() {
let c = Context::new().unwrap();
c.eval(" var x = 123; ").unwrap();
c.add_callback("myCallback", || true).unwrap();
let c2 = c.reset().unwrap();
assert_eq!(
c2.eval_as::<String>(" 'abc'.repeat(2) ").unwrap(),
"abcabc".to_string(),
);
let err_msg = c2.eval(" x ").unwrap_err().to_string();
assert!(err_msg.contains("ReferenceError"));
let err_msg = c2.eval(" myCallback() ").unwrap_err().to_string();
assert!(err_msg.contains("ReferenceError"));
}
#[inline(never)]
fn build_context() -> Context {
let ctx = Context::new().unwrap();
let name = "cb".to_string();
ctx.add_callback(&name, |a: String| a.repeat(2)).unwrap();
let code = " function f(value) { return cb(value); } ".to_string();
ctx.eval(&code).unwrap();
ctx
}
#[test]
fn moved_context() {
let c = build_context();
let v = c.call_function("f", vec!["test"]).unwrap();
assert_eq!(v, "testtest".into());
let v = c.eval(" f('la') ").unwrap();
assert_eq!(v, "lala".into());
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_serialize() {
let c = build_context();
c.eval(
"
function dateToTimestamp(date) {
return date.getTime();
}
",
)
.unwrap();
let now = chrono::Utc::now();
let now_millis = now.timestamp_millis();
let timestamp = c
.call_function("dateToTimestamp", vec![JsValue::Date(now.clone())])
.unwrap();
assert_eq!(timestamp, JsValue::Float(now_millis as f64));
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_deserialize() {
use chrono::offset::TimeZone;
let c = build_context();
let value = c.eval(" new Date(1234567555) ").unwrap();
let datetime = chrono::Utc.timestamp_millis(1234567555);
assert_eq!(value, JsValue::Date(datetime));
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_roundtrip() {
let c = build_context();
c.eval(" function identity(x) { return x; } ").unwrap();
let d = chrono::Utc::now();
let td = JsValue::Date(d.clone());
let td2 = c.call_function("identity", vec![td.clone()]).unwrap();
let d2 = if let JsValue::Date(x) = td2 {
x
} else {
panic!("expected date")
};
assert_eq!(d.timestamp_millis(), d2.timestamp_millis());
}
}