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.
@@ -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
- let mut built: HashMap<u32, v8::Local<'s, v8::Value>> = HashMap::new();
400
- jsval_to_js_d(scope, val, &mut built)
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 HashMap<u32, v8::Local<'s, v8::Value>>,
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
- // Register the container under its id BEFORE filling it, so a Ref from
439
- // a descendant (a cycle back to here) resolves to this same object.
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
- let arr = v8::Array::new(scope, items.len() as i32);
442
- built.insert(id, arr.into());
443
- for (i, it) in items.into_iter().enumerate() {
444
- let v = jsval_to_js_d(scope, it, built);
445
- arr.set_index(scope, i as u32, v);
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.set(scope, key.into(), v);
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(&id)
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::get().unwrap();
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
- // NB: bool::try_convert is RTEST (truthiness) — it returns Ok(true) for
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.eql(ruby.qfalse()).unwrap_or(false) {
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
- // Use a strict Integer type check, NOT magnus::Integer::try_convert, which
723
- // coerces a Float / to_int object — that would turn e.g. 1e300 into a BigInt
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 Ok(n) = f64::try_convert(val) {
744
- return Ok(JsVal::Num(n));
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 Array/Hash checks (a Set is neither).
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
- let id = match rb_container_id(seen, val, depth)? {
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
- let id = match rb_container_id(seen, val, depth)? {
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
- let oid = val.funcall::<_, _, usize>("object_id", ())?;
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
+ }