From fea749ba2dc192c99e900503b7db83ef964dae16 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Sat, 30 May 2026 13:13:47 +0000 Subject: [PATCH] napi: report the real byteOffset from napi_get_typedarray_info/get_dataview_info --- src/jsc/array_buffer.rs | 6 - src/jsc/bindings/napi.cpp | 139 ++++++++++++++++ src/runtime/napi/napi_body.rs | 165 +++---------------- test/napi/napi-app/standalone_tests.cpp | 209 ++++++++++++++++++++++++ test/napi/napi.test.ts | 48 ++++++ 5 files changed, 422 insertions(+), 145 deletions(-) diff --git a/src/jsc/array_buffer.rs b/src/jsc/array_buffer.rs index af27bfc598f7..86285712c69c 100644 --- a/src/jsc/array_buffer.rs +++ b/src/jsc/array_buffer.rs @@ -884,12 +884,6 @@ impl TypedArrayType { TypedArrayType::TypeDataView => C::kJSTypedArrayTypeNone, } } - - // LAYERING: Zig's `toNapi` (array_buffer.zig:524) maps to - // `napi_typedarray_type`, which is defined in `bun_runtime` (a higher-tier - // crate that depends on `bun_jsc`). The conversion lives next to its target - // type as `napi_typedarray_type::from_typed_array_type` in - // `bun_runtime::napi` to avoid the dep cycle. } // ────────────────────────────────────────────────────────────────────────── diff --git a/src/jsc/bindings/napi.cpp b/src/jsc/bindings/napi.cpp index e4d7edeb0aa6..73f9abf7ee19 100644 --- a/src/jsc/bindings/napi.cpp +++ b/src/jsc/bindings/napi.cpp @@ -1786,6 +1786,145 @@ extern "C" napi_status napi_create_typedarray( NAPI_RETURN_SUCCESS(env); } +// Inverse of getTypedArrayTypeFromNAPI: maps a JSC typed-array cell type to the +// napi enum. Returns false when there is no corresponding `napi_typedarray_type` +// — i.e. for `Float16Array` (a typed array, but N-API has no value for it), for +// DataView, and for any non-typed-array type. +static bool getNAPITypeFromJSType(JSC::JSType type, napi_typedarray_type* result) +{ + switch (type) { + case JSC::JSType::Int8ArrayType: + *result = napi_int8_array; + return true; + case JSC::JSType::Uint8ArrayType: + *result = napi_uint8_array; + return true; + case JSC::JSType::Uint8ClampedArrayType: + *result = napi_uint8_clamped_array; + return true; + case JSC::JSType::Int16ArrayType: + *result = napi_int16_array; + return true; + case JSC::JSType::Uint16ArrayType: + *result = napi_uint16_array; + return true; + case JSC::JSType::Int32ArrayType: + *result = napi_int32_array; + return true; + case JSC::JSType::Uint32ArrayType: + *result = napi_uint32_array; + return true; + case JSC::JSType::Float32ArrayType: + *result = napi_float32_array; + return true; + case JSC::JSType::Float64ArrayType: + *result = napi_float64_array; + return true; + case JSC::JSType::BigInt64ArrayType: + *result = napi_bigint64_array; + return true; + case JSC::JSType::BigUint64ArrayType: + *result = napi_biguint64_array; + return true; + default: + return false; + } +} + +extern "C" napi_status napi_get_typedarray_info( + napi_env env, + napi_value typedarray, + napi_typedarray_type* type, + size_t* length, + void** data, + napi_value* arraybuffer, + size_t* byte_offset) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + NAPI_CHECK_ARG(env, typedarray); + Zig::GlobalObject* globalObject = toJS(env); + + JSC::JSArrayBufferView* view = dynamicDowncast(toJS(typedarray)); + NAPI_RETURN_EARLY_IF_FALSE(env, view, napi_invalid_arg); + + // Reject a DataView unconditionally (Node gates on `value->IsTypedArray()` before reading + // any field, so this must run even when `type == nullptr`). `isTypedArrayType` is true for + // every typed-array kind and false for `DataViewType`. + NAPI_RETURN_EARLY_IF_FALSE(env, JSC::isTypedArrayType(view->type()), napi_invalid_arg); + + // Materialize the backing ArrayBuffer *before* reading `vector()` whenever `data` or + // `arraybuffer` is requested (Node does the same). For a `FastTypedArray` (GC-managed + // storage, no ArrayBuffer yet) `possiblySharedBuffer()` copies the bytes into a + // malloc-backed buffer and repoints `m_vector`; reading `vector()` first would leave + // `data` pointing at storage that a later `.buffer` access orphans (dangling), and + // breaks `arraybuffer + byte_offset == data`. + JSC::JSArrayBuffer* jsBuffer = (arraybuffer || data) ? view->possiblySharedJSBuffer(globalObject) : nullptr; + + if (type) { + // `Float16Array` is a typed array but has no `napi_typedarray_type` value (the enum + // stops at `napi_biguint64_array`). If the caller asked for the type we can't report + // one, so fail with `napi_invalid_arg` instead of returning success with `*type` + // uninitialized. Callers that don't request the type (`type == nullptr`) still succeed. + NAPI_RETURN_EARLY_IF_FALSE(env, getNAPITypeFromJSType(view->type(), type), napi_invalid_arg); + } + if (length) { + *length = view->length(); + } + if (data) { + *data = view->vector(); + } + if (arraybuffer) { + *arraybuffer = toNapi(jsBuffer, globalObject); + } + if (byte_offset) { + // `data` is `view->vector()`, which already has the offset folded in. The N-API + // contract is that `arraybuffer` (the base) plus `byte_offset` reconstructs `data`, + // so report the view's real byteOffset here — not 0. + *byte_offset = view->byteOffset(); + } + NAPI_RETURN_SUCCESS(env); +} + +extern "C" napi_status napi_get_dataview_info( + napi_env env, + napi_value dataview, + size_t* bytelength, + void** data, + napi_value* arraybuffer, + size_t* byte_offset) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + NAPI_CHECK_ARG(env, dataview); + Zig::GlobalObject* globalObject = toJS(env); + + JSC::JSArrayBufferView* view = dynamicDowncast(toJS(dataview)); + NAPI_RETURN_EARLY_IF_FALSE(env, view, napi_object_expected); + // Node checks `IsDataView()` — reject a typed array passed here. + NAPI_RETURN_EARLY_IF_FALSE(env, view->type() == JSC::DataViewType, napi_invalid_arg); + + // A DataView is always ArrayBuffer-backed (never a FastTypedArray), so the ordering + // below doesn't matter for correctness — but keep it symmetric with + // napi_get_typedarray_info: materialize before reading `vector()`. + JSC::JSArrayBuffer* jsBuffer = (arraybuffer || data) ? view->possiblySharedJSBuffer(globalObject) : nullptr; + + if (bytelength) { + *bytelength = view->byteLength(); + } + if (data) { + *data = view->vector(); + } + if (arraybuffer) { + *arraybuffer = toNapi(jsBuffer, globalObject); + } + if (byte_offset) { + // See napi_get_typedarray_info: report the view's real byteOffset, not 0. + *byte_offset = view->byteOffset(); + } + NAPI_RETURN_SUCCESS(env); +} + namespace Zig { extern "C" napi_status napi_get_all_property_names( diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 0e2ea644930e..1168b8c0135c 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -81,11 +81,6 @@ unsafe extern "C" { ctx: *mut JSGlobalObject, object: jsc::c_api::JSObjectRef, ) -> jsc::c_api::JSValueRef; - fn JSObjectGetTypedArrayBuffer( - ctx: *mut JSGlobalObject, - object: jsc::c_api::JSObjectRef, - exception: jsc::c_api::ExceptionRef, - ) -> jsc::c_api::JSObjectRef; fn JSObjectMakeDate( ctx: *mut JSGlobalObject, argument_count: usize, @@ -346,42 +341,11 @@ pub(super) type napi_property_attributes = c_uint; // constructs or matches variants. pub(super) type napi_valuetype = u32; -#[repr(u32)] -#[derive(Copy, Clone, PartialEq, Eq)] -pub(super) enum napi_typedarray_type { - int8_array = 0, - uint8_array = 1, - uint8_clamped_array = 2, - int16_array = 3, - uint16_array = 4, - int32_array = 5, - uint32_array = 6, - float32_array = 7, - float64_array = 8, - bigint64_array = 9, - biguint64_array = 10, -} - -impl napi_typedarray_type { - pub(super) fn from_js_type(this: jsc::JSType) -> Option { - // PORT NOTE: jsc::JSType is a newtype struct with associated consts (not an enum), - // so glob-import is unavailable; match on the qualified const paths instead. - Some(match this { - jsc::JSType::Int8Array => napi_typedarray_type::int8_array, - jsc::JSType::Uint8Array => napi_typedarray_type::uint8_array, - jsc::JSType::Uint8ClampedArray => napi_typedarray_type::uint8_clamped_array, - jsc::JSType::Int16Array => napi_typedarray_type::int16_array, - jsc::JSType::Uint16Array => napi_typedarray_type::uint16_array, - jsc::JSType::Int32Array => napi_typedarray_type::int32_array, - jsc::JSType::Uint32Array => napi_typedarray_type::uint32_array, - jsc::JSType::Float32Array => napi_typedarray_type::float32_array, - jsc::JSType::Float64Array => napi_typedarray_type::float64_array, - jsc::JSType::BigInt64Array => napi_typedarray_type::bigint64_array, - jsc::JSType::BigUint64Array => napi_typedarray_type::biguint64_array, - _ => return None, - }) - } -} +// Only passed through FFI — by value into `napi_create_typedarray` and as a +// `*mut napi_typedarray_type` out-param written by C++ `napi_get_typedarray_info`. +// Rust never constructs or matches variants, so (like `napi_valuetype`) it is a +// plain `u32` rather than an enum. The real enum lives in `node_api.h`. +pub(super) type napi_typedarray_type = u32; #[repr(u32)] #[derive(Copy, Clone, PartialEq, Eq)] @@ -1422,65 +1386,17 @@ unsafe extern "C" { value: napi_value, result: *mut bool, ) -> napi_status; -} - -#[unsafe(no_mangle)] -pub(super) extern "C" fn napi_get_typedarray_info( - env_: napi_env, - typedarray_: napi_value, - maybe_type: *mut napi_typedarray_type, - maybe_length: *mut usize, - maybe_data: *mut *mut u8, - maybe_arraybuffer: *mut napi_value, - maybe_byte_offset: *mut usize, // note: this is always 0 -) -> napi_status { - bun_output::scoped_log!(napi, "napi_get_typedarray_info"); - let env = get_env!(env_); - env.check_gc(); - let typedarray = typedarray_.get(); - if typedarray.is_empty_or_undefined_or_null() { - return env.invalid_arg(); - } - let _keep = jsc::EnsureStillAlive(typedarray); - - let Some(array_buffer) = typedarray.as_array_buffer(env.to_js()) else { - return env.invalid_arg(); - }; - // SAFETY: `maybe_type` is null or a valid exclusive out-param per N-API contract. - if let Some(ty) = unsafe { maybe_type.as_mut() } { - // Zig: `array_buffer.typed_array_type.toTypedArrayType().toNapi()`. The Rust - // `ArrayBuffer.typed_array_type` field is already a `JSType`, so map it - // straight to `napi_typedarray_type`. - let Some(napi_ty) = napi_typedarray_type::from_js_type(array_buffer.typed_array_type) - else { - return env.invalid_arg(); - }; - *ty = napi_ty; - } - - // TODO: handle detached - write_out(maybe_data, array_buffer.ptr); - write_out(maybe_length, array_buffer.len); - - // SAFETY: `maybe_arraybuffer` is null or a valid exclusive out-param per N-API contract. - if let Some(arraybuffer) = unsafe { maybe_arraybuffer.as_mut() } { - arraybuffer.set( - env, - // SAFETY: `typedarray` is a live typed-array object (kept by `_keep`); FFI reads its backing buffer. - JSValue::c(unsafe { - JSObjectGetTypedArrayBuffer( - env.to_js().as_ptr(), - typedarray.as_object_ref(), - ptr::null_mut(), - ) - }), - ); - } - - // `jsc::ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` - // already had the offset applied. See . - write_out(maybe_byte_offset, 0); - env.ok() + // Implemented in C++ (`src/jsc/bindings/napi.cpp`) so it can read the view's + // real `byteOffset` directly via `JSArrayBufferView::byteOffset()`. + pub(super) fn napi_get_typedarray_info( + env: napi_env, + typedarray: napi_value, + type_: *mut napi_typedarray_type, + length: *mut usize, + data: *mut *mut u8, + arraybuffer: *mut napi_value, + byte_offset: *mut usize, + ) -> napi_status; } unsafe extern "C" { @@ -1491,6 +1407,16 @@ unsafe extern "C" { byte_offset: usize, result: *mut napi_value, ) -> napi_status; + // Implemented in C++ (`src/jsc/bindings/napi.cpp`), alongside + // `napi_get_typedarray_info`. + pub(super) fn napi_get_dataview_info( + env: napi_env, + dataview: napi_value, + bytelength: *mut usize, + data: *mut *mut u8, + arraybuffer: *mut napi_value, + byte_offset: *mut usize, + ) -> napi_status; } #[unsafe(no_mangle)] @@ -1508,45 +1434,6 @@ pub(super) extern "C" fn napi_is_dataview( env.ok() } -#[unsafe(no_mangle)] -pub(super) extern "C" fn napi_get_dataview_info( - env_: napi_env, - dataview_: napi_value, - maybe_bytelength: *mut usize, - maybe_data: *mut *mut u8, - maybe_arraybuffer: *mut napi_value, - maybe_byte_offset: *mut usize, // note: this is always 0 -) -> napi_status { - bun_output::scoped_log!(napi, "napi_get_dataview_info"); - let env = get_env!(env_); - env.check_gc(); - let dataview = dataview_.get(); - let Some(array_buffer) = dataview.as_array_buffer(env.to_js()) else { - return NapiEnv::set_last_error(Some(env), NapiStatus::object_expected); - }; - write_out(maybe_bytelength, array_buffer.byte_len); - write_out(maybe_data, array_buffer.ptr); - // SAFETY: `maybe_arraybuffer` is null or a valid exclusive out-param per N-API contract. - if let Some(arraybuffer) = unsafe { maybe_arraybuffer.as_mut() } { - arraybuffer.set( - env, - // SAFETY: `dataview` is a live DataView object (held in handle scope); FFI reads its backing buffer. - JSValue::c(unsafe { - JSObjectGetTypedArrayBuffer( - env.to_js().as_ptr(), - dataview.as_object_ref(), - ptr::null_mut(), - ) - }), - ); - } - // `jsc::ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` - // already had the offset applied. See . - write_out(maybe_byte_offset, 0); - - env.ok() -} - #[unsafe(no_mangle)] pub(super) extern "C" fn napi_get_version(env_: napi_env, result_: *mut u32) -> napi_status { bun_output::scoped_log!(napi, "napi_get_version"); diff --git a/test/napi/napi-app/standalone_tests.cpp b/test/napi/napi-app/standalone_tests.cpp index 23050e9d2fc5..669262495bf6 100644 --- a/test/napi/napi-app/standalone_tests.cpp +++ b/test/napi/napi-app/standalone_tests.cpp @@ -2351,6 +2351,213 @@ static napi_value test_napi_create_tsfn_async_context_frame(const Napi::Callback return env.Undefined(); } +// napi_get_typedarray_info / napi_get_dataview_info must report the view's real +// byteOffset (not 0), so that arraybuffer-base + byte_offset == data. +static void check_view_byte_offset(napi_env env, const char *kind, + napi_value view, size_t expected_offset, + uint8_t expected_first_byte) { + size_t length = 0; + uint8_t *data = nullptr; + napi_value arraybuffer; + size_t byte_offset = 0xdeadbeef; + napi_status status; + if (std::strcmp(kind, "typedarray") == 0) { + napi_typedarray_type type; + status = napi_get_typedarray_info(env, view, &type, &length, (void **)&data, + &arraybuffer, &byte_offset); + } else { + status = napi_get_dataview_info(env, view, &length, (void **)&data, + &arraybuffer, &byte_offset); + } + if (status != napi_ok) { + printf("FAIL: %s napi_get_*_info status=%d\n", kind, status); + return; + } + + uint8_t *ab_base = nullptr; + size_t ab_len = 0; + status = napi_get_arraybuffer_info(env, arraybuffer, (void **)&ab_base, + &ab_len); + if (status != napi_ok) { + printf("FAIL: %s napi_get_arraybuffer_info status=%d\n", kind, status); + return; + } + + if (byte_offset != expected_offset) { + printf("FAIL: %s byte_offset=%zu (expected %zu)\n", kind, byte_offset, + expected_offset); + return; + } + if (ab_base + byte_offset != data) { + printf("FAIL: %s arraybuffer-base + byte_offset != data\n", kind); + return; + } + if (data[0] != expected_first_byte || + ab_base[byte_offset] != expected_first_byte) { + printf("FAIL: %s reconstruction reads wrong byte (got %u/%u, expected %u)\n", + kind, data[0], ab_base[byte_offset], expected_first_byte); + return; + } + printf("PASS: %s byte_offset=%zu data[0]=%u length=%zu\n", kind, byte_offset, + data[0], length); +} + +static napi_value +test_napi_typedarray_dataview_byte_offset(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + uint8_t *bytes; + napi_value ab; + NODE_API_CALL(env, napi_create_arraybuffer(env, 16, (void **)&bytes, &ab)); + for (int i = 0; i < 16; i++) + bytes[i] = (uint8_t)(i + 1); + + size_t offsets[] = {0, 4, 8}; + for (size_t off : offsets) { + napi_value ta; + NODE_API_CALL(env, napi_create_typedarray(env, napi_uint8_array, 16 - off, + ab, off, &ta)); + check_view_byte_offset(env, "typedarray", ta, off, (uint8_t)(off + 1)); + + napi_value dv; + NODE_API_CALL(env, napi_create_dataview(env, 16 - off, ab, off, &dv)); + check_view_byte_offset(env, "dataview", dv, off, (uint8_t)(off + 1)); + } + + // A typed array allocated JS-side (not from an ArrayBuffer) starts in + // "fast" mode with GC-managed storage and no backing ArrayBuffer. Reading + // its backing ArrayBuffer materializes a (possibly relocated) copy, so + // `napi_get_typedarray_info` must report `data`/`arraybuffer` consistently: + // `arraybuffer-base + byte_offset == data` must still hold. + napi_value script, fast_ta; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "new Uint8Array([11, 22, 33, 44])", NAPI_AUTO_LENGTH, + &script)); + NODE_API_CALL(env, napi_run_script(env, script, &fast_ta)); + check_view_byte_offset(env, "typedarray", fast_ta, 0, 11); + + // Node rejects a mismatched view subtype: a typed array passed to + // napi_get_dataview_info, and (even with type==NULL) a DataView passed to + // napi_get_typedarray_info both return napi_invalid_arg. + napi_value ta0, dv0; + NODE_API_CALL(env, + napi_create_typedarray(env, napi_uint8_array, 16, ab, 0, &ta0)); + NODE_API_CALL(env, napi_create_dataview(env, 16, ab, 0, &dv0)); + + size_t dummy_len = 0; + void *dummy_data = nullptr; + napi_value dummy_ab; + size_t dummy_off = 0; + napi_status s; + + s = napi_get_dataview_info(env, ta0, &dummy_len, &dummy_data, &dummy_ab, + &dummy_off); + printf("%s: get_dataview_info(typedarray) status=%d\n", + s == napi_invalid_arg ? "PASS" : "FAIL", s); + + // type==NULL must still reject a DataView. + s = napi_get_typedarray_info(env, dv0, nullptr, &dummy_len, &dummy_data, + &dummy_ab, &dummy_off); + printf("%s: get_typedarray_info(dataview, type=NULL) status=%d\n", + s == napi_invalid_arg ? "PASS" : "FAIL", s); + + // A Float16Array is a typed array, but N-API has no `napi_typedarray_type` + // for it. Node still returns napi_ok (filling length/data/byte_offset) when + // `type==NULL` — only the `type` out-param has no value to report. + // `Float16Array` is only unflagged in Node >= 24; `info[1]` says whether the + // comparison runtime (Node) has it, so both runtimes take the same branch and + // `checkSameOutput` stays stable across Node versions. + bool float16_supported = + info.Length() > 1 && info[1].As().Value(); + if (!float16_supported) { + printf("SKIP: get_typedarray_info(float16) — Float16Array unavailable\n"); + } else { + napi_value f16_script, f16; + NODE_API_CALL(env, napi_create_string_utf8(env, + "new Float16Array([1, 2, 3, 4])", + NAPI_AUTO_LENGTH, &f16_script)); + NODE_API_CALL(env, napi_run_script(env, f16_script, &f16)); + + // type==NULL: a Float16Array still returns napi_ok with length/data/offset. + // (The type!=NULL case is covered by test_napi_float16_typedarray_type_rejected, + // Bun-only: Bun reports napi_invalid_arg since N-API has no Float16 enum value, + // while Node returns napi_ok with `*type` left uninitialized.) + size_t f16_len = 0; + void *f16_data = nullptr; + size_t f16_off = 0xdead; + s = napi_get_typedarray_info(env, f16, nullptr, &f16_len, &f16_data, + nullptr, &f16_off); + printf("%s: get_typedarray_info(float16, type=NULL) status=%d length=%zu " + "byte_offset=%zu\n", + (s == napi_ok && f16_len == 4 && f16_data != nullptr && f16_off == 0) + ? "PASS" + : "FAIL", + s, f16_len, f16_off); + } + + // Requesting only `data` (arraybuffer==NULL) on a fast-mode typed array must + // still return a stable pointer: materializing the backing ArrayBuffer + // afterwards (a second call, or a JS `.buffer` access) must not move the + // storage out from under the pointer we already handed back. Verify the + // `data` from the first call equals the backing ArrayBuffer base from a + // second call. + napi_value only_data_script, only_data_ta; + NODE_API_CALL(env, napi_create_string_utf8(env, "new Uint8Array([5, 6, 7, 8])", + NAPI_AUTO_LENGTH, + &only_data_script)); + NODE_API_CALL(env, napi_run_script(env, only_data_script, &only_data_ta)); + uint8_t *first_data = nullptr; + NODE_API_CALL(env, napi_get_typedarray_info(env, only_data_ta, nullptr, + nullptr, (void **)&first_data, + nullptr, nullptr)); + napi_value materialized_ab; + NODE_API_CALL(env, napi_get_typedarray_info(env, only_data_ta, nullptr, + nullptr, nullptr, + &materialized_ab, nullptr)); + uint8_t *ab_base = nullptr; + size_t ab_len = 0; + NODE_API_CALL(env, napi_get_arraybuffer_info(env, materialized_ab, + (void **)&ab_base, &ab_len)); + printf("%s: get_typedarray_info(data-only) first_data==arraybuffer-base " + "first[0]=%u\n", + (first_data != nullptr && first_data == ab_base && first_data[0] == 5) + ? "PASS" + : "FAIL", + first_data ? first_data[0] : 0); + + return env.Undefined(); +} + +// Bun-only (not compared against Node): N-API has no `napi_typedarray_type` +// value for Float16Array, so requesting the type fails with napi_invalid_arg +// rather than returning napi_ok with `*type` left uninitialized. (Node returns +// napi_ok and leaves the out-param unwritten.) +static napi_value +test_napi_float16_typedarray_type_rejected(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + napi_value f16_script, f16; + NODE_API_CALL(env, napi_create_string_utf8(env, + "new Float16Array([1, 2, 3, 4])", + NAPI_AUTO_LENGTH, &f16_script)); + NODE_API_CALL(env, napi_run_script(env, f16_script, &f16)); + + napi_typedarray_type type = (napi_typedarray_type)0x7f; + size_t length = 0; + void *data = nullptr; + size_t byte_offset = 0; + napi_status s = napi_get_typedarray_info(env, f16, &type, &length, &data, + nullptr, &byte_offset); + printf("%s: get_typedarray_info(float16, type=&out) status=%d type=%d\n", + (s == napi_invalid_arg && type == (napi_typedarray_type)0x7f) + ? "PASS" + : "FAIL", + s, (int)type); + + return env.Undefined(); +} + void register_standalone_tests(Napi::Env env, Napi::Object exports) { REGISTER_FUNCTION(env, exports, test_issue_7685); REGISTER_FUNCTION(env, exports, test_issue_11949); @@ -2383,6 +2590,8 @@ void register_standalone_tests(Napi::Env env, Napi::Object exports) { REGISTER_FUNCTION(env, exports, test_napi_freeze_seal_indexed); REGISTER_FUNCTION(env, exports, test_napi_create_external_buffer_empty); REGISTER_FUNCTION(env, exports, test_napi_empty_buffer_info); + REGISTER_FUNCTION(env, exports, test_napi_typedarray_dataview_byte_offset); + REGISTER_FUNCTION(env, exports, test_napi_float16_typedarray_type_rejected); REGISTER_FUNCTION(env, exports, napi_get_typeof); REGISTER_FUNCTION(env, exports, test_external_buffer_data_lifetime); REGISTER_FUNCTION(env, exports, test_external_arraybuffer_finalizer); diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index cdf548e5a72f..c5ed10869054 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -874,6 +874,54 @@ describe("cleanup hooks", () => { }); }); + describe("napi_get_typedarray_info / napi_get_dataview_info", () => { + it("reports the view's real byteOffset so that arraybuffer-base + byte_offset == data", async () => { + // `Float16Array` is only unflagged in Node >= 24. Pass whether the Node we + // compare against has it, so both runtimes take the same branch. + const nodeHasFloat16 = spawnSync({ + cmd: ["node", "-e", "process.stdout.write(String(typeof Float16Array === 'function'))"], + env: bunEnv, + }); + const float16 = nodeHasFloat16.stdout.toString().trim() === "true"; + + const output = await checkSameOutput("test_napi_typedarray_dataview_byte_offset", [float16]); + expect(output).toContain("PASS: typedarray byte_offset=0"); + expect(output).toContain("PASS: typedarray byte_offset=4"); + expect(output).toContain("PASS: typedarray byte_offset=8"); + expect(output).toContain("PASS: dataview byte_offset=0"); + expect(output).toContain("PASS: dataview byte_offset=4"); + expect(output).toContain("PASS: dataview byte_offset=8"); + // A JS-allocated (fast-mode) typed array: reading its backing ArrayBuffer + // must not leave `data` pointing at stale/relocated storage. + expect(output).toContain("PASS: typedarray byte_offset=0 data[0]=11 length=4"); + // Mismatched view subtypes are rejected with napi_invalid_arg, like Node. + expect(output).toContain("PASS: get_dataview_info(typedarray)"); + expect(output).toContain("PASS: get_typedarray_info(dataview, type=NULL)"); + // A Float16Array has no napi_typedarray_type value. With type=NULL it still + // succeeds with length/data/byte_offset (matches Node). Skipped when the + // comparison Node predates unflagged Float16Array. + if (float16) { + expect(output).toContain("PASS: get_typedarray_info(float16, type=NULL)"); + } else { + expect(output).toContain("SKIP: get_typedarray_info(float16)"); + } + // Requesting only `data` must return a stable pointer even after the backing + // ArrayBuffer is later materialized (matches Node). + expect(output).toContain("PASS: get_typedarray_info(data-only)"); + expect(output).not.toContain("FAIL"); + }); + + // Bun-only (intentionally diverges from Node): requesting the `type` of a + // Float16Array fails with napi_invalid_arg because N-API has no + // napi_typedarray_type value for it. Node instead returns napi_ok and leaves + // the out-param uninitialized. + it("rejects a Float16Array with napi_invalid_arg when type is requested", async () => { + const output = await runOn(bunExe(), "test_napi_float16_typedarray_type_rejected", []); + expect(output).toContain("PASS: get_typedarray_info(float16, type=&out) status=1 type=127"); + expect(output).not.toContain("FAIL"); + }); + }); + describe("napi_typeof", () => { it("should handle empty/invalid values", async () => { const output = await checkSameOutput("test_napi_typeof_empty_value", []);