Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/perry-runtime/src/builtins/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<usize>() {
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())
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
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(),
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

unsafe fn boxed_primitive_payload_for_object(
obj_ptr: *const crate::object::ObjectHeader,
) -> Option<(u32, f64)> {
Expand Down
14 changes: 13 additions & 1 deletion crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-runtime/src/url/search_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Binary file added m5
Binary file not shown.