diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index 27f616e93b..f0767736b6 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -1265,6 +1265,10 @@ unsafe fn format_object_as_json( } let boxed_base = boxed_primitives::boxed_primitive_base_for_object(obj_ptr); + // A boxed `String` exposes its characters as integer-index own properties + // (`"0".."len-1"`). Node folds those into the `[String: '…']` base and never + // lists them in the `{ … }` body — only extra own keys appear. Skip them. + let boxed_string_char_count = boxed_primitives::boxed_string_char_index_count(obj_ptr); let class_name = { let class_id = (*obj_ptr).class_id; if class_id == 0 { @@ -1352,6 +1356,16 @@ unsafe fn format_object_as_json( continue; } + // Hide a boxed String's character index properties (`"0".."len-1"`): + // they are rendered by the `[String: '…']` base, not the body. + if let Some(char_count) = boxed_string_char_count { + if let Ok(idx) = key_str.parse::() { + if idx < char_count { + continue; + } + } + } + let is_enumerable = if descriptors_in_use { crate::object::get_property_attrs(obj_addr, &key_str) .map(|a| a.enumerable()) diff --git a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs index 64dab5528d..28c5fb708c 100644 --- a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs +++ b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs @@ -36,6 +36,33 @@ pub(super) unsafe fn boxed_primitive_base_for_object( } } +/// For a boxed `String` wrapper (`new String("abc")`), the integer-index own +/// properties `"0".."len-1"` mirror the underlying characters. Node's +/// `util.inspect` treats those as the wrapped primitive (shown via the +/// `[String: '…']` base) and never lists them in the `{ … }` body — only extra +/// own keys appear there. Returns the wrapped string's length for a boxed +/// String, otherwise `None`. +/// +/// The count is in UTF-16 code units, NOT Unicode scalar values: the index +/// properties are installed over `0..js_string_length` (`utf16_len`) by +/// `install_string_wrapper_indices`, so a non-BMP char (e.g. an emoji, two +/// UTF-16 units) occupies two indices. Counting `.chars()` would under-count +/// and leak a trailing index (e.g. `new String("a😀b")` → `{ 3: 'b' }`). +pub(super) unsafe fn boxed_string_char_index_count( + obj_ptr: *const crate::object::ObjectHeader, +) -> Option { + let (class_id, payload) = boxed_primitive_payload_for_object(obj_ptr)?; + if class_id != CLASS_ID_BOXED_STRING { + return None; + } + Some( + jsvalue_string_content(payload) + .unwrap_or_default() + .encode_utf16() + .count(), + ) +} + unsafe fn boxed_primitive_payload_for_object( obj_ptr: *const crate::object::ObjectHeader, ) -> Option<(u32, f64)> { diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index a87e7140f9..92c20253ec 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -2725,7 +2725,19 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( ); } - let obj_ptr = js_object_alloc(0, 0); + // Stamp the instance with the class id of `newTarget` (not the invoked + // `target`). Per `OrdinaryCreateFromConstructor`, the instance's + // `[[Prototype]]` is `newTarget.prototype`, so `obj instanceof newTarget` + // must be true and `obj instanceof target` false. Perry models the + // prototype chain via class ids, so allocating with `0` left + // `Reflect.construct(Target, …, NewTarget)` instances matching neither. + // A `newTarget` may be a *declared class* (an `Expr::ClassRef`, e.g. + // `Reflect.construct(plainFn, [], class C {})`) — resolve its registered + // class id first so `instanceof C` holds — or a *plain function*, for which + // the synthetic per-function id applies. (The real `[[Prototype]]` link is + // still set below from `newTarget.prototype`.) + let cid = new_target_class_id(nt).unwrap_or_else(|| synthetic_class_id_for_function(nt)); + let obj_ptr = js_object_alloc(cid, 0); let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); if let Some(proto_bits) = constructor_prototype_bits(nt) { super::prototype_chain::object_set_static_prototype(obj_ptr as usize, proto_bits); diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index c5681e8732..170bcdc7d5 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -386,6 +386,18 @@ pub(crate) fn try_read_as_search_params( return None; } unsafe { + // A genuine URLSearchParams is always allocated with `class_id == 0` + // (an ordinary object, see `create_url_search_params`). Other native + // classes — notably `util.MIMEParams` — ALSO store their data in a + // leading `_entries` slot but carry a distinct registered class id and + // a different field layout (no `_owner` slot). Without this guard such + // an object is mis-detected below, then read with the URLSearchParams + // layout (an out-of-bounds `_owner` field read) → segfault when e.g. + // `String(mimeParams)` / `mimeParams.toString()` routes through + // `js_jsvalue_to_string`. Bail for any non-zero class id. + if (*params).class_id != 0 { + return None; + } // URLSearchParams stores entries in field index 0 (URL_SEARCH_PARAMS_ENTRIES). // If this isn't a URLSearchParams, that slot likely holds a string or // is missing — we detect by checking the keys array shape. diff --git a/m5 b/m5 new file mode 100755 index 0000000000..417696094a Binary files /dev/null and b/m5 differ