diff --git a/changelog.d/12456-byte-view-tostring.md b/changelog.d/12456-byte-view-tostring.md new file mode 100644 index 0000000000..de04e4dd7c --- /dev/null +++ b/changelog.d/12456-byte-view-tostring.md @@ -0,0 +1 @@ +Keep inherited `toString` behavior consistent for `ArrayBuffer`, `SharedArrayBuffer`, and `DataView` when dispatched through Buffer helpers. These views now return their object tags without interpreting or coercing encoding and range arguments, including short strings, while Buffer encoding behavior is preserved. diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index 49878fc91a..03e4fa30f2 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -575,6 +575,14 @@ pub unsafe fn dispatch_buffer_method( let s = crate::string::js_string_from_bytes(b"[object KeyObject]".as_ptr(), 18); f64::from_bits(JSValue::string_ptr(s).bits()) } + // ArrayBuffer, SharedArrayBuffer, and DataView inherit + // Object.prototype.toString. They share BufferHeader storage and + // reach this dispatcher, but their `toString` ignores every argument; + // interpreting the first argument as a Buffer encoding leaks their + // backing bytes instead of returning the object brand. + "toString" if crate::buffer::is_non_indexed_buffer_view(addr) => { + crate::object::js_object_to_string(buf_f64) + } "toString" => { let enc = if !args.is_empty() { crate::buffer::js_encoding_tag_from_value(args[0]) diff --git a/crates/perry-runtime/src/value/to_string_buffer_tests.rs b/crates/perry-runtime/src/value/to_string_buffer_tests.rs index 91ed12b6c4..5dabc2d18a 100644 --- a/crates/perry-runtime/src/value/to_string_buffer_tests.rs +++ b/crates/perry-runtime/src/value/to_string_buffer_tests.rs @@ -2,7 +2,7 @@ use super::*; -fn text(ptr: *mut crate::string::StringHeader) -> String { +fn text(ptr: *const crate::string::StringHeader) -> String { unsafe { crate::object::has_own_helpers::str_from_string_header(ptr) } .expect("string conversion returns UTF-8") .to_string() @@ -53,3 +53,99 @@ fn array_buffer_backed_values_use_their_object_tags() { "6869" ); } + +#[test] +fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { + use crate::buffer::BufferHeader; + use crate::gc::RuntimeHandleScope; + + let scope = RuntimeHandleScope::new(); + let ab = scope.root_nanbox_f64(boxed(crate::buffer::js_array_buffer_new(2))); + crate::buffer::bytes::no_gc(|scope| { + // SAFETY: this test owns the rooted byte view and no other view can + // access its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(ab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } + }); + + let sab = scope.root_nanbox_f64(boxed(crate::buffer::js_shared_array_buffer_new(2))); + crate::buffer::bytes::no_gc(|scope| { + // SAFETY: this test owns the rooted shared view and no other thread + // or view accesses its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(sab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } + }); + + let value = crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::DataView, b"hi"); + let dv_ptr = JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut(); + let dv = scope.root_nanbox_f64(boxed(dv_ptr)); + + let short_hex = scope.root_nanbox_f64(f64::from_bits( + JSValue::short_string_unchecked(b"hex").bits(), + )); + let heap_hex = scope.root_nanbox_f64(f64::from_bits( + JSValue::string_ptr(crate::string::js_string_from_bytes(b"hex".as_ptr(), 3)).bits(), + )); + + // The short encoding and the literal both must be ignored for the three + // non-indexed byte-family receivers. + for (value, expected) in [ + (&ab, "[object ArrayBuffer]"), + (&sab, "[object SharedArrayBuffer]"), + (&dv, "[object DataView]"), + ] { + for encoding in [&short_hex, &heap_hex] { + let current = value.get_nanbox_f64(); + let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; + let argument = encoding.get_nanbox_f64(); + let result = + unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; + let result = scope.root_nanbox_f64(result); + assert_eq!( + text( + crate::value::js_get_string_pointer_unified(result.get_nanbox_f64()) + as *const _ + ), + expected + ); + } + } + + // Control: Buffer.toString continues to consume the same short encoding. + let buffer = scope.root_nanbox_f64(boxed(crate::buffer::js_buffer_alloc(2, 0))); + crate::buffer::bytes::no_gc(|scope| { + // SAFETY: this test owns the rooted Buffer and no other view can + // access its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buffer.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } + }); + let current = buffer.get_nanbox_f64(); + let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; + let argument = short_hex.get_nanbox_f64(); + let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; + let result = scope.root_nanbox_f64(result); + assert_eq!( + text(crate::value::js_get_string_pointer_unified(result.get_nanbox_f64()) as *const _), + "6869" + ); +} diff --git a/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts b/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts new file mode 100644 index 0000000000..1cb7ceac33 --- /dev/null +++ b/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts @@ -0,0 +1,25 @@ +// ArrayBuffer-family toString ignores its arguments; Buffer.toString uses them. +// S() produces an SSO string at runtime rather than a heap literal. +const S = (s: string): string => s.charAt(0) + s.slice(1); + +const ab = new ArrayBuffer(1); +new Uint8Array(ab)[0] = 65; +const sab = new SharedArrayBuffer(1); +new Uint8Array(sab)[0] = 65; +const dv = new DataView(ab); +const objectWithThrowingToString = { toString() { throw new Error('must-not-coerce'); } }; + +for (const [name, value] of [['ArrayBuffer', ab], ['SharedArrayBuffer', sab], ['DataView', dv]] as const) { + const anyValue = value as any; + console.log(name, 'no-args', JSON.stringify(anyValue.toString())); + console.log(name, 'runtime-short', JSON.stringify(anyValue.toString(S('hex')))); + console.log(name, 'heap-literal', JSON.stringify(anyValue.toString('hex'))); + console.log(name, 'extra-range', JSON.stringify(anyValue.toString(S('hex'), 0, 1))); + try { console.log(name, 'throwing-arg', JSON.stringify(anyValue.toString(objectWithThrowingToString))); } + catch (error) { console.log(name, 'throwing-arg', 'THREW', (error as Error).message); } +} + +const buffer = Buffer.from([65]); +console.log('Buffer runtime-short', JSON.stringify(buffer.toString(S('hex')))); +console.log('Buffer heap-literal', JSON.stringify(buffer.toString('hex'))); +console.log('Buffer extra-range', JSON.stringify(buffer.toString('hex', 0, 1)));