Skip to content
Merged
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
45 changes: 45 additions & 0 deletions crates/perry-runtime/src/object/field_get_set/has_property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,51 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
}
return nanbox_false;
}
// #6148: `Uint8Array` / `Buffer` are backed by a header-less registered
// buffer (not `TYPED_ARRAY_REGISTRY`), so the typed-array arm above misses
// them. A Buffer is a `Uint8Array`, so `in` consults numeric indices
// (bounds) and the own/inherited members property-get can resolve.
if crate::buffer::is_registered_buffer(obj_addr as usize) {
let buf = obj_addr as *const crate::buffer::BufferHeader;
let len = unsafe { crate::buffer::js_buffer_length(buf) };
if key_val.is_int32() {
let idx = key_val.as_int32();
return if idx >= 0 && idx < len {
nanbox_true
} else {
nanbox_false
};
}
if key_val.is_number() {
let f = f64::from_bits(key_val.bits());
let present = f.is_finite()
&& f >= 0.0
&& f.fract() == 0.0
&& f <= i32::MAX as f64
&& (f as i32) < len;
return if present { nanbox_true } else { nanbox_false };
}
if key_val.is_any_string() {
// Own view slots, always present. Inherited prototype MEMBERS
// (`subarray`, `map`, `toString`, …) via `in` on a buffer are a
// separate follow-up — the same string-key gap the registered
// typed-array arm above has, tied to lazy `%TypedArray%`
// prototype population.
let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }
.and_then(|b| std::str::from_utf8(b).ok())
{
if matches!(
name,
"length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer"
) {
return nanbox_true;
}
}
return nanbox_false;
Comment on lines +325 to +342

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

String-encoded numeric indices ("0" in buf) incorrectly return false.

The buffer branch only matches string keys against the five named view slots and returns false for everything else. This means "0" in new Uint8Array([1,2,3]) returns false, when per spec it should return trueToPropertyKey("0") resolves to an in-bounds index.

The typed-array branch above (lines 272–281) delegates all string keys to typed_array_has_property, which handles string-encoded indices. The buffer branch lacks equivalent handling. The comment at lines 326–330 claims this is "the same string-key gap the registered typed-array arm above has," but the typed-array arm handles string keys comprehensively — the string-encoded index gap is unique to this buffer branch.

This is not covered by the PR's stated scope limitation (which only excludes inherited prototype members like "subarray" in u8). String-encoded numeric indices are own properties, not inherited members.

🐛 Proposed fix: add `canonical_array_index` check for string-encoded indices
             if key_val.is_any_string() {
                 // Own view slots, always present. Inherited prototype MEMBERS
                 // (`subarray`, `map`, `toString`, …) via `in` on a buffer are a
                 // separate follow-up — the same string-key gap the registered
                 // typed-array arm above has, tied to lazy `%TypedArray%`
                 // prototype population.
                 let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
                 if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }
                     .and_then(|b| std::str::from_utf8(b).ok())
                 {
                     if matches!(
                         name,
                         "length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer"
                     ) {
                         return nanbox_true;
                     }
+                    // String-encoded numeric indices (e.g. `"0" in buf`).
+                    if let Some(idx) = super::super::canonical_array_index(name) {
+                        if (idx as i32) < len {
+                            return nanbox_true;
+                        }
+                    }
                 }
                 return nanbox_false;
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if key_val.is_any_string() {
// Own view slots, always present. Inherited prototype MEMBERS
// (`subarray`, `map`, `toString`, …) via `in` on a buffer are a
// separate follow-up — the same string-key gap the registered
// typed-array arm above has, tied to lazy `%TypedArray%`
// prototype population.
let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }
.and_then(|b| std::str::from_utf8(b).ok())
{
if matches!(
name,
"length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer"
) {
return nanbox_true;
}
}
return nanbox_false;
if key_val.is_any_string() {
// Own view slots, always present. Inherited prototype MEMBERS
// (`subarray`, `map`, `toString`, …) via `in` on a buffer are a
// separate follow-up — the same string-key gap the registered
// typed-array arm above has, tied to lazy `%TypedArray%`
// prototype population.
let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }
.and_then(|b| std::str::from_utf8(b).ok())
{
if matches!(
name,
"length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer"
) {
return nanbox_true;
}
// String-encoded numeric indices (e.g. `"0" in buf`).
if let Some(idx) = super::super::canonical_array_index(name) {
if (idx as i32) < len {
return nanbox_true;
}
}
}
return nanbox_false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/field_get_set/has_property.rs` around lines
325 - 342, The buffer-branch handling in has_property is only checking the named
view slots, so string-encoded indices like "0" are incorrectly falling through
to false; update this branch to also recognize canonical array-index string keys
the same way the typed-array path delegates to typed_array_has_property. Use the
existing key parsing in has_property (the key_val.is_any_string() block) and add
an index check before returning false so in-bounds numeric strings on
buffer-backed views are reported as present, while still preserving the current
named-slot behavior and the intended exclusion for inherited prototype members.

}
return nanbox_false;
}
let obj_ptr = obj_addr as *mut ObjectHeader;
unsafe {
if !obj_ptr.is_null() && (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID {
Expand Down
Loading