From f583c3c1c5108d96edae4d6187861e33508de78a Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 19 Jul 2026 10:18:31 -0400 Subject: [PATCH 1/3] fix(napi): normalize empty view data pointers --- examples/js_dsl/mod.test.ts | 39 +++++++++++++++++++++++++++++++++++++ examples/js_dsl/mod.zig | 36 ++++++++++++++++++++++++++++++++++ src/Value.zig | 37 +++++++++++++++++++++++------------ src/js/typed_arrays.zig | 1 + 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 36b6b00..d1fa50d 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -167,6 +167,13 @@ describe("typed arrays", () => { expect(Array.from(result)).toEqual([2.5, 5.0, 7.5]); }); + it("accepts empty Uint8Array and Float64Array values", () => { + expect(mod.uint8Sum(new Uint8Array(0))).toEqual(0); + const result = mod.float64Scale(new Float64Array(0), 2.5); + expect(result).toBeInstanceOf(Float64Array); + expect(result).toHaveLength(0); + }); + it("allocUint8 allocates and fills via alloc pattern", () => { const result = mod.allocUint8(5); expect(result).toBeInstanceOf(Uint8Array); @@ -297,6 +304,38 @@ describe("mixed DSL + N-API", () => { expect(mod.dataViewInfoMatches(view, backing, 6, 4)).toBe(true); }); + it("normalizes empty and detached ArrayBuffer data", () => { + expect(mod.arrayBufferByteLength(new ArrayBuffer(0))).toEqual(0); + + const detached = new ArrayBuffer(8); + structuredClone(detached, { transfer: [detached] }); + expect(mod.arrayBufferByteLength(detached)).toEqual(0); + }); + + it("normalizes empty Buffer data", () => { + expect(mod.bufferByteLength(Buffer.alloc(0))).toEqual(0); + }); + + it("normalizes empty and detached TypedArray data", () => { + const empty = new Uint8Array(0); + expect(mod.typedArrayInfoRangeMatches(empty, 0, 0)).toBe(true); + + const backing = new ArrayBuffer(16); + const view = new Uint16Array(backing, 4, 3); + structuredClone(backing, { transfer: [backing] }); + expect(mod.typedArrayInfoRangeMatches(view, 0, 0)).toBe(true); + }); + + it("normalizes empty and detached DataView data", () => { + const empty = new DataView(new ArrayBuffer(0)); + expect(mod.dataViewInfoRangeMatches(empty, 0, 0)).toBe(true); + + const backing = new ArrayBuffer(16); + const view = new DataView(backing, 4, 6); + structuredClone(backing, { transfer: [backing] }); + expect(mod.dataViewInfoRangeMatches(view, 0, 0)).toBe(true); + }); + it("randomBytes16 uses js.io() to produce a Uint8Array", () => { const bytes = mod.randomBytes16(); expect(bytes).toBeInstanceOf(Uint8Array); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index ec9a076..4b18d51 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -350,6 +350,16 @@ pub fn nodeRelease() !String { return String.from(std.mem.span(release)); } +pub fn arrayBufferByteLength(value: Value) !Number { + const bytes = try value.toValue().getArrayBufferInfo(); + return Number.from(bytes.len); +} + +pub fn bufferByteLength(value: Value) !Number { + const bytes = try value.toValue().getBufferInfo(); + return Number.from(bytes.len); +} + pub fn typedArrayInfoMatches( value: Value, expected_arraybuffer: Value, @@ -380,6 +390,32 @@ pub fn dataViewInfoMatches( return Boolean.from(try info.arraybuffer.strictEquals(expected_arraybuffer.toValue())); } +pub fn typedArrayInfoRangeMatches( + value: Value, + expected_length: Number, + expected_byte_offset: Number, +) !Boolean { + const info = try value.toValue().getTypedarrayInfo(); + const length: usize = @intCast(expected_length.assertU32()); + const byte_offset: usize = @intCast(expected_byte_offset.assertU32()); + if (info.length != length) return Boolean.from(false); + if (info.byte_offset != byte_offset) return Boolean.from(false); + return Boolean.from(info.data.len == length * info.array_type.elementSize()); +} + +pub fn dataViewInfoRangeMatches( + value: Value, + expected_byte_length: Number, + expected_byte_offset: Number, +) !Boolean { + const info = try value.toValue().getDataviewInfo(); + const byte_length: usize = @intCast(expected_byte_length.assertU32()); + const byte_offset: usize = @intCast(expected_byte_offset.assertU32()); + if (info.byte_length != byte_length) return Boolean.from(false); + if (info.byte_offset != byte_offset) return Boolean.from(false); + return Boolean.from(info.data.len == byte_length); +} + /// Generate 16 random bytes using the DSL-managed shared std.Io instance. pub fn randomBytes16() Uint8Array { var bytes: [16]u8 = undefined; diff --git a/src/Value.zig b/src/Value.zig index a790300..39ab58d 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const c = @import("c.zig").c; const status = @import("status.zig"); const NapiError = @import("status.zig").NapiError; @@ -12,6 +13,12 @@ value: c.napi_value, const Value = @This(); +fn napiByteSlice(data: ?*anyopaque, byte_length: usize) []u8 { + if (byte_length == 0) return &.{}; + const byte_ptr: [*]u8 = @ptrCast(data.?); + return byte_ptr[0..byte_length]; +} + /// https://nodejs.org/api/n-api.html#napi_is_array pub fn isArray(self: Value) NapiError!bool { var is_array: bool = undefined; @@ -106,22 +113,22 @@ pub fn getArrayLength(self: Value) NapiError!u32 { /// https://nodejs.org/api/n-api.html#napi_get_arraybuffer_info pub fn getArrayBufferInfo(self: Value) NapiError![]u8 { - var data: [*]u8 = undefined; + var data: ?*anyopaque = undefined; var byte_length: usize = undefined; try status.check( - c.napi_get_arraybuffer_info(self.env, self.value, @ptrCast(&data), &byte_length), + c.napi_get_arraybuffer_info(self.env, self.value, &data, &byte_length), ); - return data[0..byte_length]; + return napiByteSlice(data, byte_length); } /// https://nodejs.org/api/n-api.html#napi_get_buffer_info pub fn getBufferInfo(self: Value) NapiError![]u8 { - var data: [*]u8 = undefined; + var data: ?*anyopaque = undefined; var byte_length: usize = undefined; try status.check( - c.napi_get_buffer_info(self.env, self.value, @ptrCast(&data), &byte_length), + c.napi_get_buffer_info(self.env, self.value, &data, &byte_length), ); - return data[0..byte_length]; + return napiByteSlice(data, byte_length); } /// https://nodejs.org/api/n-api.html#napi_get_prototype @@ -146,7 +153,7 @@ pub const TypedarrayInfo = struct { pub fn getTypedarrayInfo(self: Value) NapiError!TypedarrayInfo { var array_type_raw: c.napi_typedarray_type = undefined; var length: usize = undefined; - var data: [*]u8 = undefined; + var data: ?*anyopaque = undefined; var arraybuffer: c.napi_value = undefined; var byte_offset: usize = undefined; try status.check( @@ -155,7 +162,7 @@ pub fn getTypedarrayInfo(self: Value) NapiError!TypedarrayInfo { self.value, &array_type_raw, &length, - @ptrCast(&data), + &data, &arraybuffer, &byte_offset, ), @@ -164,7 +171,7 @@ pub fn getTypedarrayInfo(self: Value) NapiError!TypedarrayInfo { return .{ .array_type = array_type, .length = length, - .data = data[0 .. length * array_type.elementSize()], + .data = napiByteSlice(data, length * array_type.elementSize()), .arraybuffer = .{ .env = self.env, .value = arraybuffer, @@ -183,7 +190,7 @@ pub const DataViewInfo = struct { /// https://nodejs.org/api/n-api.html#napi_get_dataview_info pub fn getDataviewInfo(self: Value) NapiError!DataViewInfo { var byte_length: usize = undefined; - var data: [*]u8 = undefined; + var data: ?*anyopaque = undefined; var arraybuffer: c.napi_value = undefined; var byte_offset: usize = undefined; try status.check( @@ -191,14 +198,14 @@ pub fn getDataviewInfo(self: Value) NapiError!DataViewInfo { self.env, self.value, &byte_length, - @ptrCast(&data), + &data, &arraybuffer, &byte_offset, ), ); return .{ .byte_length = byte_length, - .data = data[0..byte_length], + .data = napiByteSlice(data, byte_length), .arraybuffer = .{ .env = self.env, .value = arraybuffer, @@ -563,3 +570,9 @@ pub fn objectSeal(self: Value) NapiError!void { c.napi_object_seal(self.env, self.value), ); } + +test "napiByteSlice normalizes null data for zero byte length" { + const bytes = napiByteSlice(null, 0); + + try std.testing.expectEqual(@as(usize, 0), bytes.len); +} diff --git a/src/js/typed_arrays.zig b/src/js/typed_arrays.zig index 2f1f365..87eb5cd 100644 --- a/src/js/typed_arrays.zig +++ b/src/js/typed_arrays.zig @@ -44,6 +44,7 @@ pub fn TypedArray(comptime Element: type, comptime array_type: TypedarrayType) t pub fn toSlice(self: Self) ![]Element { const info = try self.val.getTypedarrayInfo(); if (info.array_type != array_type) return error.TypeMismatch; + if (info.length == 0) return &.{}; const byte_ptr: [*]u8 = info.data.ptr; const typed_ptr: [*]Element = @ptrCast(@alignCast(byte_ptr)); return typed_ptr[0..info.length]; From 833d0702f74d3dc39d4b8e31a1d86c400bb35892 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 19 Jul 2026 14:50:26 -0400 Subject: [PATCH 2/3] test(napi): cover empty view info getters --- examples/js_dsl/mod.test.ts | 32 ++++++++++++++++++++++++++++++++ examples/js_dsl/mod.zig | 10 ++++++++++ src/Value.zig | 1 + 3 files changed, 43 insertions(+) diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 165ff44..b5623e4 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -322,6 +322,38 @@ describe("mixed DSL + N-API", () => { expect(mod.dataViewInfoMatches(view, backing, 6, 4)).toBe(true); }); + it("reports zero byte length for empty and detached ArrayBuffers", () => { + expect(mod.arrayBufferByteLength(new ArrayBuffer(0))).toEqual(0); + + const detached = new ArrayBuffer(8); + structuredClone(detached, { transfer: [detached] }); + expect(mod.arrayBufferByteLength(detached)).toEqual(0); + }); + + it("reports zero byte length for an empty Buffer", () => { + expect(mod.bufferByteLength(Buffer.alloc(0))).toEqual(0); + }); + + it("reports zero range for empty and detached TypedArrays", () => { + const empty = new Uint8Array(0); + expect(mod.typedArrayInfoMatches(empty, empty.buffer, 0, 0)).toBe(true); + + const backing = new ArrayBuffer(16); + const view = new Uint16Array(backing, 4, 3); + structuredClone(backing, { transfer: [backing] }); + expect(mod.typedArrayInfoMatches(view, backing, 0, 0)).toBe(true); + }); + + it("reports zero range for empty and detached DataViews", () => { + const empty = new DataView(new ArrayBuffer(0)); + expect(mod.dataViewInfoMatches(empty, empty.buffer, 0, 0)).toBe(true); + + const backing = new ArrayBuffer(16); + const view = new DataView(backing, 4, 6); + structuredClone(backing, { transfer: [backing] }); + expect(mod.dataViewInfoMatches(view, backing, 0, 0)).toBe(true); + }); + it("randomBytes16 uses js.io() to produce a Uint8Array", () => { const bytes = mod.randomBytes16(); expect(bytes).toBeInstanceOf(Uint8Array); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index ec9a076..9387dd9 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -350,6 +350,16 @@ pub fn nodeRelease() !String { return String.from(std.mem.span(release)); } +pub fn arrayBufferByteLength(value: Value) !Number { + const bytes = try value.toValue().getArrayBufferInfo(); + return Number.from(bytes.len); +} + +pub fn bufferByteLength(value: Value) !Number { + const bytes = try value.toValue().getBufferInfo(); + return Number.from(bytes.len); +} + pub fn typedArrayInfoMatches( value: Value, expected_arraybuffer: Value, diff --git a/src/Value.zig b/src/Value.zig index 6483f7f..7e4c669 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -15,6 +15,7 @@ const Value = @This(); fn napiByteSlice(data: ?*anyopaque, byte_length: usize) []u8 { if (byte_length == 0) return &.{}; + // The N-API zero-length case, where data may be null or arbitrary, was handled above. const byte_ptr: [*]u8 = @ptrCast(data.?); return byte_ptr[0..byte_length]; } From ffe943e66e7c2898517c344881df937ff6d2ca67 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 19 Jul 2026 15:11:26 -0400 Subject: [PATCH 3/3] refactor(napi): rename byte slice helper --- src/Value.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Value.zig b/src/Value.zig index 7e4c669..2429bcd 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -13,7 +13,7 @@ value: c.napi_value, const Value = @This(); -fn napiByteSlice(data: ?*anyopaque, byte_length: usize) []u8 { +fn byteSlice(data: ?*anyopaque, byte_length: usize) []u8 { if (byte_length == 0) return &.{}; // The N-API zero-length case, where data may be null or arbitrary, was handled above. const byte_ptr: [*]u8 = @ptrCast(data.?); @@ -119,7 +119,7 @@ pub fn getArrayBufferInfo(self: Value) NapiError![]u8 { try status.check( c.napi_get_arraybuffer_info(self.env, self.value, &data, &byte_length), ); - return napiByteSlice(data, byte_length); + return byteSlice(data, byte_length); } /// https://nodejs.org/api/n-api.html#napi_get_buffer_info @@ -129,7 +129,7 @@ pub fn getBufferInfo(self: Value) NapiError![]u8 { try status.check( c.napi_get_buffer_info(self.env, self.value, &data, &byte_length), ); - return napiByteSlice(data, byte_length); + return byteSlice(data, byte_length); } /// https://nodejs.org/api/n-api.html#napi_get_prototype @@ -177,7 +177,7 @@ pub fn getTypedarrayInfo(self: Value) TypedarrayInfoError!TypedarrayInfo { return .{ .array_type = array_type, .length = length, - .data = napiByteSlice(data, length * array_type.elementSize()), + .data = byteSlice(data, length * array_type.elementSize()), .arraybuffer = .{ .env = self.env, .value = arraybuffer, @@ -211,7 +211,7 @@ pub fn getDataviewInfo(self: Value) NapiError!DataViewInfo { ); return .{ .byte_length = byte_length, - .data = napiByteSlice(data, byte_length), + .data = byteSlice(data, byte_length), .arraybuffer = .{ .env = self.env, .value = arraybuffer, @@ -577,8 +577,8 @@ pub fn objectSeal(self: Value) NapiError!void { ); } -test "napiByteSlice normalizes null data for zero byte length" { - const bytes = napiByteSlice(null, 0); +test "byteSlice normalizes null data for zero byte length" { + const bytes = byteSlice(null, 0); try std.testing.expectEqual(@as(usize, 0), bytes.len); }