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
9 changes: 9 additions & 0 deletions changelog.d/9205-buffer-isbuffer-uint8array-brand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
`Buffer.isBuffer()` now returns `false` for a plain `Uint8Array`, matching
Node. Perry stores both values in the same `BufferHeader` layout and buffer
registry, so registry membership alone could not distinguish their brands.

The public predicate now also consults the existing constructor-created
`Uint8Array` discriminator. Native APIs keep using the broader storage
predicate where Node accepts both `Buffer` and `Uint8Array` inputs. Regression
coverage exercises direct calls and `Buffer.isBuffer` used as a first-class
function.
7 changes: 4 additions & 3 deletions crates/perry-codegen/src/expr/array_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {

// -------- BufferIsBuffer --------
// `Buffer.isBuffer(x)`. Runtime returns i32 (0/1); wrap as NaN-boxed
// boolean. `js_buffer_is_buffer` already strips NaN-box tags and
// checks the BUFFER_REGISTRY, so any value type is safe to pass.
// boolean. `js_buffer_is_node_buffer` strips NaN-box tags, checks the
// shared BUFFER_REGISTRY, and excludes constructor-created Uint8Arrays,
// so any value type is safe to pass.
Expr::BufferIsBuffer(operand) => {
let v_box = lower_expr(ctx, operand)?;
let blk = ctx.block();
let v_handle = unbox_to_i64(blk, &v_box);
let i32_result = blk.call(I32, "js_buffer_is_buffer", &[(I64, &v_handle)]);
let i32_result = blk.call(I32, "js_buffer_is_node_buffer", &[(I64, &v_handle)]);
Ok(i32_bool_to_nanbox(blk, &i32_result))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ pub(crate) fn declare_third_party(module: &mut LlModule) {
module.declare_function("js_buffer_from_value", I64, &[I64, I32]);
module.declare_function("js_buffer_is_ascii", DOUBLE, &[DOUBLE]);
module.declare_function("js_buffer_is_buffer", I32, &[I64]);
module.declare_function("js_buffer_is_node_buffer", I32, &[I64]);
module.declare_function("js_buffer_is_encoding", I32, &[DOUBLE]);
module.declare_function("js_buffer_is_utf8", DOUBLE, &[DOUBLE]);
module.declare_function("js_buffer_print", VOID, &[I64]);
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ pub use from::{
// ---- Re-exports: predicates / byteLength (FFI) ----
pub use query::{
js_buffer_byte_length, js_buffer_byte_length_value, js_buffer_is_ascii, js_buffer_is_buffer,
js_buffer_is_encoding, js_buffer_is_utf8, js_native_buffer_byte_len, js_native_buffer_data_ptr,
js_value_buffer_or_typedarray_data,
js_buffer_is_encoding, js_buffer_is_node_buffer, js_buffer_is_utf8, js_native_buffer_byte_len,
js_native_buffer_data_ptr, js_value_buffer_or_typedarray_data,
};

// ---- Re-exports: toString / print / length / to-array ----
Expand Down
38 changes: 37 additions & 1 deletion crates/perry-runtime/src/buffer/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub unsafe extern "C" fn js_value_buffer_or_typedarray_data(
static KEEP_JS_VALUE_BUFFER_OR_TYPEDARRAY_DATA: unsafe extern "C" fn(f64, *mut u32) -> *const u8 =
js_value_buffer_or_typedarray_data;

/// Check if an object is a Buffer (using the buffer registry)
/// Check if an object uses the shared Buffer/Uint8Array representation.
#[no_mangle]
pub extern "C" fn js_buffer_is_buffer(ptr: i64) -> i32 {
if ptr == 0 || (ptr as u64) < 0x1000 {
Expand All @@ -86,6 +86,42 @@ pub extern "C" fn js_buffer_is_buffer(ptr: i64) -> i32 {
}
}

/// Check if an object has the Node `Buffer` brand.
///
/// Perry intentionally backs both `Buffer` and constructor-created
/// `Uint8Array` values with `BufferHeader` and registers both addresses in
/// `BUFFER_REGISTRY`. Keep [`js_buffer_is_buffer`] as the broader storage
/// predicate used by native APIs that accept either representation, and use
/// this discriminator for the public `Buffer.isBuffer()` identity check.
#[no_mangle]
pub extern "C" fn js_buffer_is_node_buffer(ptr: i64) -> i32 {
if js_buffer_is_buffer(ptr) == 0 {
return 0;
}
let addr = if ((ptr as u64) >> 48) != 0 {
(ptr as u64) & 0x0000_FFFF_FFFF_FFFF
} else {
ptr as u64
} as usize;
(!is_uint8array_buffer(addr)) as i32
}

#[cfg(test)]
mod buffer_brand_tests {
use super::*;

#[test]
fn node_buffer_brand_excludes_constructor_uint8arrays() {
let buffer = buffer_alloc(4);
let uint8array = js_uint8array_alloc(4);

assert_eq!(js_buffer_is_buffer(buffer as i64), 1);
assert_eq!(js_buffer_is_node_buffer(buffer as i64), 1);
assert_eq!(js_buffer_is_buffer(uint8array as i64), 1);
assert_eq!(js_buffer_is_node_buffer(uint8array as i64), 0);
}
}

/// Check if a value is a Node Buffer encoding name.
#[no_mangle]
pub extern "C" fn js_buffer_is_encoding(value: f64) -> i32 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ pub(crate) unsafe fn nm_dispatch_buffer(ctx: &NmCtx, module_name: &str, method_n
let arr = pack_args();
ptr_to_f64(crate::buffer::js_buffer_from_array(arr) as *const u8)
}
("buffer.Buffer", "isBuffer") => {
bool_to_f64(crate::buffer::js_buffer_is_buffer(arg(0).to_bits() as i64))
}
("buffer.Buffer", "isBuffer") => bool_to_f64(crate::buffer::js_buffer_is_node_buffer(
arg(0).to_bits() as i64,
)),
("buffer.Buffer", "isEncoding") => {
bool_to_f64(crate::buffer::js_buffer_is_encoding(arg(0)))
}
Expand Down
69 changes: 69 additions & 0 deletions crates/perry/tests/issue_9179_buffer_isbuffer_uint8array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Regression coverage for #9179: `Buffer.isBuffer()` is a brand check, not a
//! test for Perry's shared Buffer/Uint8Array storage representation.

use std::path::PathBuf;
use std::process::Command;

fn perry_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_perry"))
}

#[test]
fn buffer_isbuffer_rejects_plain_uint8arrays_in_every_call_path() {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("main.ts");
let output = dir.path().join("main_bin");
std::fs::write(
&entry,
r#"
const buffer = Buffer.alloc(4);
const uint8array = new Uint8Array(4);
const plain = {};

console.log(
Buffer.isBuffer(buffer),
Buffer.isBuffer(uint8array),
Buffer.isBuffer(plain),
);

const predicate = Buffer.isBuffer;
console.log(
predicate(buffer),
predicate(uint8array),
predicate(plain),
);
"#,
)
.expect("write fixture");

let compile = Command::new(perry_bin())
.current_dir(dir.path())
.arg("compile")
.arg(&entry)
.arg("-o")
.arg(&output)
.arg("--no-cache")
.output()
.expect("run perry compile");
assert!(
compile.status.success(),
"perry compile failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&compile.stdout),
String::from_utf8_lossy(&compile.stderr)
);

let run = Command::new(&output)
.output()
.expect("run compiled fixture");
assert!(
run.status.success(),
"compiled fixture failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
run.status,
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"true false false\ntrue false false\n"
);
}
Loading