Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions changelog.d/12456-byte-view-tostring.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions crates/perry-runtime/src/object/buffer_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
98 changes: 97 additions & 1 deletion crates/perry-runtime/src/value/to_string_buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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::<BufferHeader>()
.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::<u8>() 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::<u8>() 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"
);
}
25 changes: 25 additions & 0 deletions test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts
Original file line number Diff line number Diff line change
@@ -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)));
Loading