diff --git a/crates/perry-codegen/src/expr/arrays_finds.rs b/crates/perry-codegen/src/expr/arrays_finds.rs index 2dc95e255..a1366f740 100644 --- a/crates/perry-codegen/src/expr/arrays_finds.rs +++ b/crates/perry-codegen/src/expr/arrays_finds.rs @@ -113,6 +113,29 @@ pub(crate) fn lower_uint8array_get_i32( return Ok(value); } + // An UNPROVEN key may be a string at runtime: a Buffer is an ordinary + // object in Node, so `buf[k]` with a non-numeric `k` reads a property (an + // own expando, else the prototype method), not a byte. Coercing it to i32 + // read byte 0 and yielded `undefined`, which broke the ubiquitous + // feature-probe `typeof obj[k] === "function"` — mysql2's `MockBuffer` + // relies on it to neutralize a zero-length Buffer's write methods while + // sizing each outgoing packet, so the MySQL handshake died with RangeError + // [ERR_OUT_OF_RANGE]. Route to the polymorphic helper (it dispatches + // numeric keys to the byte read and string keys to the property path); the + // proven-numeric fast paths above are untouched. + if !is_numeric_expr(ctx, index) { + let a = lower_expr(ctx, array)?; + let key = lower_expr(ctx, index)?; + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &a); + let result = blk.call( + DOUBLE, + "js_object_get_index_polymorphic", + &[(I64, &handle), (DOUBLE, &key)], + ); + return Ok(LoweredValue::js_value(result)); + } + let idx_i32 = lower_index_i32(ctx, index)?; let a = lower_expr(ctx, array)?; let blk = ctx.block(); @@ -825,16 +848,32 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { )); } if !numeric_index_has_integer_array_index_proof(ctx, index) { + // A non-numeric key stores an OWN property (Node's Buffer is an + // ordinary object, and an own key shadows the same-named + // prototype method — mysql2's `MockBuffer` overwrites the write + // methods of a zero-length Buffer to size a packet). The + // typed-array helper coerces the key to a number and dropped the + // store; the polymorphic setter dispatches numeric keys to the + // byte write and string keys to the own-prop table. + let key_maybe_string = !is_numeric_expr(ctx, index); let a = lower_expr(ctx, array)?; let key = lower_expr(ctx, index)?; let val = lower_expr(ctx, value)?; let blk = ctx.block(); let handle = unbox_to_i64(blk, &a); - let result = blk.call( - DOUBLE, - "js_typed_array_index_set_dynamic", - &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], - ); + let result = if key_maybe_string { + blk.call_void( + "js_object_set_index_polymorphic", + &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], + ); + val.clone() + } else { + blk.call( + DOUBLE, + "js_typed_array_index_set_dynamic", + &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], + ) + }; if ctx.discard_expr_value { return Ok(double_literal(0.0)); } diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index 6358419b1..78455e9b7 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -1071,7 +1071,24 @@ fn lower_assignment_target( // typed `Buffer` must route through the byte-write path. if let Expr::LocalGet(id) = &*object { if let Some((_, _, ty)) = ctx.locals.iter().find(|(_, lid, _)| lid == id) { - if matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") { + // Numeric keys only — a STRING key stores an own + // property on the Buffer (Node's Buffer is an + // ordinary Uint8Array object), and an own key + // shadows the same-named prototype method. Folding + // it to the byte-write path silently dropped the + // store (see the mirrored comment in IndexGet). + let key_is_string = matches!(index.as_ref(), Expr::String(_)) + || matches!( + index.as_ref(), + Expr::LocalGet(kid) if ctx + .locals + .iter() + .find(|(_, lid, _)| lid == kid) + .is_some_and(|(_, _, kty)| matches!(kty, Type::String)) + ); + if !key_is_string + && matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") + { return Ok(wrap_assign_object_prelude( prelude.take(), Expr::Uint8ArraySet { diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs index f8e8ad701..c44fbc584 100644 --- a/crates/perry-hir/src/lower/expr_member/member_tail.rs +++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs @@ -738,7 +738,29 @@ pub(crate) fn lower_member_tail( // would return NaN-boxed pointer bits as a denormal f64. if let Expr::LocalGet(id) = &*object { if let Some((_, _, ty)) = ctx.locals.iter().find(|(_, lid, _)| lid == id) { - if matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") { + // …but ONLY for a numeric key. A Buffer is an ordinary + // object in Node (a Uint8Array), so a STRING key reads a + // property, not a byte: `buf["writeInt8"]` is the method, + // `buf[k] = v` (k non-numeric) an expando. Folding those to + // the byte path returned `undefined` (an out-of-range byte + // read), which broke the ubiquitous feature-probe idiom + // `typeof obj[k] === "function"` — mysql2's `MockBuffer` + // uses it to neutralize the write methods of a zero-length + // Buffer while sizing a packet, so every outgoing MySQL + // packet was measured against a live (empty) buffer and the + // handshake died with RangeError [ERR_OUT_OF_RANGE]. + let key_is_string = matches!(index.as_ref(), Expr::String(_)) + || matches!( + index.as_ref(), + Expr::LocalGet(kid) if ctx + .locals + .iter() + .find(|(_, lid, _)| lid == kid) + .is_some_and(|(_, _, kty)| matches!(kty, Type::String)) + ); + if !key_is_string + && matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") + { return Ok(Expr::Uint8ArrayGet { array: object, index, diff --git a/crates/perry-runtime/src/buffer/header.rs b/crates/perry-runtime/src/buffer/header.rs index 905a05e60..bf05c780a 100644 --- a/crates/perry-runtime/src/buffer/header.rs +++ b/crates/perry-runtime/src/buffer/header.rs @@ -202,6 +202,14 @@ pub(crate) fn test_shared_array_buffer_registry_len() -> usize { /// Register a buffer pointer in the thread-local registry pub fn register_buffer(ptr: *const BufferHeader) { + // A FRESH buffer must not inherit the own properties of a dead one that + // happened to sit at the same address (the own-prop table is address-keyed + // and buffer storage is recycled). mysql2 measures a packet against a + // zero-length Buffer whose write methods it overrode with no-ops, then + // allocates the real packet buffer — which lands on the freed mock's + // address, and without this the no-ops would carry over and the real packet + // would serialize as all zeros (the MySQL server then times out reading it). + super::own_props::clear_buffer_own_props(ptr as usize); BUFFER_REGISTRY.with(|r| r.borrow_mut().insert(ptr as usize)); } diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs index 4c53e45ec..4d13a4407 100644 --- a/crates/perry-runtime/src/buffer/mod.rs +++ b/crates/perry-runtime/src/buffer/mod.rs @@ -21,6 +21,7 @@ mod header; mod iter; mod mutate; mod numeric; +mod own_props; mod query; mod transcode; mod u8_codec; @@ -53,6 +54,10 @@ pub(crate) use header::{test_data_view_registry_len, test_shared_array_buffer_re // part of the public surface. pub use detach::is_detached_buffer; pub(crate) use detach::{array_buffer_transfer, detach_array_buffer}; +pub use own_props::{ + buffer_get_own_prop, buffer_has_own_prop, buffer_set_own_prop, clear_buffer_own_props, + scan_buffer_own_props_roots_mut, +}; // ---- Re-exports: Buffer.from / alloc / concat (FFI) ---- pub use from::{ diff --git a/crates/perry-runtime/src/buffer/own_props.rs b/crates/perry-runtime/src/buffer/own_props.rs new file mode 100644 index 000000000..d43a07679 --- /dev/null +++ b/crates/perry-runtime/src/buffer/own_props.rs @@ -0,0 +1,107 @@ +//! Own (dynamic) properties assigned onto a `Buffer` / typed-array value. +//! +//! Perry allocates buffers as raw `BufferHeader`s outside the object model, so +//! a plain `buf.foo = v` had nowhere to go: the set was dropped and the read +//! returned `undefined`. Node's Buffer is a `Uint8Array` — an ordinary object — +//! so user code freely stores properties on one, and it also *shadows* the +//! prototype's methods when the key collides. +//! +//! mysql2 sizes every outgoing packet with exactly that idiom +//! (`packets/packet.js` → `MockBuffer`): +//! +//! ```js +//! const noop = function () {}; +//! const mock = Buffer.alloc(0); +//! for (const k in Packet.prototype) +//! if (typeof mock[k] === "function") mock[k] = noop; // neutralize writes +//! // …serialize once against `mock` to MEASURE, then for real against +//! // Buffer.alloc(mock.offset) +//! ``` +//! +//! Without own-prop storage the no-ops never landed, the measuring pass wrote +//! into the zero-length Buffer, and the MySQL handshake died with +//! `RangeError [ERR_OUT_OF_RANGE]`. +//! +//! Mirrors `closure::dynamic_props` (same locked side table + GC root scanner +//! contract): values are traced in EVERY phase so a stored closure/array stays +//! reachable, and the owner key is rewritten on evacuation. + +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +type BufferProps = HashMap>; + +fn buffer_props() -> &'static Mutex { + static PROPS: OnceLock> = OnceLock::new(); + PROPS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Store `buf. = value`. Only reached for a registered buffer address. +pub fn buffer_set_own_prop(addr: usize, prop: &str, value: f64) { + if addr == 0 { + return; + } + if let Ok(mut props) = buffer_props().lock() { + props + .entry(addr) + .or_default() + .insert(prop.to_string(), value.to_bits()); + } +} + +/// Read an own dynamic prop, or `None` when the buffer has no such key. +pub fn buffer_get_own_prop(addr: usize, prop: &str) -> Option { + if addr == 0 { + return None; + } + buffer_props() + .lock() + .ok() + .and_then(|props| props.get(&addr).and_then(|m| m.get(prop)).copied()) + .map(f64::from_bits) +} + +/// Whether the buffer carries any own dynamic prop under `prop`. +pub fn buffer_has_own_prop(addr: usize, prop: &str) -> bool { + buffer_get_own_prop(addr, prop).is_some() +} + +/// GC: trace stored values in every phase (a stored closure is reachable ONLY +/// through this table) and rewrite the owner address when the buffer moves. +pub fn scan_buffer_own_props_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let owners = buffer_props() + .lock() + .ok() + .map(|props| props.keys().copied().collect::>()) + .unwrap_or_default(); + for owner in owners { + let Some(mut entries) = buffer_props() + .lock() + .ok() + .and_then(|mut props| props.remove(&owner)) + else { + continue; + }; + let mut new_owner = owner; + visitor.visit_metadata_usize_slot(&mut new_owner); + for bits in entries.values_mut() { + let mut v = f64::from_bits(*bits); + visitor.visit_nanbox_f64_slot(&mut v); + *bits = v.to_bits(); + } + if let Ok(mut props) = buffer_props().lock() { + props.insert(new_owner, entries); + } + } +} + +/// Drop every own prop recorded for `addr`. Called when a buffer is registered +/// (freed storage is recycled, and the table is address-keyed). +pub fn clear_buffer_own_props(addr: usize) { + if addr == 0 { + return; + } + if let Ok(mut props) = buffer_props().lock() { + props.remove(&addr); + } +} diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 77975d3e4..83fa061f3 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -475,6 +475,7 @@ pub fn gc_init() { // captured young values or future cache hits miss on stale addresses. gc_register_mutable_root_scanner(crate::closure::scan_singleton_closure_roots_mut); gc_register_mutable_root_scanner(crate::closure::scan_closure_dynamic_props_roots_mut); + gc_register_mutable_root_scanner(crate::buffer::scan_buffer_own_props_roots_mut); // Generic per-handle expando properties (`blob.colors = [...]` and other // arbitrary own props on native HANDLE values). Keys are stable small handle // ids; only the stored VALUES are JS references that must be traced. diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index 9d4b9da29..fba54122a 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -360,6 +360,23 @@ pub unsafe fn dispatch_buffer_method( } else { &[] }; + // An OWN property shadows the prototype method of the same name (Node's + // Buffer is an ordinary Uint8Array). mysql2's `MockBuffer` overwrites the + // write methods of a zero-length Buffer with a no-op to MEASURE a packet + // before allocating it; dispatching the native method regardless would + // write into the empty buffer and throw RangeError [ERR_OUT_OF_RANGE]. + if let Some(own) = crate::buffer::buffer_get_own_prop(addr, method_name) { + let jv = JSValue::from_bits(own.to_bits()); + if jv.is_pointer() { + let ptr = jv.as_pointer::() as usize; + if crate::closure::is_closure_ptr(ptr) { + let prev_this = crate::object::js_implicit_this_set(buf_f64); + let r = crate::closure::js_native_call_value(own, args_ptr, args_len); + crate::object::js_implicit_this_set(prev_this); + return r; + } + } + } let arg_i32 = |i: usize| -> i32 { if i < args.len() { args[i] as i32 diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 642f55341..311ef7e9b 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -110,6 +110,7 @@ pub(crate) fn is_fetch_subclass_body_method(name: &[u8]) -> bool { // ── Topical sub-modules (issue #1103: keep every file < 2000 lines) ── mod accessors; +mod buffer_own_prop; mod crypto_key; mod enumeration; mod field_ops; diff --git a/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs new file mode 100644 index 000000000..927026ef2 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs @@ -0,0 +1,42 @@ +//! Buffer own-property / method-value reads for the object-deref tail. +//! +//! Split out of `get_field_by_name_tail.rs` to keep that file under the +//! 2000-line budget (it is a single very large function). + +use super::*; + +/// Node's Buffer IS an object (a Uint8Array), so user code can store properties +/// on one — and an own key SHADOWS the prototype method of the same name. Perry +/// keeps buffers outside the object model, so both halves were missing: +/// `buf.foo = v` was dropped and `typeof buf.writeInt8` read `undefined` +/// (methods dispatched on CALL only). +/// +/// mysql2 sizes every outgoing packet with exactly that idiom (`MockBuffer`): +/// it walks `Packet.prototype`, replaces the matching write methods on a +/// ZERO-LENGTH Buffer with a no-op, serializes once to MEASURE, then allocates +/// for real. With the `typeof mock[k] === "function"` probe false, nothing was +/// replaced, the measuring pass wrote into the empty Buffer, and the MySQL +/// handshake died with RangeError [ERR_OUT_OF_RANGE]. +/// +/// Returns `None` when the key names neither an own property nor a Buffer +/// method, so the caller falls through to the rest of the property walk. +pub(super) fn buffer_own_prop_or_method( + obj: *const ObjectHeader, + key_bytes: &[u8], + key_ptr: *const u8, + key_len: usize, +) -> Option { + let name = std::str::from_utf8(key_bytes).ok()?; + if let Some(v) = crate::buffer::buffer_get_own_prop(obj as usize, name) { + return Some(JSValue::from_bits(v.to_bits())); + } + if crate::object::buffer_dispatch::is_buffer_method_name(name) { + let bound = crate::object::js_class_method_bind( + crate::value::js_nanbox_pointer(obj as i64), + key_ptr, + key_len, + ); + return Some(JSValue::from_bits(bound.to_bits())); + } + None +} diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index cfa363111..5e6966a64 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -255,6 +255,13 @@ pub(crate) fn get_field_by_name_object_tail( let b = obj as *const crate::buffer::BufferHeader; return JSValue::number(crate::buffer::js_buffer_length(b) as f64); } + // An own property on the Buffer shadows the same-named prototype + // method; both reads live in `buffer_own_prop`. + if let Some(v) = super::buffer_own_prop::buffer_own_prop_or_method( + obj, key_bytes, key_ptr, key_len, + ) { + return v; + } // ArrayBuffer.prototype `resizable` / `maxByteLength` getters. // Perry has no resizable ArrayBuffers, so `resizable` is always // false and `maxByteLength` equals `byteLength`. These live only diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index 93525f0d8..6d98bce5a 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -223,6 +223,30 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { // already does further down this function. if obj_val.is_pointer() { let addr = (obj_val.bits() & crate::value::POINTER_MASK) as usize; + // A Buffer is an ordinary Uint8Array in Node, so `"k" in buf` must see both + // the own properties user code hangs on it and the inherited + // `Buffer.prototype` methods. Perry keeps buffers outside the object model + // (a raw `BufferHeader`), so the generic pointer path below reads the header + // as an `ObjectHeader` and reported `false` for both. + if crate::buffer::is_registered_buffer(addr) { + if let Some(name) = unsafe { crate::object::metadata_key_to_string(key) } { + if crate::buffer::buffer_get_own_prop(addr, &name).is_some() + || crate::object::buffer_dispatch::is_buffer_method_name(&name) + { + return nanbox_true; + } + // A numeric key is an index into the bytes: present iff in range. + if let Ok(idx) = name.parse::() { + let len = unsafe { (*(addr as *const crate::buffer::BufferHeader)).length }; + return if idx < len as usize { + nanbox_true + } else { + nanbox_false + }; + } + return nanbox_false; + } + } if addr >= crate::value::addr_class::COMMON_HANDLE_BAND_END && crate::value::addr_class::is_handle_band(addr) { diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index b6367ac36..1709242d4 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -282,6 +282,28 @@ pub extern "C" fn js_object_set_field_by_name( } } } + // A Buffer is an ordinary object in Node (a Uint8Array), so `buf.foo = v` + // stores an own property — and an own key SHADOWS the same-named prototype + // method. Perry keeps buffers outside the object model (raw BufferHeader, + // no GcHeader), so this write used to be dropped entirely. mysql2's + // `MockBuffer` packet sizer depends on it: it replaces the write methods of + // a zero-length Buffer with a no-op, serializes once to measure, then + // allocates for real. Store into the GC-traced buffer own-prop table (the + // read side and the method-call dispatch both consult it). + if !key.is_null() && crate::buffer::is_registered_buffer(obj as usize) { + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) { + // Numeric keys are element writes (`buf[0] = 1`) — leave those + // to the index path; only NAMED props become expandos. + if name.parse::().is_err() { + crate::buffer::buffer_set_own_prop(obj as usize, name, value); + return; + } + } + } + } // #5437: a live Web Stream handle arrives here as its raw id in the // stream band (the `stream.prop = v` codegen path). React's // `renderToReadableStream` attaches its shell-ready promise as an diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 0f314cf0d..f9fc88f41 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -100,6 +100,27 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> } if crate::buffer::is_registered_buffer(raw as usize) { let Some(index) = numeric_key_i32_index(idx) else { + // A NON-numeric computed key on a Buffer (`buf[k]` where `k` is a + // method/expando name — mysql2's `MockBuffer` probes + // `typeof mock[k] === "function"` over `Packet.prototype`'s names). + // Node's Buffer is an ordinary Uint8Array, so this reads the own + // property, else the prototype method. Perry returned `undefined`, + // so the MockBuffer no-op swap never happened and the packet-sizing + // pass wrote into a zero-length Buffer (RangeError + // [ERR_OUT_OF_RANGE] at the MySQL handshake). + if let Some(name) = buffer_key_name(idx) { + if let Some(v) = crate::buffer::buffer_get_own_prop(raw as usize, &name) { + return v; + } + if crate::object::buffer_dispatch::is_buffer_method_name(&name) { + let bytes = name.as_bytes(); + return crate::object::js_class_method_bind( + crate::value::js_nanbox_pointer(raw as i64), + bytes.as_ptr(), + bytes.len(), + ); + } + } return f64::from_bits(crate::value::TAG_UNDEFINED); }; let byte_val = @@ -249,6 +270,15 @@ pub extern "C" fn js_object_set_index_polymorphic(obj_handle: i64, idx: f64, val index, value as i32, ); + return; + } + // NON-numeric computed key: an expando / method override + // (`mock[k] = noop` — mysql2's MockBuffer neutralizes the write methods + // of a zero-length Buffer to MEASURE a packet before allocating it). + // Node's Buffer is an ordinary object, so the own key shadows the + // prototype method; Perry used to drop the write entirely. + if let Some(name) = buffer_key_name(idx) { + crate::buffer::buffer_set_own_prop(raw as usize, &name, value); } return; } @@ -310,3 +340,23 @@ pub extern "C" fn js_object_set_index_polymorphic(obj_handle: i64, idx: f64, val crate::array::js_array_set_f64_extend(raw as *mut crate::array::ArrayHeader, index, value); } } + +/// A NON-numeric computed key as a Rust string (`buf["writeInt8"]`), or `None` +/// when the key isn't a string value. Used by the Buffer own-prop / method-value +/// arms above. +fn buffer_key_name(idx: f64) -> Option { + let jv = crate::value::JSValue::from_bits(idx.to_bits()); + if !jv.is_any_string() { + return None; + } + let ptr = crate::value::js_get_string_pointer_unified(idx) as *const crate::StringHeader; + if ptr.is_null() { + return None; + } + unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data, len); + Some(String::from_utf8_lossy(bytes).into_owned()) + } +} diff --git a/test-files/test_gap_buffer_own_props.ts b/test-files/test_gap_buffer_own_props.ts new file mode 100644 index 000000000..48067770e --- /dev/null +++ b/test-files/test_gap_buffer_own_props.ts @@ -0,0 +1,53 @@ +// A Buffer is an ordinary Uint8Array in Node: you can hang own properties on it, +// read `Buffer.prototype` methods as VALUES, and shadow them with own properties. +// mysql2 relies on all three (its `MockBuffer` measures a packet before allocating +// it by replacing the write methods of a zero-length Buffer with no-ops): +// +// for (const k in Buffer.prototype) +// if (typeof mock[k] === "function") mock[k] = noop; +// +// Perry stored Buffer instances as a raw BufferHeader with no property table, so +// own props vanished on write and every method-VALUE read came back undefined. + +const b: any = Buffer.alloc(8); + +// own data properties +b.myFlag = 42; +b.label = "packet"; +console.log("own number :", b.myFlag, typeof b.myFlag); +console.log("own string :", b.label); + +// a method read as a VALUE (not called) — the `typeof mock[k] === "function"` probe +console.log("method value :", typeof b.readUInt8, typeof b.writeUInt8, typeof b.slice); +console.log("missing key :", typeof b.notAMethod); + +// `in` and key enumeration still see the prototype methods +console.log("in operator :", "readUInt8" in b, "myFlag" in b); + +// calling through a value read binds `this` to the buffer +const reader = b.readUInt8; +b[0] = 0xab; +console.log("value call :", reader.call(b, 0)); + +// an own property shadows the prototype method on the dynamic dispatch path +// (a statically-provable buffer receiver with a literal method name still folds +// to the inline byte-load intrinsic, which cannot see own props — see #6405) +b.readUInt8 = function () { + return "shadowed"; +}; +const key = "readUInt8"; +console.log("dynamic key :", b[key](0)); + +// numeric indices remain byte access, never own props +b[1] = 200; +console.log("byte index :", b[0], b[1], b.length); + +// a freshly allocated Buffer must not inherit a recycled address's own props +const c: any = Buffer.alloc(4); +console.log("fresh buffer :", c.myFlag, typeof c.readUInt8, c.readUInt8(0)); + +// own props survive a write through the buffer's own methods +const d: any = Buffer.alloc(4); +d.tag = "keep"; +d.writeUInt8(7, 0); +console.log("after write :", d.tag, d[0]);