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
8 changes: 6 additions & 2 deletions crates/perry-codegen/src/expr/array_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,19 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
Expr::BufferConcat(operand) => {
let arr_box = lower_expr(ctx, operand)?;
let blk = ctx.block();
let arr_handle = unbox_to_i64(blk, &arr_box);
// #2013: `list` must be an Array — validate before treating the
// value as an ArrayHeader. Returns the (still NaN-boxed) bits,
// which `js_buffer_concat` strips itself.
let arr_handle = blk.call(I64, "js_buffer_validate_concat_list", &[(DOUBLE, &arr_box)]);
let buf_handle = blk.call(I64, "js_buffer_concat", &[(I64, &arr_handle)]);
Ok(nanbox_pointer_inline(blk, &buf_handle))
}
Expr::BufferConcatWithLength { list, total_length } => {
let arr_box = lower_expr(ctx, list)?;
let total_box = lower_expr(ctx, total_length)?;
let blk = ctx.block();
let arr_handle = unbox_to_i64(blk, &arr_box);
// #2013: validate `list` is an Array (see BufferConcat above).
let arr_handle = blk.call(I64, "js_buffer_validate_concat_list", &[(DOUBLE, &arr_box)]);
let buf_handle = blk.call(
I64,
"js_buffer_concat_with_length",
Expand Down
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/expr/env_clones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
Expr::BufferAllocUnsafe(size) => {
let size_box = lower_expr(ctx, size)?;
let blk = ctx.block();
let size_i32 = blk.fptosi(DOUBLE, &size_box, I32);
// #2013: validate `size` (number, in [0, kMaxLength]) and recover
// the truncated i32 in one runtime call instead of a bare fptosi.
let size_i32 = blk.call(I32, "js_buffer_validate_size", &[(DOUBLE, &size_box)]);
let buf_handle = blk.call(I64, "js_buffer_alloc_unsafe", &[(I32, &size_i32)]);
Ok(nanbox_pointer_inline(blk, &buf_handle))
}
Expand Down Expand Up @@ -333,7 +335,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// so downstream BUFFER_REGISTRY checks + `.length` paths
// can use it. Missing fill defaults to 0.
let size_box = lower_expr(ctx, size)?;
let size_i32 = ctx.block().fptosi(DOUBLE, &size_box, I32);
// #2013: validate `size` (number, in [0, kMaxLength]) and recover
// the truncated i32 in one runtime call instead of a bare fptosi.
let size_i32 = ctx
.block()
.call(I32, "js_buffer_validate_size", &[(DOUBLE, &size_box)]);
let buf_handle = if let Some(fill_expr) = fill {
let fill_box = lower_expr(ctx, fill_expr)?;
let enc_tag_i32 = if let Some(enc_expr) = encoding {
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
module.declare_function("js_buffer_byte_length_value", I32, &[DOUBLE, DOUBLE]);
module.declare_function("js_buffer_concat", I64, &[I64]);
module.declare_function("js_buffer_concat_with_length", I64, &[I64, DOUBLE]);
// #2013: Node argument validation for the Buffer factory methods.
module.declare_function("js_buffer_validate_size", I32, &[DOUBLE]);
module.declare_function("js_buffer_validate_concat_list", I64, &[DOUBLE]);
module.declare_function("js_buffer_copy", I32, &[I64, I64, I32, I32, I32]);
module.declare_function("js_buffer_equals", I32, &[I64, I64]);
module.declare_function("js_buffer_fill", I64, &[I64, I32]);
Expand Down
9 changes: 7 additions & 2 deletions crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,11 @@ pub(super) fn try_native_module_methods(
}));
}
"alloc" => {
let size = args.first().cloned().unwrap_or(Expr::Number(0.0));
// #2013: a missing `size` must surface Node's
// `ERR_INVALID_ARG_TYPE` (Received undefined), so
// default to `undefined` — not `0` — and let the
// runtime validator throw.
let size = args.first().cloned().unwrap_or(Expr::Undefined);
let fill = args.get(1).cloned().map(Box::new);
let encoding = args.get(2).cloned().map(Box::new);
return Ok(Ok(Expr::BufferAlloc {
Expand All @@ -472,7 +476,8 @@ pub(super) fn try_native_module_methods(
}));
}
"allocUnsafe" | "allocUnsafeSlow" => {
let size = args.first().cloned().unwrap_or(Expr::Number(0.0));
// #2013: missing `size` → Node ERR_INVALID_ARG_TYPE.
let size = args.first().cloned().unwrap_or(Expr::Undefined);
return Ok(Ok(Expr::BufferAllocUnsafe(Box::new(size))));
}
"concat" => {
Expand Down
16 changes: 11 additions & 5 deletions crates/perry-runtime/src/buffer/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ pub extern "C" fn js_buffer_alloc_unsafe(size: i32) -> *mut BufferHeader {
buf
}

fn throw_buffer_concat_invalid_arg_type(index: usize) -> ! {
fn throw_buffer_concat_invalid_arg_type(index: usize, element: f64) -> ! {
static REGISTER_TYPE_ERROR: std::sync::Once = std::sync::Once::new();
REGISTER_TYPE_ERROR.call_once(|| {
crate::object::js_register_class_extends_error(crate::error::CLASS_ID_TYPE_ERROR);
Expand All @@ -905,8 +905,10 @@ fn throw_buffer_concat_invalid_arg_type(index: usize) -> ! {
let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32);
f64::from_bits(crate::JSValue::string_ptr(ptr).bits())
};
let message =
format!("The \"list[{index}]\" argument must be an instance of Buffer or Uint8Array");
let message = format!(
"The \"list[{index}]\" argument must be an instance of Buffer or Uint8Array. Received {}",
crate::fs::validate::describe_received(element)
);
set(b"name", str_val(b"TypeError"));
set(b"code", str_val(b"ERR_INVALID_ARG_TYPE"));
set(b"message", str_val(message.as_bytes()));
Expand Down Expand Up @@ -966,9 +968,10 @@ fn js_buffer_concat_impl(

let mut actual_total_size: usize = 0;
for i in 0..len {
let raw_bits = strip_nanbox((*arr_data.add(i)).to_bits());
let element = *arr_data.add(i);
let raw_bits = strip_nanbox(element.to_bits());
if raw_bits < 0x1000 || !is_registered_buffer(raw_bits as usize) {
throw_buffer_concat_invalid_arg_type(i);
throw_buffer_concat_invalid_arg_type(i, element);
}
let buf_ptr = raw_bits as *const BufferHeader;
actual_total_size = actual_total_size.saturating_add((*buf_ptr).length as usize);
Expand Down Expand Up @@ -1014,5 +1017,8 @@ pub extern "C" fn js_buffer_concat_with_length(
arr_ptr: *const ArrayHeader,
total_length: f64,
) -> *mut BufferHeader {
// #2013: a provided `totalLength` must be a non-negative integer; Node
// throws `ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE` otherwise.
super::validate::validate_concat_length(total_length);
js_buffer_concat_impl(arr_ptr, normalize_buffer_concat_total_length(total_length))
}
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod mutate;
mod numeric;
mod query;
mod transcode;
pub mod validate;
mod view;

// ---- Re-exports: types & constants ----
Expand Down Expand Up @@ -103,6 +104,9 @@ pub use coding::{
// ---- Re-exports: transcode (FFI) ----
pub use transcode::js_buffer_transcode;

// ---- Re-exports: Node argument validation (FFI, #2013) ----
pub use validate::{js_buffer_validate_concat_list, js_buffer_validate_size};

// ---- Re-exports: iterator surface (FFI + dispatch hook) ----
pub use iter::{
dispatch_buffer_iterator_method, js_buffer_entries, js_buffer_keys, js_buffer_values,
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/buffer/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ pub extern "C" fn js_buffer_byte_length(str_ptr: *const StringHeader) -> i32 {
/// Node-style `Buffer.byteLength(value, encoding?)`.
#[no_mangle]
pub extern "C" fn js_buffer_byte_length_value(value: f64, encoding: f64) -> i32 {
// #2013: reject a non string/Buffer/ArrayBuffer/TypedArray first argument
// with `ERR_INVALID_ARG_TYPE`, matching Node.
super::validate::validate_byte_length_arg(value);
let bits = value.to_bits();
let jsval = crate::JSValue::from_bits(bits);

Expand Down
153 changes: 153 additions & 0 deletions crates/perry-runtime/src/buffer/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Node-compatible argument validation for the global `Buffer` factory
//! methods (#2013): `Buffer.alloc` / `allocUnsafe` / `allocUnsafeSlow`,
//! `Buffer.byteLength`, and `Buffer.concat`.
//!
//! Node throws synchronously on bad arguments to these functions with a
//! specific `.code` (`ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE`); Perry
//! previously coerced silently — `Buffer.alloc('x')` returned an empty
//! buffer, `Buffer.concat('x')` treated the string pointer as an array
//! header — so `assert.throws`-style tests saw "Missing expected exception"
//! once #1924 stopped masking the no-throw case.
//!
//! These helpers reuse the generic Node-error primitives in
//! [`crate::fs::validate`] (`describe_received`, `throw_type_error_with_code`,
//! `throw_range_error_with_code`, `validate_int32`) — the reusable validation
//! surface introduced for `fs` in #2035 and called out by the issue as the
//! shared home for this work.

use super::*;
use crate::value::JSValue;

/// Node's `buffer.constants.MAX_LENGTH` (2^53 - 1 on 64-bit platforms): the
/// upper bound `assertSize` enforces and reports in the `ERR_OUT_OF_RANGE`
/// message.
const MAX_LENGTH: f64 = 9_007_199_254_740_991.0;

/// Format a finite/non-finite number the way Node renders the `Received …`
/// clause of an `ERR_OUT_OF_RANGE` message.
fn format_received_number(n: f64) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n.is_sign_negative() {
"-Infinity"
} else {
"Infinity"
}
.to_string();
}
if n.fract() == 0.0 && n.abs() < 1e21 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}

/// True if `value` is a plain `Array` (a `GC_TYPE_ARRAY` heap pointer).
/// Mirrors the array detection in `fs::validate::describe_received`.
fn is_array(value: f64) -> bool {
let jv = JSValue::from_bits(value.to_bits());
if !jv.is_pointer() {
return false;
}
let ptr = jv.as_pointer::<u8>();
if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 {
return false;
}
let gc_header = unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) };
gc_header.obj_type == crate::gc::GC_TYPE_ARRAY
}

/// `Buffer.alloc(size)` / `allocUnsafe(size)` / `allocUnsafeSlow(size)` — Node
/// `assertSize`: `size` must be a number (`ERR_INVALID_ARG_TYPE`) in the range
/// `[0, kMaxLength]`, rejecting `NaN`/`Infinity`/negatives with
/// `ERR_OUT_OF_RANGE`. Non-integers are accepted (truncated toward zero, like
/// the previous `fptosi` lowering). Returns the validated size as `i32` so the
/// codegen call site can feed it straight to the allocator; diverges via
/// `js_throw` on bad input.
#[no_mangle]
pub extern "C" fn js_buffer_validate_size(value: f64) -> i32 {
let jv = JSValue::from_bits(value.to_bits());
if !crate::fs::validate::is_numeric(jv) {
let msg = format!(
"The \"size\" argument must be of type number. Received {}",
crate::fs::validate::describe_received(value)
);
crate::fs::validate::throw_type_error_with_code(&msg, "ERR_INVALID_ARG_TYPE");
}
let n = if jv.is_int32() {
jv.as_int32() as f64
} else {
jv.as_number()
};
if !(n >= 0.0 && n <= MAX_LENGTH) {
let msg = format!(
"The value of \"size\" is out of range. It must be >= 0 && <= 9007199254740991. Received {}",
format_received_number(n)
);
crate::fs::validate::throw_range_error_with_code(&msg);
}
n as i32
}

/// `Buffer.concat(list)` — Node requires `list` to be an `Array`
/// (`ERR_INVALID_ARG_TYPE`). Returns the raw (still NaN-boxed) value bits so
/// the caller can hand them straight to `js_buffer_concat[_with_length]`,
/// which strips the tag itself; diverges via `js_throw` on a non-array.
#[no_mangle]
pub extern "C" fn js_buffer_validate_concat_list(value: f64) -> i64 {
if !is_array(value) {
let msg = format!(
"The \"list\" argument must be an instance of Array. Received {}",
crate::fs::validate::describe_received(value)
);
crate::fs::validate::throw_type_error_with_code(&msg, "ERR_INVALID_ARG_TYPE");
}
value.to_bits() as i64
}

/// `Buffer.concat(list, totalLength)` — validate the optional `totalLength`.
/// `undefined` means "sum the element lengths" (no-op here); otherwise Node
/// requires an integer in `[0, kMaxLength]` (`validateInteger`). Reuses
/// `fs::validate::validate_int32`, whose type/integer/range message shapes
/// match Node's for the `length` argument.
pub(crate) fn validate_concat_length(total_length: f64) {
let jv = JSValue::from_bits(total_length.to_bits());
if jv.is_undefined() {
return;
}
crate::fs::validate::validate_int32(total_length, "length", 0, MAX_LENGTH as i64);
}

/// `Buffer.byteLength(string[, encoding])` — the first argument must be a
/// string, `Buffer`, `TypedArray`, `DataView`, or `ArrayBuffer`
/// (`SharedArrayBuffer` included); anything else throws
/// `ERR_INVALID_ARG_TYPE`. No-op on a valid value.
pub(crate) fn validate_byte_length_arg(value: f64) {
let jv = JSValue::from_bits(value.to_bits());
if jv.is_any_string() {
return;
}
let addr = {
let bits = value.to_bits();
if (bits >> 48) >= 0x7FF8 {
(bits & 0x0000_FFFF_FFFF_FFFF) as usize
} else {
bits as usize
}
};
if super::js_buffer_is_buffer(value.to_bits() as i64) == 1
|| super::is_any_array_buffer(addr)
|| super::is_uint8array_buffer(addr)
|| super::is_data_view(addr)
|| crate::typedarray::lookup_typed_array_kind(addr).is_some()
{
return;
}
let msg = format!(
"The \"string\" argument must be of type string or an instance of Buffer or ArrayBuffer. Received {}",
crate::fs::validate::describe_received(value)
);
crate::fs::validate::throw_type_error_with_code(&msg, "ERR_INVALID_ARG_TYPE");
}
59 changes: 59 additions & 0 deletions crates/perry-runtime/src/fs/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ pub fn describe_received(value: f64) -> String {
if jv.is_bool() {
return format!("type boolean ({})", jv.as_bool());
}
if jv.is_any_string() {
return format!("type string ({})", inspect_string_for_received(value));
}
if jv.is_bigint() {
return format!("type bigint ({}n)", bigint_decimal(value));
}
if is_numeric(jv) {
let n = if jv.is_int32() {
jv.as_int32() as f64
Expand All @@ -95,6 +101,59 @@ pub fn describe_received(value: f64) -> String {
"an unsupported value".to_string()
}

/// Read a JS string value (heap `StringHeader` or inline SSO) into a Rust
/// `String`. Used by `describe_received` to render a `Received type string
/// ('…')` clause.
fn read_js_string(value: f64) -> String {
let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader;
if ptr.is_null() {
return String::new();
}
unsafe {
let len = (*ptr).byte_len as usize;
let data = (ptr as *const u8).add(std::mem::size_of::<StringHeader>());
String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned()
}
}

/// Render a string the way Node's `determineSpecificType` does for the
/// `Received …` clause: single-quoted (switched to double quotes when the
/// content has a single quote but no double quote), then truncated to 25
/// characters plus `...` once the quoted form exceeds 28 characters.
fn inspect_string_for_received(value: f64) -> String {
let content = read_js_string(value);
let quote = if content.contains('\'') && !content.contains('"') {
'"'
} else {
'\''
};
let inspected = format!("{quote}{content}{quote}");
if inspected.chars().count() > 28 {
let truncated: String = inspected.chars().take(25).collect();
format!("{truncated}...")
} else {
inspected
}
}

/// Decimal rendering of a BigInt value for the `Received type bigint (…n)`
/// clause.
fn bigint_decimal(value: f64) -> String {
let ptr = (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::bigint::BigIntHeader;
if ptr.is_null() {
return "0".to_string();
}
let s = crate::bigint::js_bigint_to_string(ptr);
if s.is_null() {
return "0".to_string();
}
unsafe {
let len = (*s).byte_len as usize;
let data = (s as *const u8).add(std::mem::size_of::<StringHeader>());
String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned()
}
}

/// Throw `TypeError [ERR_INVALID_ARG_TYPE]` for a bad path argument, matching
/// Node's message shape. Diverges via `js_throw`.
pub(crate) fn throw_invalid_path_arg(arg_name: &str, value: f64) -> ! {
Expand Down
Loading