210 changes: 131 additions & 79 deletions py_mini_racer/extension/mini_racer_extension.cc
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ typedef struct {
unsigned long timeout;
EvalResult* result;
size_t max_memory;
bool basic_only;
} EvalParams;

enum IsolateData {
Expand Down Expand Up @@ -274,30 +275,30 @@ static void* nogvl_context_eval(void* arg) {
if (trycatch.HasCaught()) {
if (!trycatch.Exception()->IsNull()) {
result->message = new Persistent<Value>();
Local<Message> message = trycatch.Message();
char buf[1000];
int len, line, column;

if (!message->GetLineNumber(context).To(&line)) {
line = 0;
}

if (!message->GetStartColumn(context).To(&column)) {
column = 0;
}

len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()),
*String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()),
line,
column);

if ((size_t) len >= sizeof(buf)) {
len = sizeof(buf) - 1;
buf[len] = '\0';
}

Local<String> v8_message = String::NewFromUtf8(isolate, buf, NewStringType::kNormal, len).ToLocalChecked();
result->message->Reset(isolate, v8_message);
Local<Message> message = trycatch.Message();
char buf[1000];
int len, line, column;

if (!message->GetLineNumber(context).To(&line)) {
line = 0;
}

if (!message->GetStartColumn(context).To(&column)) {
column = 0;
}

len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()),
*String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()),
line,
column);

if ((size_t) len >= sizeof(buf)) {
len = sizeof(buf) - 1;
buf[len] = '\0';
}

Local<String> v8_message = String::NewFromUtf8(isolate, buf, NewStringType::kNormal, len).ToLocalChecked();
result->message->Reset(isolate, v8_message);
} else if(trycatch.HasTerminated()) {
result->terminated = true;
result->message = new Persistent<Value>();
Expand All @@ -311,14 +312,25 @@ static void* nogvl_context_eval(void* arg) {
}

if (!trycatch.StackTrace(context).IsEmpty()) {
result->backtrace = new Persistent<Value>();
result->backtrace->Reset(isolate, trycatch.StackTrace(context).ToLocalChecked()->ToString(context).ToLocalChecked());
Local<Value> stacktrace;

if (trycatch.StackTrace(context).ToLocal(&stacktrace)) {
Local<Value> tmp;

if (stacktrace->ToString(context).ToLocal(&tmp)) {
result->backtrace = new Persistent<Value>();
result->backtrace->Reset(isolate, tmp);
}
}
}
}
} else {
Persistent<Value>* persistent = new Persistent<Value>();
persistent->Reset(isolate, maybe_value.ToLocalChecked());
result->value = persistent;
Local<Value> tmp;

if (maybe_value.ToLocal(&tmp)) {
result->value = new Persistent<Value>();
result->value->Reset(isolate, tmp);
}
}
}

Expand Down Expand Up @@ -386,7 +398,7 @@ static BinaryValue *heap_stats(ContextInfo *context_info) {
content[idx++ * 2 + 1] = new_bv_int(0);
content[idx++ * 2 + 1] = new_bv_int(0);
} else {
isolate->GetHeapStatistics(&stats);
isolate->GetHeapStatistics(&stats);

content[idx++ * 2 + 1] = new_bv_int(stats.total_physical_size());
content[idx++ * 2 + 1] = new_bv_int(stats.total_heap_size_executable());
Expand All @@ -413,73 +425,100 @@ static BinaryValue *heap_stats(ContextInfo *context_info) {
}


static BinaryValue *convert_v8_to_binary(Isolate * isolate,
Local<Context> context,
Local<Value> value)
static BinaryValue *convert_basic_v8_to_binary(Isolate * isolate,
Local<Context> context,
Local<Value> value)
{
Isolate::Scope isolate_scope(isolate);
Isolate::Scope isolate_scope(isolate);
HandleScope scope(isolate);

BinaryValue *res = new (xalloc(res)) BinaryValue();

if (value->IsNull() || value->IsUndefined()) {
res->type = type_null;
}

else if (value->IsInt32()) {
res->type = type_integer;
auto val = value->Uint32Value(context).ToChecked();
res->int_val = val;
}

// ECMA-262, 4.3.20
// http://www.ecma-international.org/ecma-262/5.1/#sec-4.3.19
else if (value->IsNumber()) {
res->type = type_double;
double val = value->NumberValue(context).ToChecked();
res->double_val = val;
}

else if (value->IsBoolean()) {
res->type = type_bool;
res->int_val = (value->IsTrue() ? 1 : 0);
}
else if (value->IsFunction()){
res->type = type_function;
}
else if (value->IsSymbol()){
res->type = type_symbol;
}
else if (value->IsDate()) {
res->type = type_date;
Local<Date> date = Local<Date>::Cast(value);

double timestamp = date->ValueOf();
res->double_val = timestamp;
}
else if (value->IsString()) {
Local<String> rstr = value->ToString(context).ToLocalChecked();

res->type = type_str_utf8;
res->len = size_t(rstr->Utf8Length(isolate)); // in bytes
size_t capacity = res->len + 1;
res->str_val = xalloc(res->str_val, capacity);
rstr->WriteUtf8(isolate, res->str_val);
}
else {
BinaryValueFree(res);
res = nullptr;
}
return res;
}


static BinaryValue *convert_v8_to_binary(Isolate * isolate,
Local<Context> context,
Local<Value> value)
{
Isolate::Scope isolate_scope(isolate);
HandleScope scope(isolate);
BinaryValue *res;

res = convert_basic_v8_to_binary(isolate, context, value);
if (res) {
return res;
}

res = new (xalloc(res)) BinaryValue();

else if (value->IsArray()) {
if (value->IsArray()) {
Local<Array> arr = Local<Array>::Cast(value);
size_t len = arr->Length();
uint32_t len = arr->Length();

BinaryValue **ary = xalloc(ary, sizeof(*ary) * len);

res->type = type_array;
res->array_val = ary;
res->len = (size_t) len;

for(uint32_t i = 0; i < arr->Length(); i++) {
for(uint32_t i = 0; i < len; i++) {
Local<Value> element = arr->Get(context, i).ToLocalChecked();
BinaryValue *bin_value = convert_v8_to_binary(isolate, context, element);
if (bin_value == NULL) {
// adjust final array length
res->len = (size_t) i;
goto err;
}
ary[i] = bin_value;
res->len++;
}
}

else if (value->IsFunction()){
res->type = type_function;
}

else if (value->IsSymbol()){
res->type = type_symbol;
}

else if (value->IsDate()) {
res->type = type_date;
Local<Date> date = Local<Date>::Cast(value);

double timestamp = date->ValueOf();
res->double_val = timestamp;
}

else if (value->IsObject()) {
res->type = type_hash;

Expand All @@ -500,9 +539,9 @@ static BinaryValue *convert_v8_to_binary(Isolate * isolate,

MaybeLocal<Value> maybe_pkey = props->Get(context, i);
if (maybe_pkey.IsEmpty()) {
goto err;
}
Local<Value> pkey = maybe_pkey.ToLocalChecked();
goto err;
}
Local<Value> pkey = maybe_pkey.ToLocalChecked();
MaybeLocal<Value> maybe_pvalue = object->Get(context, pkey);
// this may have failed due to Get raising
if (maybe_pvalue.IsEmpty() || trycatch.HasCaught()) {
Expand All @@ -525,16 +564,8 @@ static BinaryValue *convert_v8_to_binary(Isolate * isolate,
res->len++;
}
} // else empty hash
}

else {
Local<String> rstr = value->ToString(context).ToLocalChecked();

res->type = type_str_utf8;
res->len = size_t(rstr->Utf8Length(isolate)); // in bytes
size_t capacity = res->len + 1;
res->str_val = xalloc(res->str_val, capacity);
rstr->WriteUtf8(isolate, res->str_val);
} else {
goto err;
}
return res;

Expand All @@ -544,8 +575,18 @@ static BinaryValue *convert_v8_to_binary(Isolate * isolate,
}


static BinaryValue *convert_basic_v8_to_binary(Isolate * isolate,
const Persistent<Context> & context,
Local<Value> value)
{
HandleScope scope(isolate);
return convert_basic_v8_to_binary(isolate,
Local<Context>::New(isolate, context),
value);
}

static BinaryValue *convert_v8_to_binary(Isolate * isolate,
const Persistent<Context> & context,
const Persistent<Context> & context,
Local<Value> value)
{
HandleScope scope(isolate);
Expand Down Expand Up @@ -607,7 +648,7 @@ ContextInfo *MiniRacer_init_context()
static BinaryValue* MiniRacer_eval_context_unsafe(
ContextInfo *context_info,
char *utf_str, int str_len,
unsigned long timeout, size_t max_memory)
unsigned long timeout, size_t max_memory, bool basic_only)
{
EvalParams eval_params;
EvalResult eval_result{};
Expand Down Expand Up @@ -640,6 +681,7 @@ static BinaryValue* MiniRacer_eval_context_unsafe(
eval_params.result = &eval_result;
eval_params.timeout = 0;
eval_params.max_memory = 0;
eval_params.basic_only = basic_only;
if (timeout > 0) {
eval_params.timeout = timeout;
}
Expand All @@ -653,14 +695,18 @@ static BinaryValue* MiniRacer_eval_context_unsafe(
Local<Value> tmp = Local<Value>::New(context_info->isolate,
*eval_result.message);

bmessage = convert_v8_to_binary(context_info->isolate, *context_info->context, tmp);
if (eval_params.basic_only) {
bmessage = convert_basic_v8_to_binary(context_info->isolate, *context_info->context, tmp);
} else {
bmessage = convert_v8_to_binary(context_info->isolate, *context_info->context, tmp);
}
}

if (eval_result.backtrace) {

Local<Value> tmp = Local<Value>::New(context_info->isolate,
*eval_result.backtrace);
bbacktrace = convert_v8_to_binary(context_info->isolate, *context_info->context, tmp);
bbacktrace = convert_basic_v8_to_binary(context_info->isolate, *context_info->context, tmp);
}
}

Expand Down Expand Up @@ -723,13 +769,17 @@ static BinaryValue* MiniRacer_eval_context_unsafe(
}
}

else {
else if (eval_result.value) {
Locker lock(context_info->isolate);
Isolate::Scope isolate_scope(context_info->isolate);
HandleScope handle_scope(context_info->isolate);

Local<Value> tmp = Local<Value>::New(context_info->isolate, *eval_result.value);
result = convert_v8_to_binary(context_info->isolate, *context_info->context, tmp);
if (eval_params.basic_only) {
result = convert_basic_v8_to_binary(context_info->isolate, *context_info->context, tmp);
} else {
result = convert_v8_to_binary(context_info->isolate, *context_info->context, tmp);
}
}

BinaryValueFree(bmessage);
Expand Down Expand Up @@ -764,8 +814,8 @@ class BufferOutputStream: public OutputStream {

extern "C" {

LIB_EXPORT BinaryValue * mr_eval_context(ContextInfo *context_info, char *str, int len, unsigned long timeout, size_t max_memory) {
BinaryValue *res = MiniRacer_eval_context_unsafe(context_info, str, len, timeout, max_memory);
LIB_EXPORT BinaryValue * mr_eval_context(ContextInfo *context_info, char *str, int len, unsigned long timeout, size_t max_memory, bool basic_only) {
BinaryValue *res = MiniRacer_eval_context_unsafe(context_info, str, len, timeout, max_memory, basic_only);
return res;
}

Expand Down Expand Up @@ -817,3 +867,5 @@ LIB_EXPORT BinaryValue * mr_heap_snapshot(ContextInfo *context_info) {
return bos.bv;
}
}

// vim: set shiftwidth=4 softtabstop=4 expandtab:
121 changes: 85 additions & 36 deletions py_mini_racer/py_mini_racer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
EXTENSION_NAME = fnmatch.filter(os.listdir(__location__), '_v8*.so')[0]
EXTENSION_PATH = os.path.join(__location__, EXTENSION_NAME)

if sys.version_info[0] < 3:
UNICODE_TYPE = unicode
else:
UNICODE_TYPE = str


class MiniRacerBaseException(Exception):
""" base MiniRacer exception class """
pass
Expand Down Expand Up @@ -59,6 +65,7 @@ class JSSymbol(object):
""" type for JS symbols """
pass


def is_unicode(value):
""" Check if a value is a valid unicode string, compatible with python 2 and python 3
Expand All @@ -73,14 +80,7 @@ def is_unicode(value):
>>> is_unicode(('abc',))
False
"""
python_version = sys.version_info[0]

if python_version == 2:
return isinstance(value, unicode)
elif python_version == 3:
return isinstance(value, str)
else:
raise NotImplementedError()
return isinstance(value, UNICODE_TYPE)


_ext_handle = None
Expand All @@ -101,7 +101,8 @@ def _fetch_ext_handle():
ctypes.c_char_p,
ctypes.c_int,
ctypes.c_ulong,
ctypes.c_size_t]
ctypes.c_size_t,
ctypes.c_bool]
_ext_handle.mr_eval_context.restype = ctypes.POINTER(PythonValue)

_ext_handle.mr_free_value.argtypes = [ctypes.c_void_p]
Expand Down Expand Up @@ -130,6 +131,8 @@ class MiniRacer(object):
https://docs.python.org/2/library/ctypes.html
"""

basic_types_only = False

def __init__(self):
""" Init a JS context """

Expand Down Expand Up @@ -171,12 +174,12 @@ def eval(self, js_str, timeout=0, max_memory=0):
bytes_val,
len(bytes_val),
ctypes.c_ulong(timeout),
ctypes.c_size_t(max_memory))
ctypes.c_size_t(max_memory),
ctypes.c_bool(self.basic_types_only))

if bool(res) is False:
raise JSConversionException()
python_value = res.contents.to_python()
return python_value
return self._eval_return(res)
finally:
self.lock.release()
if res is not None:
Expand Down Expand Up @@ -226,6 +229,43 @@ def __del__(self):

self.ext.mr_free_context(self.ctx)

@staticmethod
def _eval_return(res):
return res.contents.to_python()


class StrictMiniRacer(MiniRacer):
"""
A stricter version of MiniRacer accepting only basic types as a return value
(boolean, integer, strings, ...), array and mapping are disallowed.
"""

json_impl = json
basic_types_only = True

def execute(self, expr, **kwargs):
""" Stricter Execute with JSON serialization of returned value.
"""
wrapped_expr = "JSON.stringify((function(){return (%s)})())" % expr
ret = self.eval(wrapped_expr, **kwargs)
if is_unicode(ret):
return self.json_impl.loads(ret)

def call(self, identifier, *args, **kwargs):
""" Stricter Call with JSON serialization of returned value.
"""
json_args = self.json_impl.dumps(args, separators=(',', ':'),
cls=kwargs.pop("encoder", None))
js = "JSON.stringify({identifier}.apply(this, {json_args}))"
ret = self.eval(js.format(identifier=identifier, json_args=json_args),
**kwargs)
if is_unicode(ret):
return self.json_impl.loads(ret)

@staticmethod
def _eval_return(res):
return res.contents.basic_to_python()


class PythonTypes(object):
""" Python types identifier - need to be coherent with
Expand Down Expand Up @@ -263,9 +303,22 @@ def _double_value(self):
ptr = ctypes.c_char_p.from_buffer(self)
return ctypes.c_double.from_buffer(ptr).value

def to_python(self):
""" Return an object as native Python """
def _raise_from_error(self):
if self.type == PythonTypes.parse_exception:
msg = ctypes.c_char_p(self.value).value
raise JSParseException(msg)
elif self.type == PythonTypes.execute_exception:
msg = ctypes.c_char_p(self.value).value
raise JSEvalException(msg.decode('utf-8', errors='replace'))
elif self.type == PythonTypes.oom_exception:
msg = ctypes.c_char_p(self.value).value
raise JSOOMException(msg)
elif self.type == PythonTypes.timeout_exception:
msg = ctypes.c_char_p(self.value).value
raise JSTimeoutException(msg)

def basic_to_python(self):
self._raise_from_error()
result = None
if self.type == PythonTypes.null:
result = None
Expand All @@ -282,7 +335,23 @@ def to_python(self):
buf = ctypes.c_char_p(self.value)
ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))
result = ptr[0:self.len].decode("utf8")
elif self.type == PythonTypes.array:
elif self.type == PythonTypes.function:
result = JSFunction()
elif self.type == PythonTypes.date:
timestamp = self._double_value()
# JS timestamp are milliseconds, in python we are in seconds
result = datetime.datetime.utcfromtimestamp(timestamp / 1000.)
elif self.type == PythonTypes.symbol:
result = JSSymbol()
else:
raise JSConversionException()
return result

def to_python(self):
""" Return an object as native Python """
self._raise_from_error()
result = None
if self.type == PythonTypes.array:
if self.len == 0:
return []
ary = []
Expand All @@ -303,26 +372,6 @@ def to_python(self):
pval = PythonValue.from_address(ptr_to_hash[i*2+1])
res[pkey.to_python()] = pval.to_python()
result = res
elif self.type == PythonTypes.function:
result = JSFunction()
elif self.type == PythonTypes.parse_exception:
msg = ctypes.c_char_p(self.value).value
raise JSParseException(msg)
elif self.type == PythonTypes.execute_exception:
msg = ctypes.c_char_p(self.value).value
raise JSEvalException(msg.decode('utf-8', errors='replace'))
elif self.type == PythonTypes.oom_exception:
msg = ctypes.c_char_p(self.value).value
raise JSOOMException(msg)
elif self.type == PythonTypes.timeout_exception:
msg = ctypes.c_char_p(self.value).value
raise JSTimeoutException(msg)
elif self.type == PythonTypes.date:
timestamp = self._double_value()
# JS timestamp are milliseconds, in python we are in seconds
result = datetime.datetime.utcfromtimestamp(timestamp / 1000.)
elif self.type == PythonTypes.symbol:
result = JSSymbol()
else:
raise WrongReturnTypeException("unknown type %d" % self.type)
result = self.basic_to_python()
return result
80 changes: 80 additions & 0 deletions tests/test_array_growth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" Growing and reducing arrays """

import unittest
import json
import time

from datetime import datetime

from py_mini_racer import py_mini_racer


class Test(unittest.TestCase):
""" Test basic types """


def setUp(self):

self.mr = py_mini_racer.MiniRacer()

def test_growing_array(self):

js = """
var global_array = [
{
get first() {
for(var i=0; i<100; i++) {
global_array.push(0x41);
}
}
}
];
// when accessed, the first element will make the array grow by 100 items.
global_array;
"""

res = self.mr.eval(js)
# Initial array size was 100
self.assertEqual(res, [{'first': None}])


def test_shrinking_array(self):
js = """
var global_array = [
{
get first() {
for(var i=0; i<100; i++) {
global_array.pop();
}
}
}
];
// build a 200 elements array
for(var i=0; i < 200; i++)
global_array.push(0x41);
// when the first item will be accessed, it should remove 100 items.
global_array;
"""

# The final array should have:
# The initial item
# The next 100 items (value 0x41)
# The last 100 items which have been removed when the initial key was accessed
array = [{'first': None}] + \
[0x41] * 100 + \
[None] * 100

res = self.mr.eval(js)
self.assertEqual(res, array)


if __name__ == '__main__':
import sys
sys.exit(unittest.main())
38 changes: 38 additions & 0 deletions tests/test_strict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import unittest

from py_mini_racer import py_mini_racer


class StrictTestCase(unittest.TestCase):
"""Test StrictMiniRacer"""

def setUp(self):
self.mr = py_mini_racer.StrictMiniRacer()

def test_basic_int(self):
self.assertEqual(42, self.mr.execute("42"))

def test_basic_string(self):
self.assertEqual("42", self.mr.execute('"42"'))

def test_basic_hash(self):
self.assertEqual({}, self.mr.execute('{}'))

def test_basic_array(self):
self.assertEqual([1, 2, 3], self.mr.execute('[1, 2, 3]'))

def test_not_allowed_type(self):
with self.assertRaises(py_mini_racer.JSConversionException):
self.mr.eval("Object()")

def test_call(self):
js_func = """var f = function(args) {
return args.length;
}"""

self.assertIsNone(self.mr.eval(js_func))
self.assertEqual(self.mr.call('f', list(range(5))), 5)

def test_message(self):
with self.assertRaises(py_mini_racer.JSEvalException):
res = self.mr.eval("throw new EvalError('Hello', 'someFile.js', 10);")