rusty_racer 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -0
- data/README.md +62 -0
- data/ext/rusty_racer/Cargo.toml +9 -3
- data/ext/rusty_racer/src/lib.rs +267 -77
- data/ext/rusty_racer/src/marshal.rs +356 -117
- data/ext/rusty_racer/src/ops.rs +244 -89
- data/ext/rusty_racer/src/stack.rs +383 -107
- data/lib/rusty_racer/version.rb +1 -1
- metadata +3 -2
|
@@ -8,6 +8,7 @@ use std::cell::RefCell;
|
|
|
8
8
|
use std::collections::HashMap;
|
|
9
9
|
use std::ffi::c_void;
|
|
10
10
|
|
|
11
|
+
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
11
12
|
use magnus::value::ReprValue;
|
|
12
13
|
use magnus::{
|
|
13
14
|
Error, ExceptionClass, IntoValue, RArray, RHash, RString, Ruby, TryConvert, Value, prelude::*,
|
|
@@ -172,6 +173,13 @@ fn copy_buffer_bytes(data: Option<std::ptr::NonNull<c_void>>, len: usize) -> Vec
|
|
|
172
173
|
buf
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// JS -> JsVal reads a value the way a script would, so it can run the page's
|
|
177
|
+
// JavaScript: an object's own enumerable string keys are listed and each is read
|
|
178
|
+
// with [[Get]] (a getter runs), a Proxy answers through its ownKeys /
|
|
179
|
+
// getOwnPropertyDescriptor / get traps, an array's holes read through its
|
|
180
|
+
// prototype chain (`get_index` is `arr[i]`), and a value with no structural form
|
|
181
|
+
// is its ToString (`stringify`). A throw there is the op's (`marshalled!`). Map and
|
|
182
|
+
// Set contents (`as_array`), typed arrays and buffers are read without any.
|
|
175
183
|
pub(crate) fn js_to_jsval(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>) -> JsVal {
|
|
176
184
|
let mut seen = JsSeen::default();
|
|
177
185
|
js_to_jsval_d(scope, value, &mut seen, 0)
|
|
@@ -392,18 +400,54 @@ fn unregister_container(seen: &mut JsSeen, obj: v8::Local<v8::Object>, id: u32)
|
|
|
392
400
|
// Owned-by-value (not &JsVal): a JsVal::Bytes hands its Vec straight to V8's
|
|
393
401
|
// backing store with no copy of the payload, so a large binary blob crosses
|
|
394
402
|
// Ruby->JS with zero extra allocation.
|
|
403
|
+
//
|
|
404
|
+
// Nothing here runs the page's JavaScript. An array's elements and an object's
|
|
405
|
+
// entries are DEFINED (CreateDataProperty), never assigned: an assignment is
|
|
406
|
+
// [[Set]], which walks the prototype chain, so an accessor a page put on
|
|
407
|
+
// `Array.prototype[0]` or `Object.prototype.x` would take the value — or swallow
|
|
408
|
+
// it — before it reached the new container. The rest makes nothing a page can
|
|
409
|
+
// reach either: a Map and a Set are filled by V8's own builtins (Map::Set calls
|
|
410
|
+
// the realm's original Map.prototype.set, not whatever the page left there), and
|
|
411
|
+
// a Uint8Array, a Date and a BigInt are made outright. A "__proto__" key is an
|
|
412
|
+
// own property like any other (as JSON.parse makes it), not the prototype.
|
|
395
413
|
pub(crate) fn jsval_to_js<'s>(
|
|
396
414
|
scope: &mut v8::PinScope<'s, '_>,
|
|
397
415
|
val: JsVal,
|
|
398
416
|
) -> v8::Local<'s, v8::Value> {
|
|
399
|
-
|
|
400
|
-
|
|
417
|
+
jsval_to_js_d(scope, val, &mut Built::default())
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The containers made so far in one jsval_to_js, by id, for a Ref to resolve to.
|
|
421
|
+
#[derive(Default)]
|
|
422
|
+
struct Built<'s> {
|
|
423
|
+
values: HashMap<u32, v8::Local<'s, v8::Value>>,
|
|
424
|
+
// Arrays whose elements are still being made — id -> length. An array is
|
|
425
|
+
// made from its finished elements in one step (Array::new_with_elements), so
|
|
426
|
+
// until then it has no object for a Ref to name; a Ref from inside it (a
|
|
427
|
+
// cycle) makes it early instead (`Built::get`).
|
|
428
|
+
open_arrays: HashMap<u32, usize>,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
impl<'s> Built<'s> {
|
|
432
|
+
fn get(
|
|
433
|
+
&mut self,
|
|
434
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
435
|
+
id: u32,
|
|
436
|
+
) -> Option<v8::Local<'s, v8::Value>> {
|
|
437
|
+
if let Some(value) = self.values.get(&id) {
|
|
438
|
+
return Some(*value);
|
|
439
|
+
}
|
|
440
|
+
let length = *self.open_arrays.get(&id)?;
|
|
441
|
+
let array: v8::Local<v8::Value> = v8::Array::new(scope, length as i32).into();
|
|
442
|
+
self.values.insert(id, array);
|
|
443
|
+
Some(array)
|
|
444
|
+
}
|
|
401
445
|
}
|
|
402
446
|
|
|
403
447
|
fn jsval_to_js_d<'s>(
|
|
404
448
|
scope: &mut v8::PinScope<'s, '_>,
|
|
405
449
|
val: JsVal,
|
|
406
|
-
built: &mut
|
|
450
|
+
built: &mut Built<'s>,
|
|
407
451
|
) -> v8::Local<'s, v8::Value> {
|
|
408
452
|
match val {
|
|
409
453
|
JsVal::Undefined => v8::undefined(scope).into(),
|
|
@@ -425,7 +469,7 @@ fn jsval_to_js_d<'s>(
|
|
|
425
469
|
.map(|a| a.into())
|
|
426
470
|
.unwrap_or_else(|| v8::undefined(scope).into());
|
|
427
471
|
if let Some(id) = id {
|
|
428
|
-
built.insert(id, arr);
|
|
472
|
+
built.values.insert(id, arr);
|
|
429
473
|
}
|
|
430
474
|
arr
|
|
431
475
|
}
|
|
@@ -435,32 +479,47 @@ fn jsval_to_js_d<'s>(
|
|
|
435
479
|
JsVal::Date(ms) => v8::Date::new(scope, ms)
|
|
436
480
|
.map(|d| d.into())
|
|
437
481
|
.unwrap_or_else(|| v8::undefined(scope).into()),
|
|
438
|
-
//
|
|
439
|
-
//
|
|
482
|
+
// The elements first, then the array made of them — unless one of them
|
|
483
|
+
// referred back to it, which made it early (`Built::get`): then they are
|
|
484
|
+
// defined on it one by one.
|
|
440
485
|
JsVal::Array { id, items } => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
486
|
+
built.open_arrays.insert(id, items.len());
|
|
487
|
+
let elements: Vec<v8::Local<v8::Value>> = items
|
|
488
|
+
.into_iter()
|
|
489
|
+
.map(|it| jsval_to_js_d(scope, it, built))
|
|
490
|
+
.collect();
|
|
491
|
+
built.open_arrays.remove(&id);
|
|
492
|
+
if let Some(early) = built.values.get(&id).copied() {
|
|
493
|
+
let array = v8::Local::<v8::Object>::try_from(early).unwrap();
|
|
494
|
+
for (i, element) in elements.into_iter().enumerate() {
|
|
495
|
+
if let Some(key) = v8::String::new(scope, &i.to_string()) {
|
|
496
|
+
array.create_data_property(scope, key.into(), element);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
early
|
|
500
|
+
} else {
|
|
501
|
+
let array = v8::Array::new_with_elements(scope, &elements).into();
|
|
502
|
+
built.values.insert(id, array);
|
|
503
|
+
array
|
|
446
504
|
}
|
|
447
|
-
arr.into()
|
|
448
505
|
}
|
|
506
|
+
// Register the container under its id BEFORE filling it, so a Ref from
|
|
507
|
+
// a descendant (a cycle back to here) resolves to this same object.
|
|
449
508
|
JsVal::Obj { id, entries } => {
|
|
450
509
|
let obj = v8::Object::new(scope);
|
|
451
|
-
built.insert(id, obj.into());
|
|
510
|
+
built.values.insert(id, obj.into());
|
|
452
511
|
for (k, it) in entries {
|
|
453
512
|
let Some(key) = v8::String::new(scope, &k) else {
|
|
454
513
|
continue;
|
|
455
514
|
};
|
|
456
515
|
let v = jsval_to_js_d(scope, it, built);
|
|
457
|
-
obj.
|
|
516
|
+
obj.create_data_property(scope, key.into(), v);
|
|
458
517
|
}
|
|
459
518
|
obj.into()
|
|
460
519
|
}
|
|
461
520
|
JsVal::Map { id, pairs } => {
|
|
462
521
|
let map = v8::Map::new(scope);
|
|
463
|
-
built.insert(id, map.into());
|
|
522
|
+
built.values.insert(id, map.into());
|
|
464
523
|
for (k, v) in pairs {
|
|
465
524
|
let kk = jsval_to_js_d(scope, k, built);
|
|
466
525
|
let vv = jsval_to_js_d(scope, v, built);
|
|
@@ -470,7 +529,7 @@ fn jsval_to_js_d<'s>(
|
|
|
470
529
|
}
|
|
471
530
|
JsVal::Set { id, items } => {
|
|
472
531
|
let set = v8::Set::new(scope);
|
|
473
|
-
built.insert(id, set.into());
|
|
532
|
+
built.values.insert(id, set.into());
|
|
474
533
|
for it in items {
|
|
475
534
|
let v = jsval_to_js_d(scope, it, built);
|
|
476
535
|
set.add(scope, v);
|
|
@@ -478,8 +537,7 @@ fn jsval_to_js_d<'s>(
|
|
|
478
537
|
set.into()
|
|
479
538
|
}
|
|
480
539
|
JsVal::Ref(id) => built
|
|
481
|
-
.get(
|
|
482
|
-
.copied()
|
|
540
|
+
.get(scope, id)
|
|
483
541
|
.unwrap_or_else(|| v8::undefined(scope).into()),
|
|
484
542
|
}
|
|
485
543
|
}
|
|
@@ -694,40 +752,26 @@ pub(crate) fn ruby_to_jsval(val: Value) -> Result<JsVal, Error> {
|
|
|
694
752
|
}
|
|
695
753
|
|
|
696
754
|
fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, Error> {
|
|
697
|
-
let ruby = Ruby::
|
|
755
|
+
let ruby = Ruby::get_with(val);
|
|
756
|
+
// The built-in types first, each by its type tag alone: no class looked up,
|
|
757
|
+
// no method called, nothing raised and rescued — a String asked whether it
|
|
758
|
+
// converts to a Float raises a TypeError to say no, which cost a String
|
|
759
|
+
// argument over a microsecond. A subclass of a built-in has the same tag, and
|
|
760
|
+
// goes the same way, as it always did.
|
|
698
761
|
if val.is_nil() {
|
|
699
762
|
return Ok(JsVal::Null);
|
|
700
763
|
}
|
|
701
|
-
|
|
702
|
-
// ANY non-false value — so check the actual true/false singletons by
|
|
703
|
-
// identity instead, or every Integer/String/Array would marshal as `true`.
|
|
704
|
-
if val.eql(ruby.qtrue()).unwrap_or(false) {
|
|
764
|
+
if val.as_raw() == ruby.qtrue().as_raw() {
|
|
705
765
|
return Ok(JsVal::Bool(true));
|
|
706
766
|
}
|
|
707
|
-
if val.
|
|
767
|
+
if val.as_raw() == ruby.qfalse().as_raw() {
|
|
708
768
|
return Ok(JsVal::Bool(false));
|
|
709
769
|
}
|
|
710
|
-
// Ruby Time -> JS Date. Must precede the numeric checks: magnus's
|
|
711
|
-
// i64/f64 TryConvert coerces a Time via to_i/to_f, so it would otherwise
|
|
712
|
-
// marshal as a bare epoch number. Time#to_f is epoch seconds; Date wants ms.
|
|
713
|
-
if let Ok(time_class) = ruby.class_object().const_get::<_, magnus::RClass>("Time")
|
|
714
|
-
&& val.is_kind_of(time_class)
|
|
715
|
-
{
|
|
716
|
-
let sec = val.funcall::<_, _, f64>("to_f", ())?;
|
|
717
|
-
return Ok(JsVal::Date(sec * 1000.0));
|
|
718
|
-
}
|
|
719
770
|
// Integer. A JS Number is an f64, so only integers exactly representable
|
|
720
771
|
// there (|n| <= 2^53) become Int/Number; anything larger (the rest of the
|
|
721
772
|
// i64 range AND true bignums) becomes a BigInt so no precision is lost.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
// instead of a Number.
|
|
725
|
-
if let Ok(int_class) = ruby
|
|
726
|
-
.class_object()
|
|
727
|
-
.const_get::<_, magnus::RClass>("Integer")
|
|
728
|
-
&& val.is_kind_of(int_class)
|
|
729
|
-
{
|
|
730
|
-
if let Ok(i) = i64::try_convert(val)
|
|
773
|
+
if let Some(int) = magnus::Integer::from_value(val) {
|
|
774
|
+
if let Ok(i) = int.to_i64()
|
|
731
775
|
&& i.unsigned_abs() <= (1u64 << 53)
|
|
732
776
|
{
|
|
733
777
|
return Ok(JsVal::Int(i));
|
|
@@ -740,8 +784,8 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
740
784
|
words: hex_to_words(&hex),
|
|
741
785
|
});
|
|
742
786
|
}
|
|
743
|
-
if let
|
|
744
|
-
return Ok(JsVal::Num(
|
|
787
|
+
if let Some(float) = magnus::Float::from_value(val) {
|
|
788
|
+
return Ok(JsVal::Num(float.to_f64()));
|
|
745
789
|
}
|
|
746
790
|
// Bare Symbol -> JS string (one-way: it comes back as a Ruby String). A
|
|
747
791
|
// binary-encoded symbol surfaces the same curated EncodingError as a text
|
|
@@ -779,6 +823,40 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
779
823
|
}
|
|
780
824
|
return string_to_jsval(&ruby, rstr);
|
|
781
825
|
}
|
|
826
|
+
if let Some(arr) = RArray::from_value(val) {
|
|
827
|
+
return array_to_jsval(arr, val, seen, depth);
|
|
828
|
+
}
|
|
829
|
+
if let Some(hash) = RHash::from_value(val) {
|
|
830
|
+
// A plain Hash is a JS object. RustyRacer::JSMap, a Hash subclass, is a JS
|
|
831
|
+
// Map, its arbitrary keys kept as real JS values (NOT string-coerced) — it
|
|
832
|
+
// is what makes a JS Map round-trip (JS Map -> JSMap -> JS Map) instead of
|
|
833
|
+
// degrading to an object. Only a Hash of another class asks which it is.
|
|
834
|
+
let plain = val.class().as_raw() == ruby.class_hash().as_raw();
|
|
835
|
+
if !plain
|
|
836
|
+
&& let Ok(js_map) = js_map_class(&ruby)
|
|
837
|
+
&& val.is_kind_of(js_map)
|
|
838
|
+
{
|
|
839
|
+
return map_to_jsval(hash, val, seen, depth);
|
|
840
|
+
}
|
|
841
|
+
return hash_to_jsval(&ruby, hash, val, seen, depth);
|
|
842
|
+
}
|
|
843
|
+
// What only a class, or a conversion method, can say.
|
|
844
|
+
//
|
|
845
|
+
// Ruby Time -> JS Date. Before the Numeric fallback: magnus's f64 TryConvert
|
|
846
|
+
// coerces a Time via to_f, so it would otherwise marshal as a bare epoch
|
|
847
|
+
// number. Time#to_f is epoch seconds; Date wants ms.
|
|
848
|
+
if let Ok(time_class) = ruby.class_object().const_get::<_, magnus::RClass>("Time")
|
|
849
|
+
&& val.is_kind_of(time_class)
|
|
850
|
+
{
|
|
851
|
+
let sec = val.funcall::<_, _, f64>("to_f", ())?;
|
|
852
|
+
return Ok(JsVal::Date(sec * 1000.0));
|
|
853
|
+
}
|
|
854
|
+
// Any other Float-convertible value (a Rational, a BigDecimal, an
|
|
855
|
+
// ActiveSupport::Duration — whatever rb_num2dbl takes, to_f included) -> its
|
|
856
|
+
// Float. A probe that raises to say no, but the built-ins are past it by now.
|
|
857
|
+
if let Ok(n) = f64::try_convert(val) {
|
|
858
|
+
return Ok(JsVal::Num(n));
|
|
859
|
+
}
|
|
782
860
|
// A String-like (to_str) gets the same tag-driven treatment, but its result
|
|
783
861
|
// is transient so it is not identity-tracked.
|
|
784
862
|
if val.respond_to("to_str", false).unwrap_or(false) {
|
|
@@ -787,7 +865,7 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
787
865
|
return string_to_jsval(&ruby, rstr);
|
|
788
866
|
}
|
|
789
867
|
}
|
|
790
|
-
// Ruby Set -> JS Set. Before the
|
|
868
|
+
// Ruby Set -> JS Set. Before the to_ary/to_hash conversions (a Set is neither).
|
|
791
869
|
if let Ok(set_class) = ruby.class_object().const_get::<_, magnus::RClass>("Set")
|
|
792
870
|
&& val.is_kind_of(set_class)
|
|
793
871
|
{
|
|
@@ -804,79 +882,10 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
804
882
|
return Ok(JsVal::Set { id, items });
|
|
805
883
|
}
|
|
806
884
|
if let Ok(arr) = RArray::try_convert(val) {
|
|
807
|
-
|
|
808
|
-
RbId::New(id) => id,
|
|
809
|
-
RbId::Reuse(jv) => return Ok(jv),
|
|
810
|
-
};
|
|
811
|
-
let mut items = Vec::with_capacity(arr.len());
|
|
812
|
-
for i in 0..arr.len() {
|
|
813
|
-
let el: Value = arr.entry::<Value>(i as isize)?;
|
|
814
|
-
items.push(ruby_to_jsval_d(el, seen, depth + 1)?);
|
|
815
|
-
}
|
|
816
|
-
return Ok(JsVal::Array { id, items });
|
|
817
|
-
}
|
|
818
|
-
// RustyRacer::JSMap (a Hash subclass) -> JS Map, preserving arbitrary keys as
|
|
819
|
-
// real JS values (NOT string-coerced like a plain Hash -> JS Object). Must come
|
|
820
|
-
// BEFORE the Hash branch, since a JSMap IS-A Hash. This is what makes a JS Map
|
|
821
|
-
// round-trip (JS Map -> JSMap -> JS Map) instead of degrading to an object.
|
|
822
|
-
if let Ok(js_map) = js_map_class(&ruby)
|
|
823
|
-
&& val.is_kind_of(js_map)
|
|
824
|
-
{
|
|
825
|
-
let id = match rb_container_id(seen, val, depth)? {
|
|
826
|
-
RbId::New(id) => id,
|
|
827
|
-
RbId::Reuse(jv) => return Ok(jv),
|
|
828
|
-
};
|
|
829
|
-
let hash = RHash::from_value(val).expect("JSMap is a Hash");
|
|
830
|
-
let pairs = RefCell::new(Vec::new());
|
|
831
|
-
hash.foreach(|k: Value, v: Value| {
|
|
832
|
-
pairs.borrow_mut().push((
|
|
833
|
-
ruby_to_jsval_d(k, seen, depth + 1)?,
|
|
834
|
-
ruby_to_jsval_d(v, seen, depth + 1)?,
|
|
835
|
-
));
|
|
836
|
-
Ok(magnus::r_hash::ForEach::Continue)
|
|
837
|
-
})?;
|
|
838
|
-
return Ok(JsVal::Map {
|
|
839
|
-
id,
|
|
840
|
-
pairs: pairs.into_inner(),
|
|
841
|
-
});
|
|
885
|
+
return array_to_jsval(arr, val, seen, depth);
|
|
842
886
|
}
|
|
843
887
|
if let Ok(hash) = RHash::try_convert(val) {
|
|
844
|
-
|
|
845
|
-
RbId::New(id) => id,
|
|
846
|
-
RbId::Reuse(jv) => return Ok(jv),
|
|
847
|
-
};
|
|
848
|
-
let entries = RefCell::new(Vec::new());
|
|
849
|
-
hash.foreach(|k: Value, v: Value| {
|
|
850
|
-
// String/Symbol keys -> a UTF-8 String; anything else via to_s. A JS
|
|
851
|
-
// object key has nowhere to put raw bytes, so unlike a binary VALUE
|
|
852
|
-
// (-> Uint8Array) a binary KEY with invalid UTF-8 RAISES (string_key),
|
|
853
|
-
// and a to_s returning a non-String is a loud error, not a silent "".
|
|
854
|
-
let key = match string_key(&ruby, k) {
|
|
855
|
-
Some(r) => r?,
|
|
856
|
-
None => {
|
|
857
|
-
// A non-String key (Symbol, Integer, ...) -> to_s, then the
|
|
858
|
-
// same UTF-8 rule.
|
|
859
|
-
let s: Value = k.funcall("to_s", ())?;
|
|
860
|
-
match string_key(&ruby, s) {
|
|
861
|
-
Some(r) => r?,
|
|
862
|
-
None => {
|
|
863
|
-
return Err(Error::new(
|
|
864
|
-
ruby.exception_type_error(),
|
|
865
|
-
"hash key's to_s did not return a String",
|
|
866
|
-
));
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
entries
|
|
872
|
-
.borrow_mut()
|
|
873
|
-
.push((key, ruby_to_jsval_d(v, seen, depth + 1)?));
|
|
874
|
-
Ok(magnus::r_hash::ForEach::Continue)
|
|
875
|
-
})?;
|
|
876
|
-
return Ok(JsVal::Obj {
|
|
877
|
-
id,
|
|
878
|
-
entries: entries.into_inner(),
|
|
879
|
-
});
|
|
888
|
+
return hash_to_jsval(&ruby, hash, val, seen, depth);
|
|
880
889
|
}
|
|
881
890
|
Err(Error::new(
|
|
882
891
|
ruby.exception_type_error(),
|
|
@@ -884,6 +893,86 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
884
893
|
))
|
|
885
894
|
}
|
|
886
895
|
|
|
896
|
+
// An Array (or what to_ary made of |val|) -> a JS array; |val| is the identity.
|
|
897
|
+
fn array_to_jsval(arr: RArray, val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, Error> {
|
|
898
|
+
let id = match rb_container_id(seen, val, depth)? {
|
|
899
|
+
RbId::New(id) => id,
|
|
900
|
+
RbId::Reuse(jv) => return Ok(jv),
|
|
901
|
+
};
|
|
902
|
+
let mut items = Vec::with_capacity(arr.len());
|
|
903
|
+
for i in 0..arr.len() {
|
|
904
|
+
let el: Value = arr.entry::<Value>(i as isize)?;
|
|
905
|
+
items.push(ruby_to_jsval_d(el, seen, depth + 1)?);
|
|
906
|
+
}
|
|
907
|
+
Ok(JsVal::Array { id, items })
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// A RustyRacer::JSMap -> a JS Map.
|
|
911
|
+
fn map_to_jsval(hash: RHash, val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, Error> {
|
|
912
|
+
let id = match rb_container_id(seen, val, depth)? {
|
|
913
|
+
RbId::New(id) => id,
|
|
914
|
+
RbId::Reuse(jv) => return Ok(jv),
|
|
915
|
+
};
|
|
916
|
+
let pairs = RefCell::new(Vec::new());
|
|
917
|
+
hash.foreach(|k: Value, v: Value| {
|
|
918
|
+
pairs.borrow_mut().push((
|
|
919
|
+
ruby_to_jsval_d(k, seen, depth + 1)?,
|
|
920
|
+
ruby_to_jsval_d(v, seen, depth + 1)?,
|
|
921
|
+
));
|
|
922
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
923
|
+
})?;
|
|
924
|
+
Ok(JsVal::Map {
|
|
925
|
+
id,
|
|
926
|
+
pairs: pairs.into_inner(),
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// A Hash (or what to_hash made of |val|) -> a JS object with string keys.
|
|
931
|
+
fn hash_to_jsval(
|
|
932
|
+
ruby: &Ruby,
|
|
933
|
+
hash: RHash,
|
|
934
|
+
val: Value,
|
|
935
|
+
seen: &mut RbSeen,
|
|
936
|
+
depth: u32,
|
|
937
|
+
) -> Result<JsVal, Error> {
|
|
938
|
+
let id = match rb_container_id(seen, val, depth)? {
|
|
939
|
+
RbId::New(id) => id,
|
|
940
|
+
RbId::Reuse(jv) => return Ok(jv),
|
|
941
|
+
};
|
|
942
|
+
let entries = RefCell::new(Vec::new());
|
|
943
|
+
hash.foreach(|k: Value, v: Value| {
|
|
944
|
+
// String/Symbol keys -> a UTF-8 String; anything else via to_s. A JS
|
|
945
|
+
// object key has nowhere to put raw bytes, so unlike a binary VALUE
|
|
946
|
+
// (-> Uint8Array) a binary KEY with invalid UTF-8 RAISES (string_key),
|
|
947
|
+
// and a to_s returning a non-String is a loud error, not a silent "".
|
|
948
|
+
let key = match string_key(ruby, k) {
|
|
949
|
+
Some(r) => r?,
|
|
950
|
+
None => {
|
|
951
|
+
// A non-String key (Symbol, Integer, ...) -> to_s, then the
|
|
952
|
+
// same UTF-8 rule.
|
|
953
|
+
let s: Value = k.funcall("to_s", ())?;
|
|
954
|
+
match string_key(ruby, s) {
|
|
955
|
+
Some(r) => r?,
|
|
956
|
+
None => {
|
|
957
|
+
return Err(Error::new(
|
|
958
|
+
ruby.exception_type_error(),
|
|
959
|
+
"hash key's to_s did not return a String",
|
|
960
|
+
));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
entries
|
|
966
|
+
.borrow_mut()
|
|
967
|
+
.push((key, ruby_to_jsval_d(v, seen, depth + 1)?));
|
|
968
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
969
|
+
})?;
|
|
970
|
+
Ok(JsVal::Obj {
|
|
971
|
+
id,
|
|
972
|
+
entries: entries.into_inner(),
|
|
973
|
+
})
|
|
974
|
+
}
|
|
975
|
+
|
|
887
976
|
enum RbId {
|
|
888
977
|
New(u32),
|
|
889
978
|
Reuse(JsVal),
|
|
@@ -893,7 +982,9 @@ enum RbId {
|
|
|
893
982
|
// Reuse(jsval) to emit directly (a Ref to an already-seen object, or a
|
|
894
983
|
// depth-truncated Str). Computes object_id once.
|
|
895
984
|
fn rb_container_id(seen: &mut RbSeen, val: Value, depth: u32) -> Result<RbId, Error> {
|
|
896
|
-
|
|
985
|
+
// rb_obj_id itself rather than a call to #object_id: the same id, without a
|
|
986
|
+
// method dispatch per container.
|
|
987
|
+
let oid = usize::try_convert(unsafe { Value::from_raw(rb_sys::rb_obj_id(val.as_raw())) })?;
|
|
897
988
|
if let Some(id) = seen.map.get(&oid) {
|
|
898
989
|
return Ok(RbId::Reuse(JsVal::Ref(*id)));
|
|
899
990
|
}
|
|
@@ -910,3 +1001,151 @@ fn rb_container_id(seen: &mut RbSeen, val: Value, depth: u32) -> Result<RbId, Er
|
|
|
910
1001
|
seen.map.insert(oid, id);
|
|
911
1002
|
Ok(RbId::New(id))
|
|
912
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
#[cfg(test)]
|
|
1006
|
+
mod tests {
|
|
1007
|
+
use super::*;
|
|
1008
|
+
|
|
1009
|
+
// Run |check| in a fresh context of a fresh isolate, after |setup|.
|
|
1010
|
+
fn in_context(setup: &str, check: impl FnOnce(&mut v8::PinScope<'_, '_>)) {
|
|
1011
|
+
crate::init_v8();
|
|
1012
|
+
let isolate = &mut v8::Isolate::new(Default::default());
|
|
1013
|
+
v8::scope!(let scope, isolate);
|
|
1014
|
+
let context = v8::Context::new(scope, Default::default());
|
|
1015
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1016
|
+
eval(scope, setup);
|
|
1017
|
+
check(scope);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> {
|
|
1021
|
+
let code = v8::String::new(scope, source).unwrap();
|
|
1022
|
+
v8::Script::compile(scope, code, None)
|
|
1023
|
+
.unwrap()
|
|
1024
|
+
.run(scope)
|
|
1025
|
+
.unwrap()
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
fn global_set(scope: &mut v8::PinScope<'_, '_>, name: &str, value: v8::Local<v8::Value>) {
|
|
1029
|
+
let key = v8::String::new(scope, name).unwrap();
|
|
1030
|
+
let global = scope.get_current_context().global(scope);
|
|
1031
|
+
global.create_data_property(scope, key.into(), value);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
fn as_string(scope: &mut v8::PinScope<'_, '_>, source: &str) -> String {
|
|
1035
|
+
let value = eval(scope, source);
|
|
1036
|
+
value.to_rust_string_lossy(scope)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const TRAPS: &str = r#"
|
|
1040
|
+
globalThis.hits = [];
|
|
1041
|
+
Object.defineProperty(Array.prototype, '0', {set(v) { hits.unshift(v) }, get() { return 'proto' }});
|
|
1042
|
+
Object.defineProperty(Object.prototype, 'k', {set(v) { hits.unshift(v) }, get() { return 'proto' }});
|
|
1043
|
+
"#;
|
|
1044
|
+
|
|
1045
|
+
#[test]
|
|
1046
|
+
fn arrays_and_objects_are_defined_past_prototype_setters() {
|
|
1047
|
+
in_context(TRAPS, |scope| {
|
|
1048
|
+
let value = jsval_to_js(
|
|
1049
|
+
scope,
|
|
1050
|
+
JsVal::Array {
|
|
1051
|
+
id: 0,
|
|
1052
|
+
items: vec![
|
|
1053
|
+
JsVal::Int(41),
|
|
1054
|
+
JsVal::Obj {
|
|
1055
|
+
id: 1,
|
|
1056
|
+
entries: vec![("k".into(), JsVal::Str("own".into()))],
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
);
|
|
1061
|
+
global_set(scope, "v", value);
|
|
1062
|
+
assert_eq!(
|
|
1063
|
+
as_string(
|
|
1064
|
+
scope,
|
|
1065
|
+
"[v[0], v[1].k, Object.hasOwn(v[1], 'k'), hits.length].join()"
|
|
1066
|
+
),
|
|
1067
|
+
"41,own,true,0"
|
|
1068
|
+
);
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
#[test]
|
|
1073
|
+
fn a_proto_key_is_an_own_property() {
|
|
1074
|
+
in_context("", |scope| {
|
|
1075
|
+
let value = jsval_to_js(
|
|
1076
|
+
scope,
|
|
1077
|
+
JsVal::Obj {
|
|
1078
|
+
id: 0,
|
|
1079
|
+
entries: vec![("__proto__".into(), JsVal::Null)],
|
|
1080
|
+
},
|
|
1081
|
+
);
|
|
1082
|
+
global_set(scope, "v", value);
|
|
1083
|
+
assert_eq!(
|
|
1084
|
+
as_string(
|
|
1085
|
+
scope,
|
|
1086
|
+
"[Object.getPrototypeOf(v) === Object.prototype, Object.hasOwn(v, '__proto__')].join()"
|
|
1087
|
+
),
|
|
1088
|
+
"true,true"
|
|
1089
|
+
);
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
#[test]
|
|
1094
|
+
fn an_array_referring_to_itself_is_one_array() {
|
|
1095
|
+
in_context(TRAPS, |scope| {
|
|
1096
|
+
// [1, <itself>, [<itself>]]
|
|
1097
|
+
let value = jsval_to_js(
|
|
1098
|
+
scope,
|
|
1099
|
+
JsVal::Array {
|
|
1100
|
+
id: 0,
|
|
1101
|
+
items: vec![
|
|
1102
|
+
JsVal::Int(1),
|
|
1103
|
+
JsVal::Ref(0),
|
|
1104
|
+
JsVal::Array {
|
|
1105
|
+
id: 1,
|
|
1106
|
+
items: vec![JsVal::Ref(0)],
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
);
|
|
1111
|
+
global_set(scope, "v", value);
|
|
1112
|
+
assert_eq!(
|
|
1113
|
+
as_string(
|
|
1114
|
+
scope,
|
|
1115
|
+
"[v.length, v[0], v[1] === v, v[2][0] === v, Array.isArray(v), hits.length].join()"
|
|
1116
|
+
),
|
|
1117
|
+
"3,1,true,true,true,0"
|
|
1118
|
+
);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
#[test]
|
|
1123
|
+
fn maps_and_sets_are_filled_by_the_original_builtins() {
|
|
1124
|
+
in_context(
|
|
1125
|
+
"Map.prototype.set = () => { throw new Error('patched') }; Set.prototype.add = Map.prototype.set;",
|
|
1126
|
+
|scope| {
|
|
1127
|
+
let value = jsval_to_js(
|
|
1128
|
+
scope,
|
|
1129
|
+
JsVal::Array {
|
|
1130
|
+
id: 0,
|
|
1131
|
+
items: vec![
|
|
1132
|
+
JsVal::Map {
|
|
1133
|
+
id: 1,
|
|
1134
|
+
pairs: vec![(JsVal::Str("a".into()), JsVal::Int(1))],
|
|
1135
|
+
},
|
|
1136
|
+
JsVal::Set {
|
|
1137
|
+
id: 2,
|
|
1138
|
+
items: vec![JsVal::Int(2)],
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
},
|
|
1142
|
+
);
|
|
1143
|
+
global_set(scope, "v", value);
|
|
1144
|
+
assert_eq!(
|
|
1145
|
+
as_string(scope, "[v[0].get('a'), v[1].has(2)].join()"),
|
|
1146
|
+
"1,true"
|
|
1147
|
+
);
|
|
1148
|
+
},
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|