feat(quickjs): add quickjs

from: https://crates.io/crates/quick-js
This commit is contained in:
2020-07-12 12:53:57 +08:00
parent 2ff135faaa
commit ae0b65aef3
55 changed files with 91533 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
use std::{convert::TryFrom, marker::PhantomData, panic::RefUnwindSafe};
use crate::value::{JsValue, ValueError};
pub trait IntoCallbackResult {
fn into_callback_res(self) -> Result<JsValue, String>;
}
impl<T: Into<JsValue>> IntoCallbackResult for T {
fn into_callback_res(self) -> Result<JsValue, String> {
Ok(self.into())
}
}
impl<T: Into<JsValue>, E: std::fmt::Display> IntoCallbackResult for Result<T, E> {
fn into_callback_res(self) -> Result<JsValue, String> {
match self {
Ok(v) => Ok(v.into()),
Err(e) => Err(e.to_string()),
}
}
}
/// The Callback trait is implemented for functions/closures that can be
/// used as callbacks in the JS runtime.
pub trait Callback<F>: RefUnwindSafe {
/// The number of JS arguments required.
fn argument_count(&self) -> usize;
/// Execute the callback.
///
/// Should return:
/// - Err(_) if the JS values could not be converted
/// - Ok(Err(_)) if an error ocurred while processing.
/// The given error will be raised as a JS exception.
/// - Ok(Ok(result)) when execution succeeded.
fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError>;
}
macro_rules! impl_callback {
(@call $len:literal $self:ident $args:ident ) => {
$self()
};
(@call $len:literal $self:ident $args:ident $( $arg:ident ),* ) => {
{
let mut iter = $args.into_iter();
$self(
$(
$arg::try_from(iter.next().unwrap())?,
)*
)
}
};
[ $( $len:literal : ( $( $arg:ident, )* ), )* ] => {
$(
impl<
$( $arg, )*
R,
F,
> Callback<PhantomData<(
$( &$arg, )*
&R,
&F,
)>> for F
where
$( $arg: TryFrom<JsValue, Error = ValueError>, )*
R: IntoCallbackResult,
F: Fn( $( $arg, )* ) -> R + Sized + RefUnwindSafe,
{
fn argument_count(&self) -> usize {
$len
}
fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
if args.len() != $len {
return Ok(Err(format!(
"Invalid argument count: Expected {}, got {}",
self.argument_count(),
args.len()
)));
}
let res = impl_callback!(@call $len self args $($arg),* );
Ok(res.into_callback_res())
}
}
)*
};
}
impl_callback![
0: (),
1: (A1,),
2: (A1, A2,),
3: (A1, A2, A3,),
4: (A1, A2, A3, A4,),
5: (A1, A2, A3, A4, A5,),
];
/// A wrapper around Vec<JsValue>, used for vararg callbacks.
///
/// To create a callback with a variable number of arguments, a callback closure
/// must take a single `Arguments` argument.
pub struct Arguments(Vec<JsValue>);
impl Arguments {
/// Unpack the arguments into a Vec.
pub fn into_vec(self) -> Vec<JsValue> {
self.0
}
}
impl<F> Callback<PhantomData<(&Arguments, &F)>> for F
where
F: Fn(Arguments) + Sized + RefUnwindSafe,
{
fn argument_count(&self) -> usize {
0
}
fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
(self)(Arguments(args));
Ok(Ok(JsValue::Null))
}
}
impl<F, R> Callback<PhantomData<(&Arguments, &F, &R)>> for F
where
R: IntoCallbackResult,
F: Fn(Arguments) -> R + Sized + RefUnwindSafe,
{
fn argument_count(&self) -> usize {
0
}
fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
let res = (self)(Arguments(args));
Ok(res.into_callback_res())
}
}
// Implement Callback for Fn() -> R functions.
//impl<R, F> Callback<PhantomData<(&R, &F)>> for F
//where
//R: Into<JsValue>,
//F: Fn() -> R + Sized + RefUnwindSafe,
//{
//fn argument_count(&self) -> usize {
//0
//}
//fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
//if !args.is_empty() {
//return Ok(Err(format!(
//"Invalid argument count: Expected 0, got {}",
//args.len()
//)));
//}
//let res = self().into();
//Ok(Ok(res))
//}
//}
// Implement Callback for Fn(A) -> R functions.
//impl<A1, R, F> Callback<PhantomData<(&A1, &R, &F)>> for F
//where
//A1: TryFrom<JsValue, Error = ValueError>,
//R: Into<JsValue>,
//F: Fn(A1) -> R + Sized + RefUnwindSafe,
//{
//fn argument_count(&self) -> usize {
//1
//}
//fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
//if args.len() != 1 {
//return Ok(Err(format!(
//"Invalid argument count: Expected 1, got {}",
//args.len()
//)));
//}
//let arg_raw = args.into_iter().next().expect("Invalid argument count");
//let arg = A1::try_from(arg_raw)?;
//let res = self(arg).into();
//Ok(Ok(res))
//}
//}
//// Implement Callback for Fn(A1, A2) -> R functions.
//impl<A1, A2, R, F> Callback<PhantomData<(&A1, &A2, &R, &F)>> for F
//where
//A1: TryFrom<JsValue, Error = ValueError>,
//A2: TryFrom<JsValue, Error = ValueError>,
//R: Into<JsValue>,
//F: Fn(A1, A2) -> R + Sized + RefUnwindSafe,
//{
//fn argument_count(&self) -> usize {
//2
//}
//fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
//if args.len() != 2 {
//return Ok(Err(format!(
//"Invalid argument count: Expected 2, got {}",
//args.len()
//)));
//}
//let mut iter = args.into_iter();
//let arg1_raw = iter.next().expect("Invalid argument count");
//let arg1 = A1::try_from(arg1_raw)?;
//let arg2_raw = iter.next().expect("Invalid argument count");
//let arg2 = A2::try_from(arg2_raw)?;
//let res = self(arg1, arg2).into();
//Ok(Ok(res))
//}
//}
// Implement Callback for Fn(A1, A2, A3) -> R functions.
//impl<A1, A2, A3, R, F> Callback<PhantomData<(&A1, &A2, &A3, &R, &F)>> for F
//where
//A1: TryFrom<JsValue, Error = ValueError>,
//A2: TryFrom<JsValue, Error = ValueError>,
//A3: TryFrom<JsValue, Error = ValueError>,
//R: Into<JsValue>,
//F: Fn(A1, A2, A3) -> R + Sized + RefUnwindSafe,
//{
//fn argument_count(&self) -> usize {
//3
//}
//fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
//if args.len() != self.argument_count() {
//return Ok(Err(format!(
//"Invalid argument count: Expected 3, got {}",
//args.len()
//)));
//}
//let mut iter = args.into_iter();
//let arg1_raw = iter.next().expect("Invalid argument count");
//let arg1 = A1::try_from(arg1_raw)?;
//let arg2_raw = iter.next().expect("Invalid argument count");
//let arg2 = A2::try_from(arg2_raw)?;
//let arg3_raw = iter.next().expect("Invalid argument count");
//let arg3 = A3::try_from(arg3_raw)?;
//let res = self(arg1, arg2, arg3).into();
//Ok(Ok(res))
//}
//}
//// Implement Callback for Fn(A1, A2, A3, A4) -> R functions.
//impl<A1, A2, A3, A4, R, F> Callback<PhantomData<(&A1, &A2, &A3, &A4, &R, &F)>> for F
//where
//A1: TryFrom<JsValue, Error = ValueError>,
//A2: TryFrom<JsValue, Error = ValueError>,
//A3: TryFrom<JsValue, Error = ValueError>,
//A4: TryFrom<JsValue, Error = ValueError>,
//R: Into<JsValue>,
//F: Fn(A1, A2, A3) -> R + Sized + RefUnwindSafe,
//{
//fn argument_count(&self) -> usize {
//4
//}
//fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
//if args.len() != self.argument_count() {
//return Ok(Err(format!(
//"Invalid argument count: Expected 3, got {}",
//args.len()
//)));
//}
//let mut iter = args.into_iter();
//let arg1_raw = iter.next().expect("Invalid argument count");
//let arg1 = A1::try_from(arg1_raw)?;
//let arg2_raw = iter.next().expect("Invalid argument count");
//let arg2 = A2::try_from(arg2_raw)?;
//let arg3_raw = iter.next().expect("Invalid argument count");
//let arg3 = A3::try_from(arg3_raw)?;
//let res = self(arg1, arg2, arg3).into();
//Ok(Ok(res))
//}
//}
// RESULT variants.

View File

@@ -0,0 +1,137 @@
//! Javascript console integration.
//! See the [ConsoleBackend] trait for more info.
use super::JsValue;
/// Log level of a log message sent via the console.
/// These levels represent the different functions defined in the spec:
/// https://s3.amazonaws.com/temp.michaelfbryan.com/callbacks/index.html
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Level {
Trace,
Debug,
Log,
Info,
Warn,
Error,
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Level::*;
let v = match self {
Trace => "trace",
Debug => "debug",
Log => "log",
Info => "info",
Warn => "warn",
Error => "error",
};
write!(f, "{}", v)
}
}
/// A console backend that handles console messages sent from JS via
/// console.{log,debug,trace,...} functions.
///
/// A backend has to be registered via the `ContextBuilder::console` method.
///
/// A backend that forwads to the `log` crate is available with the `log` feature.
///
/// Note that any closure of type `Fn(Level, Vec<JsValue>)` implements this trait.
///
/// A very simple logger that just prints to stderr could look like this:
///
/// ```rust
/// use quick_js::{Context, JsValue, console::Level};
///
/// Context::builder()
/// .console(|level: Level, args: Vec<JsValue>| {
/// eprintln!("{}: {:?}", level, args);
/// })
/// .build()
/// # .unwrap();
/// ```
///
pub trait ConsoleBackend: std::panic::RefUnwindSafe + 'static {
/// Handle a log message.
fn log(&self, level: Level, values: Vec<JsValue>);
}
impl<F> ConsoleBackend for F
where
F: Fn(Level, Vec<JsValue>) + std::panic::RefUnwindSafe + 'static,
{
fn log(&self, level: Level, values: Vec<JsValue>) {
(self)(level, values);
}
}
#[cfg(feature = "log")]
mod log {
use super::{JsValue, Level};
/// A console implementation that logs messages via the `log` crate.
///
/// Only available with the `log` feature.
pub struct LogConsole;
fn print_value(value: JsValue) -> String {
match value {
JsValue::Null => "null".to_string(),
JsValue::Bool(v) => v.to_string(),
JsValue::Int(v) => v.to_string(),
JsValue::Float(v) => v.to_string(),
JsValue::String(v) => v,
JsValue::Array(values) => {
let parts = values
.into_iter()
.map(print_value)
.collect::<Vec<_>>()
.join(", ");
format!("[{}]", parts)
}
JsValue::Object(map) => {
let parts = map
.into_iter()
.map(|(key, value)| format!("{}: {}", key, print_value(value)))
.collect::<Vec<_>>()
.join(", ");
format!("{{{}}}", parts)
}
#[cfg(feature = "chrono")]
JsValue::Date(v) => v.to_string(),
#[cfg(feature = "bigint")]
JsValue::BigInt(v) => v.to_string(),
JsValue::__NonExhaustive => unreachable!(),
}
}
impl super::ConsoleBackend for LogConsole {
fn log(&self, level: Level, values: Vec<JsValue>) {
if values.is_empty() {
return;
}
let log_level = match level {
Level::Trace => log::Level::Trace,
Level::Debug => log::Level::Debug,
Level::Log => log::Level::Info,
Level::Info => log::Level::Info,
Level::Warn => log::Level::Warn,
Level::Error => log::Level::Error,
};
let msg = values
.into_iter()
.map(print_value)
.collect::<Vec<_>>()
.join(" ");
log::log!(log_level, "{}", msg);
}
}
}
#[cfg(feature = "log")]
pub use self::log::LogConsole;

View File

@@ -0,0 +1,47 @@
/// A small wrapper that frees resources that have to be freed
/// automatically when they go out of scope.
pub struct DroppableValue<T, F>
where
F: FnMut(&mut T),
{
value: T,
drop_fn: F,
}
impl<T, F> DroppableValue<T, F>
where
F: FnMut(&mut T),
{
pub fn new(value: T, drop_fn: F) -> Self {
Self { value, drop_fn }
}
}
impl<T, F> Drop for DroppableValue<T, F>
where
F: FnMut(&mut T),
{
fn drop(&mut self) {
(self.drop_fn)(&mut self.value);
}
}
impl<T, F> std::ops::Deref for DroppableValue<T, F>
where
F: FnMut(&mut T),
{
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
impl<T, F> std::ops::DerefMut for DroppableValue<T, F>
where
F: FnMut(&mut T),
{
fn deref_mut(&mut self) -> &mut T {
&mut self.value
}
}

949
quickjs/external/quickjs-rs/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,949 @@
//! quick-js is a a Rust wrapper for [QuickJS](https://bellard.org/quickjs/), a new Javascript
//! engine by Fabrice Bellard.
//!
//! It enables easy and straight-forward execution of modern Javascript from Rust.
//!
//! ## Limitations
//!
//! * Windows is not supported yet
//!
//! ## Quickstart:
//!
//! ```rust
//! use quick_js::{Context, JsValue};
//!
//! let context = Context::new().unwrap();
//!
//! // Eval.
//!
//! let value = context.eval("1 + 2").unwrap();
//! assert_eq!(value, JsValue::Int(3));
//!
//! let value = context.eval_as::<String>(" var x = 100 + 250; x.toString() ").unwrap();
//! assert_eq!(&value, "350");
//!
//! // Callbacks.
//!
//! context.add_callback("myCallback", |a: i32, b: i32| a + b).unwrap();
//!
//! context.eval(r#"
//! // x will equal 30
//! var x = myCallback(10, 20);
//! "#).unwrap();
//! ```
#![deny(missing_docs)]
mod bindings;
mod callback;
pub mod console;
mod droppable_value;
mod value;
use std::{convert::TryFrom, error, fmt};
pub use callback::{Arguments, Callback};
pub use value::*;
/// Error on Javascript execution.
#[derive(PartialEq, Debug)]
pub enum ExecutionError {
/// Code to be executed contained zero-bytes.
InputWithZeroBytes,
/// Value conversion failed. (either input arguments or result value).
Conversion(ValueError),
/// Internal error.
Internal(String),
/// JS Exception was thrown.
Exception(JsValue),
/// JS Runtime exceeded the memory limit.
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)
}
}
/// Error on context creation.
#[derive(Debug)]
pub enum ContextError {
/// Runtime could not be created.
RuntimeCreationFailed,
/// Context could not be created.
ContextCreationFailed,
/// Execution error while building.
Execution(ExecutionError),
#[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"),
Execution(e) => e.fmt(f),
__NonExhaustive => unreachable!(),
}
}
}
impl error::Error for ContextError {}
/// A builder for [Context](Context).
///
/// Create with [Context::builder](Context::builder).
pub struct ContextBuilder {
memory_limit: Option<usize>,
console_backend: Option<Box<dyn console::ConsoleBackend>>,
}
impl ContextBuilder {
fn new() -> Self {
Self {
memory_limit: None,
console_backend: None,
}
}
/// Sets the memory limit of the Javascript runtime (in bytes).
///
/// If the limit is exceeded, methods like `eval` will return
/// a `Err(ExecutionError::Exception(JsValue::Null))`
// TODO: investigate why we don't get a proper exception message here.
pub fn memory_limit(self, max_bytes: usize) -> Self {
let mut s = self;
s.memory_limit = Some(max_bytes);
s
}
/// Set a console handler that will proxy `console.{log,trace,debug,...}`
/// calls.
///
/// The given argument must implement the [ConsoleBackend] trait.
///
/// A very simple logger could look like this:
pub fn console<B>(mut self, backend: B) -> Self
where
B: console::ConsoleBackend,
{
self.console_backend = Some(Box::new(backend));
self
}
/// Finalize the builder and build a JS Context.
pub fn build(self) -> Result<Context, ContextError> {
let wrapper = bindings::ContextWrapper::new(self.memory_limit)?;
if let Some(be) = self.console_backend {
wrapper.set_console(be).map_err(ContextError::Execution)?;
}
Ok(Context::from_wrapper(wrapper))
}
}
/// Context is a wrapper around a QuickJS Javascript context.
/// It is the primary way to interact with the runtime.
///
/// For each `Context` instance a new instance of QuickJS
/// runtime is created. It means that it is safe to use
/// different contexts in different threads, but each
/// `Context` instance must be used only from a single thread.
pub struct Context {
wrapper: bindings::ContextWrapper,
}
impl Context {
fn from_wrapper(wrapper: bindings::ContextWrapper) -> Self {
Self { wrapper }
}
/// Create a `ContextBuilder` that allows customization of JS Runtime settings.
///
/// For details, see the methods on `ContextBuilder`.
///
/// ```rust
/// let _context = quick_js::Context::builder()
/// .memory_limit(100_000)
/// .build()
/// .unwrap();
/// ```
pub fn builder() -> ContextBuilder {
ContextBuilder::new()
}
/// Create a new Javascript context with default settings.
pub fn new() -> Result<Self, ContextError> {
let wrapper = bindings::ContextWrapper::new(None)?;
Ok(Self::from_wrapper(wrapper))
}
/// Reset the Javascript engine.
///
/// All state and callbacks will be removed.
pub fn reset(self) -> Result<Self, ContextError> {
let wrapper = self.wrapper.reset()?;
Ok(Self { wrapper })
}
/// Evaluates Javascript code and returns the value of the final expression.
///
/// **Promises**:
/// If the evaluated code returns a Promise, the event loop
/// will be executed until the promise is finished. The final value of
/// the promise will be returned, or a `ExecutionError::Exception` if the
/// promise failed.
///
/// ```rust
/// use quick_js::{Context, JsValue};
/// let context = Context::new().unwrap();
///
/// let value = context.eval(" 1 + 2 + 3 ");
/// assert_eq!(
/// value,
/// Ok(JsValue::Int(6)),
/// );
///
/// let value = context.eval(r#"
/// function f() { return 55 * 3; }
/// let y = f();
/// var x = y.toString() + "!"
/// x
/// "#);
/// assert_eq!(
/// value,
/// Ok(JsValue::String("165!".to_string())),
/// );
/// ```
pub fn eval(&self, code: &str) -> Result<JsValue, ExecutionError> {
let value_raw = self.wrapper.eval(code)?;
let value = value_raw.to_value()?;
Ok(value)
}
/// Evaluates Javascript code and returns the value of the final expression
/// as a Rust type.
///
/// **Promises**:
/// If the evaluated code returns a Promise, the event loop
/// will be executed until the promise is finished. The final value of
/// the promise will be returned, or a `ExecutionError::Exception` if the
/// promise failed.
///
/// ```rust
/// use quick_js::{Context};
/// let context = Context::new().unwrap();
///
/// let res = context.eval_as::<bool>(" 100 > 10 ");
/// assert_eq!(
/// res,
/// Ok(true),
/// );
///
/// let value: i32 = context.eval_as(" 10 + 10 ").unwrap();
/// assert_eq!(
/// value,
/// 20,
/// );
/// ```
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)
}
/// Call a global function in the Javascript namespace.
///
/// **Promises**:
/// If the evaluated code returns a Promise, the event loop
/// will be executed until the promise is finished. The final value of
/// the promise will be returned, or a `ExecutionError::Exception` if the
/// promise failed.
///
/// ```rust
/// use quick_js::{Context, JsValue};
/// let context = Context::new().unwrap();
///
/// let res = context.call_function("encodeURIComponent", vec!["a=b"]);
/// assert_eq!(
/// res,
/// Ok(JsValue::String("a%3Db".to_string())),
/// );
/// ```
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)
}
/// Add a global JS function that is backed by a Rust function or closure.
///
/// The callback must satisfy several requirements:
/// * accepts 0 - 5 arguments
/// * each argument must be convertible from a JsValue
/// * must return a value
/// * the return value must either:
/// - be convertible to JsValue
/// - be a Result<T, E> where T is convertible to JsValue
/// if Err(e) is returned, a Javascript exception will be raised
///
/// ```rust
/// use quick_js::{Context, JsValue};
/// let context = Context::new().unwrap();
///
/// // Register a closue as a callback under the "add" name.
/// // The 'add' function can now be called from Javascript code.
/// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap();
///
/// // Now we try out the 'add' function via eval.
/// let output = context.eval_as::<i32>(" add( 3 , 4 ) ").unwrap();
/// assert_eq!(
/// output,
/// 7,
/// );
/// ```
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_global_properties() {
// let c = Context::new().unwrap();
// assert_eq!(
// c.global_property("lala"),
// Err(ExecutionError::Exception(
// "Global object does not have property 'lala'".into()
// ))
// );
// c.set_global_property("testprop", true).unwrap();
// assert_eq!(
// c.global_property("testprop").unwrap(),
// JsValue::Bool(true),
// );
// }
#[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");
#[cfg(feature = "bigint")]
assert_eq!(
c.eval_as::<num_bigint::BigInt>("1n << 100n").unwrap(),
num_bigint::BigInt::from(1i128 << 100)
);
#[cfg(feature = "bigint")]
assert_eq!(c.eval_as::<i64>("1 << 30").unwrap(), 1i64 << 30);
#[cfg(feature = "bigint")]
assert_eq!(c.eval_as::<u128>("1n << 100n").unwrap(), 1u128 << 100);
}
#[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 ),* ),
)*
] => {
$(
{
// Test plain return type.
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));
// Test Result<T, E> return type with OK(_) returns.
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));
// Test Result<T, E> return type with Err(_) returns.
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_varargs() {
let c = Context::new().unwrap();
// No return.
c.add_callback("cb", |args: Arguments| {
let args = args.into_vec();
assert_eq!(
args,
vec![
JsValue::String("hello".into()),
JsValue::Bool(true),
JsValue::from(100),
]
);
})
.unwrap();
c.eval(" cb('hello', true, 100) ").unwrap();
// With return.
c.add_callback("cb2", |args: Arguments| -> u32 {
let args = args.into_vec();
assert_eq!(
args,
vec![JsValue::from(1), JsValue::from(10), JsValue::from(100),]
);
111
})
.unwrap();
c.eval(
r#"
var x = cb2(1, 10, 100);
if (x !== 111) {
throw new Error('Expected 111, got ' + x);
}
"#,
)
.unwrap();
}
#[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();
// Check it still works.
assert_eq!(
c2.eval_as::<String>(" 'abc'.repeat(2) ").unwrap(),
"abcabc".to_string(),
);
// Check old state is gone.
let err_msg = c2.eval(" x ").unwrap_err().to_string();
assert!(err_msg.contains("ReferenceError"));
// Check callback is gone.
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());
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_deserialize_i64() {
for i in vec![0, std::i64::MAX, std::i64::MIN] {
let c = Context::new().unwrap();
let value = c.eval(&format!("{}n", i)).unwrap();
assert_eq!(value, JsValue::BigInt(i.into()));
}
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_deserialize_bigint() {
for i in vec![
std::i64::MAX as i128 + 1,
std::i64::MIN as i128 - 1,
std::i128::MAX,
std::i128::MIN,
] {
let c = Context::new().unwrap();
let value = c.eval(&format!("{}n", i)).unwrap();
let expected = num_bigint::BigInt::from(i);
assert_eq!(value, JsValue::BigInt(expected.into()));
}
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_serialize_i64() {
for i in vec![0, std::i64::MAX, std::i64::MIN] {
let c = Context::new().unwrap();
c.eval(&format!(" function isEqual(x) {{ return x === {}n }} ", i))
.unwrap();
assert_eq!(
c.call_function("isEqual", vec![JsValue::BigInt(i.into())])
.unwrap(),
JsValue::Bool(true)
);
}
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_serialize_bigint() {
for i in vec![
std::i64::MAX as i128 + 1,
std::i64::MIN as i128 - 1,
std::i128::MAX,
std::i128::MIN,
] {
let c = Context::new().unwrap();
c.eval(&format!(" function isEqual(x) {{ return x === {}n }} ", i))
.unwrap();
let value = JsValue::BigInt(num_bigint::BigInt::from(i).into());
assert_eq!(
c.call_function("isEqual", vec![value]).unwrap(),
JsValue::Bool(true)
);
}
}
#[test]
fn test_console() {
use console::Level;
use std::sync::{Arc, Mutex};
let messages = Arc::new(Mutex::new(Vec::<(Level, Vec<JsValue>)>::new()));
let m = messages.clone();
let c = Context::builder()
.console(move |level: Level, args: Vec<JsValue>| {
m.lock().unwrap().push((level, args));
})
.build()
.unwrap();
c.eval(
r#"
console.log("hi");
console.error(false);
"#,
)
.unwrap();
let m = messages.lock().unwrap();
assert_eq!(
*m,
vec![
(Level::Log, vec![JsValue::from("hi")]),
(Level::Error, vec![JsValue::from(false)]),
]
);
}
}

View File

@@ -0,0 +1,107 @@
use num_traits::cast::ToPrimitive;
#[derive(Clone, Debug)]
pub enum BigIntOrI64 {
Int(i64),
BigInt(num_bigint::BigInt),
}
impl PartialEq for BigIntOrI64 {
fn eq(&self, other: &Self) -> bool {
use BigIntOrI64::*;
match (&self, &other) {
(Int(i), Int(j)) => i == j,
(Int(i), BigInt(b)) | (BigInt(b), Int(i)) => b == &num_bigint::BigInt::from(*i),
(BigInt(a), BigInt(b)) => a == b,
}
}
}
impl Eq for BigIntOrI64 {}
/// A value holding JavaScript
/// [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) type
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigInt {
pub(crate) inner: BigIntOrI64,
}
impl BigInt {
/// Return `Some` if value fits into `i64` and `None` otherwise
pub fn as_i64(&self) -> Option<i64> {
match &self.inner {
BigIntOrI64::Int(int) => Some(*int),
BigIntOrI64::BigInt(bigint) => bigint.to_i64(),
}
}
/// Convert value into `num_bigint::BigInt`
pub fn into_bigint(self) -> num_bigint::BigInt {
match self.inner {
BigIntOrI64::Int(int) => int.into(),
BigIntOrI64::BigInt(bigint) => bigint,
}
}
}
impl std::fmt::Display for BigInt {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.inner {
BigIntOrI64::Int(i) => write!(f, "{}", i),
BigIntOrI64::BigInt(ref i) => write!(f, "{}", i),
}
}
}
impl From<i64> for BigInt {
fn from(int: i64) -> Self {
BigInt {
inner: BigIntOrI64::Int(int),
}
}
}
impl From<num_bigint::BigInt> for BigInt {
fn from(bigint: num_bigint::BigInt) -> Self {
BigInt {
inner: BigIntOrI64::BigInt(bigint),
}
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_bigint_as_i64() {
let value = BigInt {
inner: BigIntOrI64::Int(1234i64),
};
assert_eq!(value.as_i64(), Some(1234i64));
}
#[test]
fn test_bigint_as_i64_overflow() {
let value = BigInt {
inner: BigIntOrI64::BigInt(num_bigint::BigInt::from(std::i128::MAX)),
};
assert_eq!(value.as_i64(), None);
}
#[test]
fn test_bigint_into_bigint() {
for i in vec![
0 as i128,
std::i64::MAX as i128,
std::i64::MIN as i128,
std::i128::MAX,
std::i128::MIN,
] {
let value = BigInt {
inner: BigIntOrI64::BigInt(num_bigint::BigInt::from(i)),
};
assert_eq!(value.into_bigint(), num_bigint::BigInt::from(i));
}
}
}

View File

@@ -0,0 +1,320 @@
#[cfg(feature = "bigint")]
pub(crate) mod bigint;
use std::convert::{TryFrom, TryInto};
use std::{collections::HashMap, error, fmt};
#[cfg(feature = "bigint")]
pub use bigint::BigInt;
/// A value that can be (de)serialized to/from the quickjs runtime.
#[derive(PartialEq, Clone, Debug)]
#[allow(missing_docs)]
pub enum JsValue {
Null,
Bool(bool),
Int(i32),
Float(f64),
String(String),
Array(Vec<JsValue>),
Object(HashMap<String, JsValue>),
/// chrono::Datetime<Utc> / JS Date integration.
/// Only available with the optional `chrono` feature.
#[cfg(feature = "chrono")]
Date(chrono::DateTime<chrono::Utc>),
/// num_bigint::BigInt / JS BigInt integration
/// Only available with the optional `bigint` feature
#[cfg(feature = "bigint")]
BigInt(crate::BigInt),
#[doc(hidden)]
__NonExhaustive,
}
impl JsValue {
/// Cast value to a str.
///
/// Returns `Some(&str)` if value is a `JsValue::String`, None otherwise.
pub fn as_str(&self) -> Option<&str> {
match self {
JsValue::String(ref s) => Some(s.as_str()),
_ => None,
}
}
/// Convert to `String`.
pub fn into_string(self) -> Option<String> {
match self {
JsValue::String(s) => Some(s),
_ => None,
}
}
}
macro_rules! value_impl_from {
(
(
$( $t1:ty => $var1:ident, )*
)
(
$( $t2:ty => |$exprname:ident| $expr:expr => $var2:ident, )*
)
) => {
$(
impl From<$t1> for JsValue {
fn from(value: $t1) -> Self {
JsValue::$var1(value)
}
}
impl std::convert::TryFrom<JsValue> for $t1 {
type Error = ValueError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
match value {
JsValue::$var1(inner) => Ok(inner),
_ => Err(ValueError::UnexpectedType)
}
}
}
)*
$(
impl From<$t2> for JsValue {
fn from(value: $t2) -> Self {
let $exprname = value;
let inner = $expr;
JsValue::$var2(inner)
}
}
)*
}
}
value_impl_from! {
(
bool => Bool,
i32 => Int,
f64 => Float,
String => String,
)
(
i8 => |x| i32::from(x) => Int,
i16 => |x| i32::from(x) => Int,
u8 => |x| i32::from(x) => Int,
u16 => |x| i32::from(x) => Int,
u32 => |x| f64::from(x) => Float,
)
}
#[cfg(feature = "bigint")]
value_impl_from! {
()
(
i64 => |x| x.into() => BigInt,
u64 => |x| num_bigint::BigInt::from(x).into() => BigInt,
i128 => |x| num_bigint::BigInt::from(x).into() => BigInt,
u128 => |x| num_bigint::BigInt::from(x).into() => BigInt,
num_bigint::BigInt => |x| x.into() => BigInt,
)
}
#[cfg(feature = "bigint")]
impl std::convert::TryFrom<JsValue> for i64 {
type Error = ValueError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
match value {
JsValue::Int(int) => Ok(int as i64),
JsValue::BigInt(bigint) => bigint.as_i64().ok_or(ValueError::UnexpectedType),
_ => Err(ValueError::UnexpectedType),
}
}
}
#[cfg(feature = "bigint")]
macro_rules! value_bigint_impl_tryfrom {
(
($($t:ty => $to_type:ident, )*)
) => {
$(
impl std::convert::TryFrom<JsValue> for $t {
type Error = ValueError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
use num_traits::ToPrimitive;
match value {
JsValue::Int(int) => Ok(int as $t),
JsValue::BigInt(bigint) => bigint
.into_bigint()
.$to_type()
.ok_or(ValueError::UnexpectedType),
_ => Err(ValueError::UnexpectedType),
}
}
}
)*
}
}
#[cfg(feature = "bigint")]
value_bigint_impl_tryfrom! {
(
u64 => to_u64,
i128 => to_i128,
u128 => to_u128,
)
}
#[cfg(feature = "bigint")]
impl std::convert::TryFrom<JsValue> for num_bigint::BigInt {
type Error = ValueError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
match value {
JsValue::Int(int) => Ok(num_bigint::BigInt::from(int)),
JsValue::BigInt(bigint) => Ok(bigint.into_bigint()),
_ => Err(ValueError::UnexpectedType),
}
}
}
impl<T> From<Vec<T>> for JsValue
where
T: Into<JsValue>,
{
fn from(values: Vec<T>) -> Self {
let items = values.into_iter().map(|x| x.into()).collect();
JsValue::Array(items)
}
}
impl<'a> From<&'a str> for JsValue {
fn from(val: &'a str) -> Self {
JsValue::String(val.into())
}
}
impl<T> From<Option<T>> for JsValue
where
T: Into<JsValue>,
{
fn from(opt: Option<T>) -> Self {
if let Some(value) = opt {
value.into()
} else {
JsValue::Null
}
}
}
impl<K, V> From<HashMap<K, V>> for JsValue
where
K: Into<String>,
V: Into<JsValue>,
{
fn from(map: HashMap<K, V>) -> Self {
let new_map = map.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
JsValue::Object(new_map)
}
}
impl<V> TryFrom<JsValue> for HashMap<String, V>
where
V: TryFrom<JsValue>,
{
type Error = ValueError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
match value {
JsValue::Object(object) => object
.into_iter()
.map(|(k, v)| match v.try_into() {
Ok(v) => Ok((k, v)),
Err(_) => Err(ValueError::UnexpectedType),
})
.collect(),
_ => Err(ValueError::UnexpectedType),
}
}
}
/// Error during value conversion.
#[derive(PartialEq, Eq, Debug)]
pub enum ValueError {
/// Invalid non-utf8 string.
InvalidString(std::str::Utf8Error),
/// Encountered string with \0 bytes.
StringWithZeroBytes(std::ffi::NulError),
/// Internal error.
Internal(String),
/// Received an unexpected type that could not be converted.
UnexpectedType,
#[doc(hidden)]
__NonExhaustive,
}
// TODO: remove this once either the Never type get's stabilized or the compiler
// can properly handle Infallible.
impl From<std::convert::Infallible> for ValueError {
fn from(_: std::convert::Infallible) -> Self {
unreachable!()
}
}
impl fmt::Display for ValueError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ValueError::*;
match self {
InvalidString(e) => write!(
f,
"Value conversion failed - invalid non-utf8 string: {}",
e
),
StringWithZeroBytes(_) => write!(f, "String contains \\0 bytes",),
Internal(e) => write!(f, "Value conversion failed - internal error: {}", e),
UnexpectedType => write!(f, "Could not convert - received unexpected type"),
__NonExhaustive => unreachable!(),
}
}
}
impl error::Error for ValueError {}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_from_i64() {
let int = 1234i64;
let value = JsValue::from(int);
if let JsValue::BigInt(value) = value {
assert_eq!(value.as_i64(), Some(int));
} else {
panic!("Expected JsValue::BigInt");
}
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_from_bigint() {
let bigint = num_bigint::BigInt::from(std::i128::MAX);
let value = JsValue::from(bigint.clone());
if let JsValue::BigInt(value) = value {
assert_eq!(value.into_bigint(), bigint);
} else {
panic!("Expected JsValue::BigInt");
}
}
#[cfg(feature = "bigint")]
#[test]
fn test_bigint_i64_bigint_eq() {
let value_i64 = JsValue::BigInt(1234i64.into());
let value_bigint = JsValue::BigInt(num_bigint::BigInt::from(1234i64).into());
assert_eq!(value_i64, value_bigint);
}
}