diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index fba54122af..cda5b577ab 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -1100,7 +1100,7 @@ pub unsafe fn dispatch_buffer_method( // non-callable callback) nor iterating. Delegate any method the Buffer // API doesn't claim to the shared uint8 typed-array dispatcher (it // validates callbacks and reads the typed store); it returns `None` for - // names it doesn't implement, preserving the `undefined` fallback. + // names it doesn't implement. _ => { if super::typed_array_proto_thunks::is_typed_array_buffer(addr) { if let Some(r) = super::typed_array_proto_thunks::dispatch_uint8_buffer_method( @@ -1111,7 +1111,26 @@ pub unsafe fn dispatch_buffer_method( return r; } } - f64::from_bits(crate::value::TAG_UNDEFINED) + // Internal perry hooks (`using` disposal probes and friends) may + // reach any receiver; they are duck-typed and must stay non-throwing. + if method_name.starts_with("__perry_") { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // A method neither the Buffer API nor %TypedArray%.prototype + // implements must throw like Node (`buf.charCodeAt is not a + // function`), not silently return undefined. The silent fallback + // turned a readFileSync-now-returns-Buffer migration into invisible + // data corruption downstream: `content.charCodeAt(i)` yielded + // undefined in every comparison, so e.g. a NUL-byte binary-file + // scan misclassified every text file while the program kept + // running as if the calls had worked. Same shape as the string / + // number primitive catch-alls, which already route here. + crate::error::js_throw_type_error_not_a_function( + b"Buffer".as_ptr(), + 6, + method_name.as_ptr(), + method_name.len(), + ) } } } diff --git a/test-files/test_gap_buffer_unknown_method_throws.ts b/test-files/test_gap_buffer_unknown_method_throws.ts new file mode 100644 index 0000000000..1b2cd9dc9b --- /dev/null +++ b/test-files/test_gap_buffer_unknown_method_throws.ts @@ -0,0 +1,45 @@ +// Calling a method that neither the Buffer API nor %TypedArray%.prototype +// implements must throw a TypeError like Node — not silently return undefined. +// +// Pre-fix, `dispatch_buffer_method`'s catch-all returned undefined for any +// unknown name. That silence turned the readFileSync Node-parity migration +// (no encoding → Buffer, not string) into invisible data corruption for +// callers still treating the result as a string: `content.charCodeAt(i)` +// yielded undefined in every comparison and the program kept running as if +// the calls had worked (real case: an editor's NUL-byte binary-file scan +// misclassified every text file it opened). + +const buf = Buffer.from('hello', 'utf8'); + +// The implemented Buffer API surface keeps working. +console.log('toString:', buf.toString('utf8')); +console.log('indexOf:', buf.indexOf('l')); + +// Inherited %TypedArray%.prototype methods keep working via delegation. +console.log('every:', buf.every((b) => b > 0)); + +// A String.prototype method reached through a Buffer receiver throws. +try { + (buf as any).charCodeAt(0); + console.log('charCodeAt: no throw'); +} catch (e) { + console.log('charCodeAt throws TypeError:', e instanceof TypeError); + const msg = (e as Error).message; + console.log('mentions method:', msg.includes('charCodeAt')); + console.log('mentions not-a-function:', msg.includes('is not a function')); +} + +// A method that exists nowhere throws too. +try { + (buf as any).definitelyNotAMethod(1, 2); + console.log('bogus: no throw'); +} catch (e) { + console.log('bogus throws TypeError:', e instanceof TypeError); + console.log( + 'mentions not-a-function:', + (e as Error).message.includes('is not a function') + ); +} + +// The throw is catchable and execution continues normally afterwards. +console.log('still running:', buf.readUInt8(0));