#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#[allow(dead_code)]
#[allow(improper_ctypes)]
mod duktape {
#![allow(clippy::all)]
include!(concat!(env!("OUT_DIR"), "/duktape-bindings.rs"));
}
use std::ffi::{c_void, CStr, CString};
use std::{io, ptr, time};
#[no_mangle]
extern "C" fn jsaas_duk_exec_timeout_check(udata: *mut c_void) -> duktape::duk_bool_t {
let ctx = udata as *const Vec<EvaluateContext>;
let cont = unsafe {
if (*ctx).len() == 1 {
(*(ctx))[0].start.elapsed() <= (*(ctx))[0].limit
} else {
false
}
};
if cont {
0
} else {
1
}
}
extern "C" fn jsaas_btoa(ctx: *mut duktape::duk_context) -> duktape::duk_ret_t {
unsafe { duktape::duk_base64_encode(ctx, -1) };
1
}
extern "C" fn jsaas_atob(ctx: *mut duktape::duk_context) -> duktape::duk_ret_t {
let result = unsafe {
duktape::duk_require_string(ctx, -1);
duktape::duk_base64_decode(ctx, -1);
let mut buffer_size: duktape::duk_size_t = 0;
let buffer_ptr =
duktape::duk_get_buffer_data(ctx, -1, &mut buffer_size as *mut duktape::duk_size_t)
as *mut i8;
let name_string = CString::new("String").unwrap_or_default().into_raw();
let name_from_char_code = CString::new("fromCharCode").unwrap_or_default().into_raw();
duktape::duk_get_global_string(ctx, name_string);
duktape::duk_get_prop_string(ctx, -1, name_from_char_code);
drop(CString::from_raw(name_string));
drop(CString::from_raw(name_from_char_code));
duktape::duk_dup(ctx, -2);
for offset in 0..buffer_size {
let byte = *(buffer_ptr.offset(offset as isize) as *mut u8) as u32;
duktape::duk_push_uint(ctx, byte);
}
duktape::duk_pcall_method(ctx, buffer_size as i32)
};
match result {
0 => 1,
_ => 0,
}
}
const GLOBAL_FN_BTOA: *const u8 = b"btoa\0" as *const u8;
const GLOBAL_FN_ATOB: *const u8 = b"atob\0" as *const u8;
struct EvaluateContext {
limit: time::Duration,
start: time::Instant,
}
pub(crate) struct Context {
ctx: *mut duktape::duk_hthread,
latest_evaluate_context: *mut Vec<EvaluateContext>,
}
impl Context {
pub(crate) fn new() -> io::Result<Context> {
let latest_evaluate_context = Box::into_raw(Box::new(vec![]));
let ctx = unsafe {
duktape::duk_create_heap(
None,
None,
None,
latest_evaluate_context as *mut c_void,
None,
)
};
unsafe {
duktape::duk_push_global_object(ctx);
duktape::duk_push_c_function(ctx, Some(jsaas_btoa), 1);
duktape::duk_put_prop_string(ctx, -2, GLOBAL_FN_BTOA as *const std::os::raw::c_char);
duktape::duk_push_c_function(ctx, Some(jsaas_atob), 1);
duktape::duk_put_prop_string(ctx, -2, GLOBAL_FN_ATOB as *const std::os::raw::c_char);
duktape::duk_pop(ctx);
}
if ctx.is_null() {
Err(io::Error::new(
io::ErrorKind::Other,
"error initializing Duktape heap",
))
} else {
Ok(Context {
ctx,
latest_evaluate_context,
})
}
}
pub(crate) fn evaluate<S: AsRef<str>>(
&mut self,
code: S,
args: S,
limit: time::Duration,
) -> io::Result<String> {
if !args.as_ref().trim_start().starts_with('[') {
return Err(io::Error::new(
io::ErrorKind::Other,
"args must be a JSON-encoded array",
));
}
let code = CString::new(code.as_ref())?;
let code_ptr = code.as_ptr();
let args = CString::new(args.as_ref())?;
let args_ptr = args.as_ptr();
let function = CString::new("function")?;
let function_ptr = function.as_ptr();
let loader =
CString::new("function(fn, args) { return fn.apply(null, JSON.parse(args)); }")?;
let loader_ptr = loader.as_ptr();
self.duk_clear_stack();
unsafe {
(*self.latest_evaluate_context).clear();
(*self.latest_evaluate_context).push(EvaluateContext {
start: time::Instant::now(),
limit,
});
}
{
unsafe {
duktape::duk_push_string(self.ctx, loader_ptr);
duktape::duk_push_string(self.ctx, function_ptr);
}
self.duk_compile().map_err(|e| {
self.duk_clear_stack();
e
})?;
}
{
unsafe {
duktape::duk_push_string(self.ctx, code_ptr);
duktape::duk_push_string(self.ctx, function_ptr);
}
self.duk_compile().map_err(|e| {
self.duk_clear_stack();
e
})?;
}
let result = unsafe {
duktape::duk_push_string(self.ctx, args_ptr);
duktape::duk_pcall(self.ctx, 2) };
if result == 0 {
let json_ptr = unsafe { duktape::duk_json_encode(self.ctx, 0) };
if json_ptr.is_null() {
self.duk_clear_stack();
Err(io::Error::new(io::ErrorKind::Other, "undefined"))
} else {
let json_cstr = unsafe { CStr::from_ptr(json_ptr) };
let json_string: String = json_cstr
.to_str()
.map_err(|_e| io::Error::new(io::ErrorKind::Other, "undefined"))?
.to_string();
self.duk_clear_stack();
Ok(json_string)
}
} else {
let error_result = self
.duk_error_message()
.and_then(|e| Err(io::Error::new(io::ErrorKind::Other, e)));
self.duk_clear_stack();
error_result
}
}
fn duk_clear_stack(&mut self) {
unsafe {
duktape::duk_pop_n(self.ctx, duktape::duk_get_top(self.ctx));
}
}
fn duk_compile(&mut self) -> io::Result<()> {
let result = unsafe {
duktape::duk_compile_raw(
self.ctx,
ptr::null_mut(),
0,
2 | duktape::DUK_COMPILE_FUNCTION | duktape::DUK_COMPILE_SAFE,
)
};
if result != 0 {
self.duk_error_message()
.and_then(|e| Err(io::Error::new(io::ErrorKind::Other, e)))
} else {
Ok(())
}
}
fn duk_error_message(&mut self) -> io::Result<String> {
let error_cstr =
unsafe { CStr::from_ptr(duktape::duk_safe_to_lstring(self.ctx, -1, ptr::null_mut())) };
error_cstr
.to_str()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("error decoding Duktape error message: {}", e),
)
})
.map(|s| s.to_string())
}
}
impl Drop for Context {
fn drop(&mut self) {
self.duk_clear_stack();
unsafe {
duktape::duk_destroy_heap(self.ctx);
Box::from_raw(self.latest_evaluate_context);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn test_duktake_add_result_number() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"function(a, b) { return a + b; }",
"[2, 4]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, "6");
}
#[test]
fn test_duktake_add_mult_result_obj() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"function(a, b) { return { sum: a + b, product: a * b };}",
"[2, 4]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, "{\"sum\":6,\"product\":8}");
}
#[test]
fn test_duktape_bad_code() {
let mut ctx = Context::new().unwrap();
match ctx.evaluate(
"function()) { return 0; }}",
"[]",
time::Duration::from_millis(5000),
) {
Ok(_) => panic!("should have failed"),
Err(e) => assert_eq!(e.to_string(), "SyntaxError: parse error (line 1)"),
};
}
#[test]
fn test_duktape_bad_args() {
let mut ctx = Context::new().unwrap();
let r = ctx.evaluate(
"function(a, b) { return { sum: a + b, product: a * b };}",
"[{{",
time::Duration::from_millis(5000),
);
match r {
Ok(_) => panic!("should have failed"),
Err(e) => assert_eq!(e.to_string(), "SyntaxError: invalid json (at offset 3)"),
};
}
#[test]
fn test_duktape_wrong_args() {
let mut ctx = Context::new().unwrap();
let r = ctx.evaluate(
"function(a, b) { return { sum: a + b, product: a * b };}",
" {}",
time::Duration::from_millis(5000),
);
assert!(r.is_err());
}
#[test]
fn test_duktape_no_return() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate("function() {}", "[]", time::Duration::from_millis(5000))
.err()
.unwrap();
assert_eq!(r.description(), "undefined");
}
#[test]
fn test_duktape_return_undefined() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"function() { return undefined; }",
"[]",
time::Duration::from_millis(5000),
)
.err()
.unwrap();
assert_eq!(r.description(), "undefined");
}
#[test]
fn test_duktape_return_null() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"function() { return null; }",
"[]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, "null");
}
#[test]
fn test_duktape_return_func() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"function() { return function() {}; }",
"[]",
time::Duration::from_millis(5000),
)
.err()
.unwrap();
assert_eq!(r.description(), "undefined");
}
#[test]
fn test_duktape_usable_after_error() {
let mut ctx = Context::new().unwrap();
let r = ctx.evaluate(
"funktion()) { return 0; }}",
"[]",
time::Duration::from_millis(5000),
);
assert!(r.is_err());
let r = ctx
.evaluate(
"function(a, b) { return a + b; }",
"[2, 4]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, "6");
}
#[test]
fn test_duktake_while_true_recoverable() {
let mut ctx = Context::new().unwrap();
let r = ctx.evaluate(
"function() { while(true) {}",
"[]",
time::Duration::from_millis(100),
);
assert!(r.is_err());
let r = ctx
.evaluate(
"function(a, b) { return a + b; }",
"[2, 4]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, "6");
}
#[test]
fn test_duktape_btoa() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
"
function() {
return {
string: btoa('hello'),
number: btoa(1234),
undefined: btoa(),
undefinedExplicit: btoa(undefined),
true: btoa(true),
false: btoa(false),
null: btoa(null),
obj: btoa({ test: 42 })
};
}
",
"[]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(r, r#"{"string":"aGVsbG8=","number":"MTIzNA==","undefined":"dW5kZWZpbmVk","undefinedExplicit":"dW5kZWZpbmVk","true":"dHJ1ZQ==","false":"ZmFsc2U=","null":"bnVsbA==","obj":"W29iamVjdCBPYmplY3Rd"}"#);
}
#[test]
fn test_duktape_atob() {
let mut ctx = Context::new().unwrap();
let r = ctx
.evaluate(
r#"
function() {
return {
one: atob("aGVsbG8="),
two: [0, 1, 2, 3, 4, 5, 6, 7].map(function(i) {
var value = atob("AacABdxfoCQ=");
return value.charCodeAt(i);
}),
three: atob("")
};
}
"#,
"[]",
time::Duration::from_millis(5000),
)
.unwrap();
assert_eq!(
r,
r#"{"one":"hello","two":[1,167,0,5,220,95,160,36],"three":""}"#
);
let r = ctx.evaluate(
"function() { return atob(1234); }",
"[]",
time::Duration::from_millis(5000),
);
assert!(r.is_err());
let r = ctx.evaluate(
r#"function() { return atob("Z"); }"#,
"[]",
time::Duration::from_millis(5000),
);
assert!(r.is_err());
}
}