Skip to content

Commit 550c25a

Browse files
proggeramlugRalph Küpper
andauthored
fix(object): Object.entries/values skip non-enumerable descriptor slots (#5046) (#5049)
Object.keys consulted the per-property descriptor table and filtered keys defined with enumerable: false; js_object_values and js_object_entries walked the keys_array unfiltered, so Object.defineProperty(o, k, { value }) slots leaked into entries/values output (chalk's vendored ansi-styles: 55 entries under Perry vs Node's 45). Add a shared descriptor_marks_non_enumerable helper and gate it behind the same cheap any-descriptor probe js_object_keys uses, so descriptor-free objects stay on the fast path. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 553a9da commit 550c25a

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

crates/perry-runtime/src/object/field_get_set.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,30 @@ pub(crate) unsafe fn instance_private_key_hidden(
18741874
.unwrap_or(false)
18751875
}
18761876

1877+
/// True when a per-property descriptor marks `key_val`'s name non-enumerable
1878+
/// (`Object.defineProperty(o, k, { enumerable: false })`). Mirrors the
1879+
/// slow-path filter in `js_object_keys` so `Object.values`/`Object.entries`
1880+
/// agree with `Object.keys` (#5046). Callers gate on a cheap "does this object
1881+
/// have any descriptors at all" probe so the common descriptor-free object
1882+
/// never pays the string extraction.
1883+
pub(crate) unsafe fn descriptor_marks_non_enumerable(
1884+
obj: *const ObjectHeader,
1885+
key_val: crate::JSValue,
1886+
) -> bool {
1887+
let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN];
1888+
let bytes = match crate::string::js_string_key_bytes(key_val, &mut buf) {
1889+
Some(b) => b,
1890+
None => return false,
1891+
};
1892+
let key_str = match std::str::from_utf8(bytes) {
1893+
Ok(s) => s,
1894+
Err(_) => return false,
1895+
};
1896+
get_property_attrs(obj as usize, key_str)
1897+
.map(|attrs| !attrs.enumerable())
1898+
.unwrap_or(false)
1899+
}
1900+
18771901
/// Returns an array of the object's field values
18781902
#[no_mangle]
18791903
pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader {
@@ -1963,13 +1987,21 @@ pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader
19631987
None => j as u32,
19641988
}
19651989
};
1990+
// #5046: skip keys a descriptor marks non-enumerable, like
1991+
// `js_object_keys` does. Cheap any-descriptor probe first so
1992+
// descriptor-free objects stay on the fast path.
1993+
let has_descriptors =
1994+
PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == obj as usize));
19661995
for j in 0..count {
19671996
let i = pos(j);
19681997
if !keys.is_null() && i < crate::array::js_array_length(keys) {
19691998
let key_val = crate::array::js_array_get(keys, i);
19701999
if instance_private_key_hidden(obj, key_val) {
19712000
continue;
19722001
}
2002+
if has_descriptors && descriptor_marks_non_enumerable(obj, key_val) {
2003+
continue;
2004+
}
19732005
}
19742006
let value = js_object_get_field(obj as *mut ObjectHeader, i);
19752007
crate::array::js_array_push_f64(result, f64::from_bits(value.bits()));
@@ -2101,13 +2133,21 @@ pub extern "C" fn js_object_entries(obj: *const ObjectHeader) -> *mut ArrayHeade
21012133
None => j as u32,
21022134
}
21032135
};
2136+
// #5046: skip keys a descriptor marks non-enumerable, like
2137+
// `js_object_keys` does. Cheap any-descriptor probe first so
2138+
// descriptor-free objects stay on the fast path.
2139+
let has_descriptors =
2140+
PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == obj as usize));
21042141
for j in 0..count {
21052142
let i = pos(j);
21062143
if !keys.is_null() && i < crate::array::js_array_length(keys) {
21072144
let key_val = crate::array::js_array_get(keys, i);
21082145
if instance_private_key_hidden(obj, key_val) {
21092146
continue;
21102147
}
2148+
if has_descriptors && descriptor_marks_non_enumerable(obj, key_val) {
2149+
continue;
2150+
}
21112151
}
21122152
// Create a pair array [key, value]
21132153
let pair = crate::array::js_array_alloc(2);

crates/perry-runtime/src/object/tests.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,3 +642,49 @@ fn transition_cache_lookup_rejects_mutated_edge_target() {
642642
};
643643
});
644644
}
645+
646+
#[test]
647+
fn entries_and_values_skip_non_enumerable_descriptor_slots() {
648+
// #5046: Object.defineProperty(o, 'hidden', { value: 1 }) defaults to
649+
// enumerable: false. Object.keys filtered it; entries/values did not.
650+
unsafe {
651+
let obj = js_object_alloc(0, 0);
652+
let hidden_key = crate::string::js_string_from_bytes(b"hidden".as_ptr(), 6);
653+
let shown_key = crate::string::js_string_from_bytes(b"shown".as_ptr(), 5);
654+
let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5);
655+
656+
let descriptor = js_object_alloc(0, 0);
657+
js_object_set_field_by_name(descriptor, value_key, 1.0);
658+
659+
let obj_value = crate::value::js_nanbox_pointer(obj as i64);
660+
let hidden_value = f64::from_bits(JSValue::string_ptr(hidden_key).bits());
661+
let descriptor_value = crate::value::js_nanbox_pointer(descriptor as i64);
662+
js_object_define_property(obj_value, hidden_value, descriptor_value);
663+
js_object_set_field_by_name(obj as *mut ObjectHeader, shown_key, 2.0);
664+
665+
let keys = js_object_keys(obj);
666+
assert_eq!(crate::array::js_array_length(keys), 1);
667+
assert_eq!(
668+
js_string_to_rust(crate::array::js_array_get(keys, 0).into()),
669+
"shown"
670+
);
671+
672+
let values = js_object_values(obj);
673+
assert_eq!(crate::array::js_array_length(values), 1);
674+
assert_eq!(
675+
crate::array::js_array_get(values, 0).bits(),
676+
2.0f64.to_bits()
677+
);
678+
679+
let entries = js_object_entries(obj);
680+
assert_eq!(crate::array::js_array_length(entries), 1);
681+
let pair = crate::value::js_nanbox_get_pointer(f64::from_bits(
682+
crate::array::js_array_get(entries, 0).bits(),
683+
)) as *const crate::array::ArrayHeader;
684+
assert_eq!(
685+
js_string_to_rust(crate::array::js_array_get(pair, 0).into()),
686+
"shown"
687+
);
688+
assert_eq!(crate::array::js_array_get(pair, 1).bits(), 2.0f64.to_bits());
689+
}
690+
}

0 commit comments

Comments
 (0)