From 7ac9c1f5c831315ac17d6179d247820e234290cd Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 8 Jun 2026 14:58:15 -0700 Subject: [PATCH 01/11] zlib: remove unused zlib-sys surface and tidy native compression bindings --- Cargo.lock | 1 - src/runtime/node/node_zlib_binding.rs | 390 ++++++++++++++------------ src/runtime/node/zlib/NativeBrotli.rs | 82 +----- src/runtime/node/zlib/NativeZlib.rs | 63 +---- src/runtime/node/zlib/NativeZstd.rs | 78 +----- src/zlib/Cargo.toml | 1 - src/zlib/lib.rs | 292 +++---------------- src/zlib_sys/lib.rs | 2 - src/zlib_sys/posix.rs | 43 --- src/zlib_sys/win32.rs | 310 -------------------- 10 files changed, 286 insertions(+), 976 deletions(-) delete mode 100644 src/zlib_sys/posix.rs delete mode 100644 src/zlib_sys/win32.rs diff --git a/Cargo.lock b/Cargo.lock index 3ae3d5fb2386..982f280faa1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2269,7 +2269,6 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", - "bun_io", "bun_zlib_sys", "const_format", "enum-map", diff --git a/src/runtime/node/node_zlib_binding.rs b/src/runtime/node/node_zlib_binding.rs index 63b7c71be943..9aacbab90d85 100644 --- a/src/runtime/node/node_zlib_binding.rs +++ b/src/runtime/node/node_zlib_binding.rs @@ -72,6 +72,79 @@ impl Error { } } +/// Placeholder `WorkPoolTask.callback` for the `task` field at construction — +/// `CompressionStream::write` overwrites it before the task is ever scheduled. +/// Safe fn: coerces to the `WorkPoolTask.callback` field type at the +/// struct-init site. +pub(crate) fn unset_task_callback(_: *mut WorkPoolTask) { + unreachable!("WorkPoolTask scheduled before CompressionStream set its callback"); +} + +/// Parses the constructor `mode` argument shared by `Native{Zlib,Brotli,Zstd}`: +/// must be an integer number within the class's `NodeMode` range. +pub(crate) fn validate_mode( + global: &JSGlobalObject, + mode: JSValue, + min: u8, + max: u8, +) -> JsResult { + if !mode.is_number() { + return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); + } + let mode_double = mode.as_number(); + if mode_double % 1.0 != 0.0 { + return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); + } + let mode_int = mode_double as i64; + if mode_int < i64::from(min) || mode_int > i64::from(max) { + return Err(global.throw_range_error( + mode_int, + jsc::RangeErrorOptions { + field_name: b"mode", + min: i64::from(min), + max: i64::from(max), + msg: b"", + }, + )); + } + Ok(bun_zlib::NodeMode::from_int(mode_int as u8)) +} + +/// Validates that `value` is a `Uint32Array` view and returns it. +pub(crate) fn validate_uint32_array( + global: &JSGlobalObject, + value: JSValue, + name: &str, +) -> JsResult { + let Some(buf) = value.as_array_buffer(global) else { + return Err(global.throw_invalid_argument_type_value(name, "Uint32Array", value)); + }; + if buf.typed_array_type != jsc::JSType::Uint32Array { + return Err(global.throw_invalid_argument_type_value(name, "Uint32Array", value)); + } + Ok(buf) +} + +/// Validates the JS-owned write-result array passed to `init` (`writeResult` / +/// `writeState`): `flush_write_result` writes two u32s into it, so it must be +/// a `Uint32Array` with at least 2 elements. +pub(crate) fn validate_write_result_array( + global: &JSGlobalObject, + value: JSValue, + name: &str, +) -> JsResult<()> { + let mut buf = validate_uint32_array(global, value, name)?; + if buf.as_u32().len() < 2 { + return Err(global + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("{name} must be a Uint32Array with at least 2 elements"), + ) + .throw()); + } + Ok(()) +} + // ─── local shims (upstream-crate gaps) ──────────────────────────────────── /// Local `JSValue::toU32` shim — `bun_jsc::JSValue` doesn't expose `to_u32()` @@ -290,102 +363,143 @@ pub(crate) trait CompressionStreamImpl: Sized + Taskable + 'static { fn pending_output_get_cached(this_value: JSValue) -> Option; } -impl CompressionStream { - pub(crate) fn write( - this: &T, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let args = callframe.arguments_undef::<7>(); - let arguments = args.slice(); - - if arguments.len() != 7 { - return Err(global_this - .err( - ErrorCode::MISSING_ARGS, - format_args!("write(flush, in, in_off, in_len, out, out_off, out_len)"), - ) - .throw()); - } - - let in_off: u32; - let in_len: u32; - - let this_value = callframe.this(); +/// Validated `write`/`writeSync` arguments: +/// `(flush, in, in_off, in_len, out, out_off, out_len)`. +/// `in_buf` is `None` when the `in` argument is null (flush-only write). The +/// buffers are non-owning views kept alive by the argument `JSValue`s on the +/// caller's frame. +struct WriteArgs { + flush: u32, + in_buf: Option, + in_off: u32, + in_len: u32, + out_buf: jsc::ArrayBuffer, + out_off: u32, + out_len: u32, +} - if arguments[0].is_undefined() { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("flush value is required"), - ) - .throw()); - } - let flush: u32 = jsv_to_u32(arguments[0]); - if !flush_value_is_valid(flush) { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid flush value"), - ) - .throw()); - } +/// Shared 7-argument validation for `write` and `writeSync`; `sig` is the +/// function signature echoed in the missing-args error. +fn parse_write_args( + global_this: &JSGlobalObject, + arguments: &[JSValue], + sig: &str, +) -> JsResult { + if arguments.len() != 7 { + return Err(global_this + .err(ErrorCode::MISSING_ARGS, format_args!("{sig}")) + .throw()); + } - if arguments[1].is_null() { - // just a flush - in_len = 0; - in_off = 0; - } else { - let in_buf = match arguments[1].as_array_buffer(global_this) { - Some(b) => b, - None => { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"in\" argument must be a TypedArray or DataView"), - ) - .throw()); - } - }; - in_off = jsv_to_u32(arguments[2]); - in_len = jsv_to_u32(arguments[3]); - if in_buf.byte_len < in_off as usize + in_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "in_off + in_len ({}) exceeds input buffer length ({})", - in_off as usize + in_len as usize, - in_buf.byte_len, - ), - ) - .throw()); - } - } + if arguments[0].is_undefined() { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("flush value is required"), + ) + .throw()); + } + let flush: u32 = jsv_to_u32(arguments[0]); + if !flush_value_is_valid(flush) { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("Invalid flush value"), + ) + .throw()); + } - let Some(out_buf) = arguments[4].as_array_buffer(global_this) else { + let in_buf: Option; + let in_off: u32; + let in_len: u32; + if arguments[1].is_null() { + // just a flush + in_buf = None; + in_off = 0; + in_len = 0; + } else { + let Some(buf) = arguments[1].as_array_buffer(global_this) else { return Err(global_this .err( ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"out\" argument must be a TypedArray or DataView"), + format_args!("The \"in\" argument must be a TypedArray or DataView"), ) .throw()); }; - let out_off: u32 = jsv_to_u32(arguments[5]); - let out_len: u32 = jsv_to_u32(arguments[6]); - if out_buf.byte_len < out_off as usize + out_len as usize { + in_off = jsv_to_u32(arguments[2]); + in_len = jsv_to_u32(arguments[3]); + if buf.byte_len < in_off as usize + in_len as usize { return Err(global_this .err( ErrorCode::OUT_OF_RANGE, format_args!( - "out_off + out_len ({}) exceeds output buffer length ({})", - out_off as usize + out_len as usize, - out_buf.byte_len, + "in_off + in_len ({}) exceeds input buffer length ({})", + in_off as usize + in_len as usize, + buf.byte_len, ), ) .throw()); } - let _ = (in_off, in_len, out_off, out_len); + in_buf = Some(buf); + } + + let Some(out_buf) = arguments[4].as_array_buffer(global_this) else { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_TYPE, + format_args!("The \"out\" argument must be a TypedArray or DataView"), + ) + .throw()); + }; + let out_off: u32 = jsv_to_u32(arguments[5]); + let out_len: u32 = jsv_to_u32(arguments[6]); + if out_buf.byte_len < out_off as usize + out_len as usize { + return Err(global_this + .err( + ErrorCode::OUT_OF_RANGE, + format_args!( + "out_off + out_len ({}) exceeds output buffer length ({})", + out_off as usize + out_len as usize, + out_buf.byte_len, + ), + ) + .throw()); + } + + Ok(WriteArgs { + flush, + in_buf, + in_off, + in_len, + out_buf, + out_off, + out_len, + }) +} + +impl CompressionStream { + pub(crate) fn write( + this: &T, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + let args = callframe.arguments_undef::<7>(); + let arguments = args.slice(); + let this_value = callframe.this(); + + let WriteArgs { + flush, + in_buf: in_validated, + in_off, + in_len, + out_off, + out_len, + .. + } = parse_write_args( + global_this, + arguments, + "write(flush, in, in_off, in_len, out, out_off, out_len)", + )?; if this.write_in_progress().get() { return Err(global_this @@ -404,7 +518,7 @@ impl CompressionStream { // FastTypedArray's backing store can fail on OOM, and failing here // leaves nothing to unwind. let in_buf: jsc::ArrayBuffer; - let in_: Option<&[u8]> = if arguments[1].is_null() { + let in_: Option<&[u8]> = if in_validated.is_none() { None } else { let Some(buf) = arguments[1].as_pinned_arraybuffer(global_this) else { @@ -414,7 +528,7 @@ impl CompressionStream { Some(&in_buf.byte_slice()[in_off as usize..in_off as usize + in_len as usize]) }; let Some(mut out_buf) = arguments[4].as_pinned_arraybuffer(global_this) else { - if !arguments[1].is_null() { + if in_validated.is_some() { arguments[1].unpin_array_buffer(); } return Err(global_this.throw_out_of_memory()); @@ -578,103 +692,29 @@ impl CompressionStream { let args = callframe.arguments_undef::<7>(); let arguments = args.slice(); - if arguments.len() != 7 { - return Err(global_this - .err( - ErrorCode::MISSING_ARGS, - format_args!("writeSync(flush, in, in_off, in_len, out, out_off, out_len)"), - ) - .throw()); - } - - let in_off: u32; - let in_len: u32; - let in_: Option<&[u8]>; - - if arguments[0].is_undefined() { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("flush value is required"), - ) - .throw()); - } - let flush: u32 = jsv_to_u32(arguments[0]); - if !flush_value_is_valid(flush) { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid flush value"), - ) - .throw()); - } - - // Hoisted so `in_` can borrow it past the `else` arm (mirrors `out_buf`). - let in_buf: jsc::ArrayBuffer; - if arguments[1].is_null() { - // just a flush - in_ = None; - in_len = 0; - in_off = 0; - } else { - in_buf = match arguments[1].as_array_buffer(global_this) { - Some(b) => b, - None => { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"in\" argument must be a TypedArray or DataView"), - ) - .throw()); - } - }; - in_off = jsv_to_u32(arguments[2]); - in_len = jsv_to_u32(arguments[3]); - if in_buf.byte_len < in_off as usize + in_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "in_off + in_len ({}) exceeds input buffer length ({})", - in_off as usize + in_len as usize, - in_buf.byte_len, - ), - ) - .throw()); - } - // Bounds checked above; `byte_slice` is the safe accessor for the JS - // ArrayBuffer's backing store (rooted via `arguments[1]` on the call stack). - in_ = Some(&in_buf.byte_slice()[in_off as usize..in_off as usize + in_len as usize]); - } - - let Some(mut out_buf) = arguments[4].as_array_buffer(global_this) else { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"out\" argument must be a TypedArray or DataView"), - ) - .throw()); - }; - let out_off: u32 = jsv_to_u32(arguments[5]); - let out_len: u32 = jsv_to_u32(arguments[6]); - if out_buf.byte_len < out_off as usize + out_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "out_off + out_len ({}) exceeds output buffer length ({})", - out_off as usize + out_len as usize, - out_buf.byte_len, - ), - ) - .throw()); - } - // Bounds checked above; `byte_slice_mut` is the safe accessor for the JS - // ArrayBuffer's backing store (rooted via `arguments[4]` on the call stack). + let WriteArgs { + flush, + in_buf, + in_off, + in_len, + mut out_buf, + out_off, + out_len, + } = parse_write_args( + global_this, + arguments, + "writeSync(flush, in, in_off, in_len, out, out_off, out_len)", + )?; + + // Bounds checked in `parse_write_args`; `byte_slice`/`byte_slice_mut` + // are the safe accessors for the JS ArrayBuffers' backing stores + // (rooted via `arguments[1]`/`arguments[4]` on the call stack). + let in_: Option<&[u8]> = in_buf + .as_ref() + .map(|b| &b.byte_slice()[in_off as usize..in_off as usize + in_len as usize]); let out: Option<&mut [u8]> = Some( &mut out_buf.byte_slice_mut()[out_off as usize..out_off as usize + out_len as usize], ); - let _ = (in_off, in_len, out_off, out_len); if this.write_in_progress().get() { return Err(global_this diff --git a/src/runtime/node/zlib/NativeBrotli.rs b/src/runtime/node/zlib/NativeBrotli.rs index 9aee4a90d5dd..9b6cc7b7dfc6 100644 --- a/src/runtime/node/zlib/NativeBrotli.rs +++ b/src/runtime/node/zlib/NativeBrotli.rs @@ -53,11 +53,14 @@ mod _impl { use core::ffi::c_uint; use bun_jsc::{ - CallFrame, ErrorCode, JSGlobalObject, JSValue, JsCell, JsResult, RangeErrorOptions, - StrongOptional, WorkPoolTask, + CallFrame, ErrorCode, JSGlobalObject, JSValue, JsCell, JsResult, StrongOptional, + WorkPoolTask, }; - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive, Error}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, Error, unset_task_callback, validate_mode, + validate_uint32_array, validate_write_result_array, + }; use crate::node::util::validators; // Intrusive refcount: the handle type is `bun_ptr::IntrusiveRc`; the @@ -110,28 +113,7 @@ mod _impl { ) -> JsResult> { let arguments = callframe.arguments_undef::<1>(); - let mode = arguments.ptr[0]; - if !mode.is_number() { - return Err(global_this.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global_this.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 8 || mode_int > 9 { - return Err(global_this.throw_range_error( - mode_int, - RangeErrorOptions { - field_name: b"mode", - min: 8, - max: 9, - ..Default::default() - }, - )); - } - - let mode = bun_zlib::NodeMode::from_int(mode_int as u8); + let mode = validate_mode(global_this, arguments.ptr[0], 8, 9)?; let stream = Context { mode, ..Default::default() @@ -150,7 +132,7 @@ mod _impl { // .callback = undefined — overwritten before WorkPool::schedule() task: JsCell::new(WorkPoolTask { node: Default::default(), - callback: noop_task_callback, + callback: unset_task_callback, }), estimated_external_size: Self::external_size_for(mode), })) @@ -190,53 +172,15 @@ mod _impl { .throw()); } - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. let write_result_value = arguments.ptr[1]; - let Some(mut write_result_buf) = write_result_value.as_array_buffer(global_this) else { - return Err(global_this.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - }; - if write_result_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global_this.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - } - let write_result_slice = write_result_buf.as_u32(); - if write_result_slice.len() < 2 { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("writeResult must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global_this, write_result_value, "writeResult")?; let write_callback = validators::validate_function(global_this, "writeCallback", arguments.ptr[2])?; // Validate `params` before any native state is initialized so the // error path needs no cleanup. `as_u32` reinterprets the view's // bytes, so the element type must actually be Uint32Array. - let params_value = arguments.ptr[0]; - let Some(mut params_buf) = params_value.as_array_buffer(global_this) else { - return Err(global_this.throw_invalid_argument_type_value( - "params", - "Uint32Array", - params_value, - )); - }; - if params_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global_this.throw_invalid_argument_type_value( - "params", - "Uint32Array", - params_value, - )); - } + let mut params_buf = validate_uint32_array(global_this, arguments.ptr[0], "params")?; js::write_result_set_cached(this_value, global_this, write_result_value); @@ -607,12 +551,6 @@ mod _impl { s.as_ptr() } - /// Placeholder for `WorkPoolTask.callback` — overwritten before scheduling - /// (see `CompressionStream::write` in node_zlib_binding.rs). - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn noop_task_callback(_task: *mut WorkPoolTask) {} - crate::__compression_stream_mixin_reexports!(NativeBrotli); } // mod _impl diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index f452a45204ed..e46ddd5d8165 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -19,15 +19,12 @@ mod _impl { CallFrame, JSGlobalObject, JSValue, JsCell, JsResult, StrongOptional, WorkPoolTask, }; - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, unset_task_callback, validate_mode, + validate_write_result_array, + }; use crate::node::util::validators; - /// Placeholder for `WorkPoolTask.callback` — overwritten before scheduling - /// (see `CompressionStream::write` in node_zlib_binding.rs). - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn noop_task_callback(_task: *mut WorkPoolTask) {} - // `mod js { write_callback_*, error_callback_*, dictionary_* }` is emitted by // `__impl_compression_stream!` below (wraps `bun_jsc::codegen_cached_accessors!`). @@ -65,29 +62,9 @@ mod _impl { pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { let arguments = frame.arguments_undef::<4>(); - let mode = arguments.ptr[0]; - if !mode.is_number() { - return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 1 || mode_int > 7 { - return Err(global.throw_range_error( - mode_int, - bun_jsc::RangeErrorOptions { - field_name: b"mode", - min: 1, - max: 7, - msg: b"", - }, - )); - } - + let mode = validate_mode(global, arguments.ptr[0], 1, 7)?; let stream = Context { - mode: c::NodeMode::from_int(mode_int as u8), + mode, ..Default::default() }; Ok(Box::new(Self { @@ -103,7 +80,7 @@ mod _impl { closed: Cell::new(false), task: JsCell::new(WorkPoolTask { node: Default::default(), - callback: noop_task_callback, + callback: unset_task_callback, }), })) } @@ -138,32 +115,8 @@ mod _impl { validators::validate_int32(global, arguments.ptr[2], "memLevel", None, None)?; let strategy = validators::validate_int32(global, arguments.ptr[3], "strategy", None, None)?; - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. let write_result_value = arguments.ptr[4]; - let Some(mut write_result_buf) = write_result_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - }; - if write_result_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - } - let write_result_slice = write_result_buf.as_u32(); - if write_result_slice.len() < 2 { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("writeResult must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global, write_result_value, "writeResult")?; let write_callback = validators::validate_function(global, "writeCallback", arguments.ptr[5])?; // Bind the ArrayBuffer view to a local so the borrowed byte_slice() outlives diff --git a/src/runtime/node/zlib/NativeZstd.rs b/src/runtime/node/zlib/NativeZstd.rs index 2e6227071738..ad4aab37f28b 100644 --- a/src/runtime/node/zlib/NativeZstd.rs +++ b/src/runtime/node/zlib/NativeZstd.rs @@ -11,7 +11,10 @@ mod _impl { }; use bun_zstd::c; // `bun.c` translated-c-headers (ZSTD_* fns/consts live here) - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive, Error}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, Error, unset_task_callback, validate_mode, + validate_uint32_array, validate_write_result_array, + }; use crate::node::util::validators; // #[repr(u8)] enum shared by all native-zlib stream types. use bun_zlib::NodeMode; @@ -21,14 +24,6 @@ mod _impl { // `NativeZstdPrototype__${prop}{Get,Set}CachedValue` C++ symbols emitted by // `src/codegen/generate-classes.ts` for `values: [...]` in `zlib.classes.ts`. - /// Placeholder WorkPoolTask callback — overwritten by CompressionStream::write - /// before the task is ever scheduled. - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn unset_task_callback(_: *mut WorkPoolTask) { - unreachable!("WorkPoolTask scheduled before CompressionStream set its callback"); - } - // R-2 (host-fn re-entrancy): every JS-exposed method takes `&self`; per-field // interior mutability via `Cell` (Copy) / `JsCell` (non-Copy). The codegen // `host_fn_this` shim still passes `&mut NativeZstd` — `&mut T` auto-reborrows @@ -75,28 +70,7 @@ mod _impl { pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { let arguments = frame.arguments_as_array::<1>(); - let mode = arguments[0]; - if !mode.is_number() { - return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 10 || mode_int > 11 { - return Err(global.throw_range_error( - mode_int, - jsc::RangeErrorOptions { - field_name: b"mode", - min: 10, - max: 11, - msg: b"", - }, - )); - } - - let mode = NodeMode::from_int(mode_int as u8); + let mode = validate_mode(global, arguments[0], 10, 11)?; let stream = Context { mode, ..Default::default() @@ -158,31 +132,7 @@ mod _impl { let write_state_value = arguments[2]; let process_callback_value = arguments[3]; - let Some(mut write_state) = write_state_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "writeState", - "Uint32Array", - write_state_value, - )); - }; - if write_state.typed_array_type != jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "writeState", - "Uint32Array", - write_state_value, - )); - } - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. - let write_state_slice = write_state.as_u32(); - if write_state_slice.len() < 2 { - return Err(global - .err( - jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("writeState must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global, write_state_value, "writeState")?; js::write_result_set_cached(this_value, global, write_state_value); let write_js_callback = @@ -210,20 +160,8 @@ mod _impl { return Ok(JSValue::FALSE); } - let Some(mut params_) = init_params_array_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "initParamsArray", - "Uint32Array", - init_params_array_value, - )); - }; - if params_.typed_array_type != jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "initParamsArray", - "Uint32Array", - init_params_array_value, - )); - } + let mut params_ = + validate_uint32_array(global, init_params_array_value, "initParamsArray")?; for (i, &x) in params_.as_u32().iter().enumerate() { if x == u32::MAX { continue; diff --git a/src/zlib/Cargo.toml b/src/zlib/Cargo.toml index ab97722c782d..55b23bfbc03d 100644 --- a/src/zlib/Cargo.toml +++ b/src/zlib/Cargo.toml @@ -22,4 +22,3 @@ bun_alloc.workspace = true bun_collections.workspace = true bun_core.workspace = true bun_zlib_sys.workspace = true -bun_io.workspace = true diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index 76be3eb88710..5aeb7dd42854 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -142,187 +142,10 @@ unsafe extern "C" { pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; } -// `W: bun_io::Write` bound is applied on `read_all` (the only method that touches `context`). -pub struct ZlibReader<'a, W, const BUFFER_SIZE: usize> { - pub context: W, - pub input: &'a [u8], - pub buf: [u8; BUFFER_SIZE], - pub zlib: zStream_struct, - // allocator field dropped (global mimalloc) - pub state: ZlibReaderState, -} - pub use bun_core::compress::State; -pub type ZlibReaderState = State; pub type ZlibReaderArrayListState = State; pub type ZlibCompressorArrayListState = State; -impl<'a, W, const BUFFER_SIZE: usize> ZlibReader<'a, W, BUFFER_SIZE> { - pub fn end(&mut self) { - if self.state == ZlibReaderState::Inflating { - // SAFETY: zlib was initialized via inflateInit2_; safe to end. - unsafe { inflateEnd(&raw mut self.zlib) }; - self.state = ZlibReaderState::End; - } - } - - pub fn init(writer: W, input: &'a [u8]) -> Result, ZlibError> { - let mut zlib_reader = Box::new(Self { - context: writer, - input, - buf: [0u8; BUFFER_SIZE], - zlib: bun_core::ffi::zeroed(), - state: ZlibReaderState::Uninitialized, - }); - - zlib_reader.zlib = zStream_struct { - next_in: input.as_ptr(), - avail_in: u32::try_from(input.len()).expect("int cast"), - total_in: u32::try_from(input.len()).expect("int cast") as _, - - next_out: zlib_reader.buf.as_mut_ptr(), - avail_out: BUFFER_SIZE as uInt, - total_out: BUFFER_SIZE as _, - - err_msg: core::ptr::null(), - alloc_func: Some(zlib_mi_malloc), - free_func: Some(zlib_mi_free), - - internal_state: core::ptr::null_mut(), - user_data: (&raw mut *zlib_reader).cast::(), - - data_type: DataType::Unknown, - adler: 0, - reserved: 0, - }; - - // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { - inflateInit2_( - &raw mut zlib_reader.zlib, - 15 + 32, - zlibVersion().cast::(), - size_of::() as c_int, - ) - } { - ReturnCode::Ok => Ok(zlib_reader), - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } - } - - pub fn error_message(&self) -> Option<&[u8]> { - if !self.zlib.err_msg.is_null() { - // SAFETY: err_msg is a NUL-terminated C string from zlib (static or stream-owned). - return Some( - unsafe { bun_core::ffi::cstr(self.zlib.err_msg.cast::()) }.to_bytes(), - ); - } - None - } - - pub fn read_all(&mut self, is_done: bool) -> Result<(), bun_core::Error> - where - W: bun_io::Write, - { - while self.state == ZlibReaderState::Uninitialized - || self.state == ZlibReaderState::Inflating - { - // Before the call of inflate(), the application should ensure - // that at least one of the actions is possible, by providing - // more input and/or consuming more output, and updating the - // next_* and avail_* values accordingly. If the caller of - // inflate() does not provide both available input and available - // output space, it is possible that there will be no progress - // made. The application can consume the uncompressed output - // when it wants, for example when the output buffer is full - // (avail_out == 0), or after each call of inflate(). If inflate - // returns Z_OK and with zero avail_out, it must be called again - // after making room in the output buffer because there might be - // more output pending. - - // - Decompress more input starting at next_in and update - // next_in and avail_in accordingly. If not all input can be - // processed (because there is not enough room in the output - // buffer), then next_in and avail_in are updated accordingly, - // and processing will resume at this point for the next call - // of inflate(). - - // - Generate more output starting at next_out and update - // next_out and avail_out accordingly. inflate() provides as - // much output as possible, until there is no more input data - // or no more space in the output buffer (see below about the - // flush parameter). - - if self.zlib.avail_out == 0 { - self.context.write_all(&self.buf)?; - self.zlib.avail_out = BUFFER_SIZE as uInt; - self.zlib.next_out = self.buf.as_mut_ptr(); - } - - // Try to inflate even if avail_in is 0, as this could be a valid empty gzip stream - // SAFETY: self.zlib was initialized via inflateInit2_. - let rc = unsafe { inflate(&raw mut self.zlib, FlushValue::NoFlush) }; - self.state = ZlibReaderState::Inflating; - - match rc { - ReturnCode::StreamEnd => { - self.state = ZlibReaderState::End; - let remainder = &self.buf[0..BUFFER_SIZE - self.zlib.avail_out as usize]; - self.context.write_all(remainder)?; - self.end(); - return Ok(()); - } - ReturnCode::MemError => { - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("OutOfMemory")); - } - ReturnCode::BufError => { - // BufError with avail_in == 0 means we need more input data - if self.zlib.avail_in == 0 { - if is_done { - // Stream is truncated - we're at EOF but decoder needs more data - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - // Not at EOF - we can retry with more data - return Err(bun_core::err!("ShortRead")); - } - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - ReturnCode::StreamError - | ReturnCode::DataError - | ReturnCode::NeedDict - | ReturnCode::VersionError - | ReturnCode::ErrNo => { - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - ReturnCode::Ok => {} - } - } - Ok(()) - } -} - -impl<'a, W, const BUFFER_SIZE: usize> Drop for ZlibReader<'a, W, BUFFER_SIZE> { - fn drop(&mut self) { - self.end(); - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] pub enum ZlibError { OutOfMemory, @@ -335,7 +158,26 @@ bun_core::impl_tag_error!(ZlibError); bun_core::named_error_set!(ZlibError); -// zlib `alloc_func`/`free_func` thunks → mimalloc. Shared by `ZlibReader` and +/// Map an `inflateInit2_`/`deflateInit2_` return code to a `ZlibError`. +fn map_init_return_code(rc: ReturnCode) -> Result<(), ZlibError> { + match rc { + ReturnCode::Ok => Ok(()), + ReturnCode::MemError => Err(ZlibError::OutOfMemory), + ReturnCode::StreamError | ReturnCode::VersionError => Err(ZlibError::InvalidArgument), + _ => unreachable!(), + } +} + +/// Point the stream's output at freshly reserved tail capacity of `list`, +/// capping `avail_out` at `budget` bytes (`usize::MAX` = unbounded). +fn regrow_output_tail(zlib: &mut zStream_struct, list: &mut Vec, budget: usize) { + // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. + let (next_out, avail_out) = unsafe { list.reserve_expand_tail(budget.min(4096)) }; + zlib.next_out = next_out; + zlib.avail_out = avail_out.min(budget) as uInt; +} + +// zlib `alloc_func`/`free_func` thunks → mimalloc, used by // `ZlibCompressorArrayList`. Intentionally // `mi_malloc`, NOT `mi_calloc` (see `ZlibAllocator::alloc` for the zeroing // heap-breakdown variant used by `ZlibReaderArrayList`). @@ -431,29 +273,15 @@ impl<'a> ZlibReaderArrayList<'a> { }; // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { + map_init_return_code(unsafe { inflateInit2_( &raw mut zlib_reader.zlib, options.window_bits, zlibVersion().cast::(), size_of::() as c_int, ) - } { - ReturnCode::Ok => Ok(zlib_reader), - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } + })?; + Ok(zlib_reader) } pub fn error_message(&self) -> Option<&[u8]> { @@ -507,14 +335,8 @@ impl<'a> ZlibReaderArrayList<'a> { self.state = ZlibReaderArrayListState::Error; return Err(ZlibError::ZlibError); } - // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. - let (next_out, avail_out) = unsafe { - self.list_ptr - .reserve_expand_tail(remaining_budget.min(4096)) - }; - self.zlib.next_out = next_out; // Clamp so a single inflate call cannot write past `max_output_size`. - self.zlib.avail_out = avail_out.min(remaining_budget) as uInt; + regrow_output_tail(&mut self.zlib, self.list_ptr, remaining_budget); } // Try to inflate even if avail_in is 0, as this could be a valid empty gzip stream @@ -930,7 +752,7 @@ impl<'a> ZlibCompressorArrayList<'a> { }; // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { + map_init_return_code(unsafe { deflateInit2_( &raw mut zlib_reader.zlib, options.level, @@ -945,37 +767,22 @@ impl<'a> ZlibCompressorArrayList<'a> { zlibVersion().cast::(), size_of::() as c_int, ) - } { - ReturnCode::Ok => { - // SAFETY: zlib initialized; deflateBound returns upper bound on output. - let bound = unsafe { - deflateBound( - &raw mut zlib_reader.zlib, - uLong::try_from(input.len()).expect("int cast"), - ) - }; - // ensureTotalCapacityPrecise → reserve_exact - let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); - zlib_reader.list_ptr.reserve_exact(need); - zlib_reader.zlib.avail_out = zlib_reader.list_ptr.capacity() as uInt; - zlib_reader.zlib.next_out = zlib_reader.list_ptr.as_mut_ptr(); - - Ok(zlib_reader) - } - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } + })?; + + // SAFETY: zlib initialized; deflateBound returns upper bound on output. + let bound = unsafe { + deflateBound( + &raw mut zlib_reader.zlib, + uLong::try_from(input.len()).expect("int cast"), + ) + }; + // ensureTotalCapacityPrecise → reserve_exact + let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); + zlib_reader.list_ptr.reserve_exact(need); + zlib_reader.zlib.avail_out = zlib_reader.list_ptr.capacity() as uInt; + zlib_reader.zlib.next_out = zlib_reader.list_ptr.as_mut_ptr(); + + Ok(zlib_reader) } pub fn error_message(&self) -> Option<&[u8]> { @@ -1020,10 +827,7 @@ impl<'a> ZlibCompressorArrayList<'a> { // flush parameter). if self.zlib.avail_out == 0 { - // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. - let (next_out, avail_out) = unsafe { self.list_ptr.reserve_expand_tail(4096) }; - self.zlib.next_out = next_out; - self.zlib.avail_out = avail_out as uInt; + regrow_output_tail(&mut self.zlib, self.list_ptr, usize::MAX); } if self.zlib.avail_out == 0 { @@ -1076,14 +880,8 @@ impl<'a> Drop for ZlibCompressorArrayList<'a> { } } -// Re-export from bun_zlib_sys, platform-selected. +// Re-export from bun_zlib_sys. mod internal { - #[cfg(not(windows))] - pub(super) use bun_zlib_sys::posix::{DataType, zStream_struct}; - #[cfg(not(windows))] - pub use bun_zlib_sys::posix::{FlushValue, ReturnCode, z_stream, z_streamp}; - #[cfg(windows)] - pub(super) use bun_zlib_sys::win32::{DataType, zStream_struct}; - #[cfg(windows)] - pub use bun_zlib_sys::win32::{FlushValue, ReturnCode, z_stream, z_streamp}; + pub(super) use bun_zlib_sys::shared::{DataType, zStream_struct}; + pub use bun_zlib_sys::shared::{FlushValue, ReturnCode, z_stream, z_streamp}; } diff --git a/src/zlib_sys/lib.rs b/src/zlib_sys/lib.rs index 1ef905b360e6..bf0de1783cae 100644 --- a/src/zlib_sys/lib.rs +++ b/src/zlib_sys/lib.rs @@ -1,5 +1,3 @@ #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] #![warn(unused_must_use)] -pub mod posix; pub mod shared; -pub mod win32; diff --git a/src/zlib_sys/posix.rs b/src/zlib_sys/posix.rs deleted file mode 100644 index 57f4919febd1..000000000000 --- a/src/zlib_sys/posix.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case)] - -use core::ffi::{c_char, c_int}; - -pub use crate::shared::{ - DataType, FlushValue, ReturnCode, alloc_func, free_func, struct_internal_state, z_alloc_fn, - z_free_fn, z_stream, z_streamp, zStream_struct, -}; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; -} diff --git a/src/zlib_sys/win32.rs b/src/zlib_sys/win32.rs deleted file mode 100644 index fe7423daf777..000000000000 --- a/src/zlib_sys/win32.rs +++ /dev/null @@ -1,310 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] - -use core::ffi::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort, c_void}; - -pub use crate::shared::{ - Bytef, DataType, FlushValue, ReturnCode, alloc_func, free_func, gzFile, gzFile_s, - internal_state, struct_gzFile_s, struct_internal_state, struct_z_stream_s, uInt, uLong, uLongf, - voidpf, z_alloc_func, z_free_func, z_stream, z_stream_s, z_streamp, zStream_struct, -}; - -pub type rsize_t = usize; -pub type _ino_t = c_ushort; -pub type ino_t = _ino_t; -pub type _dev_t = c_uint; -pub type dev_t = _dev_t; -pub type _off_t = c_long; -pub type off_t = _off_t; -type z_size_t = usize; -type voidpc = *const c_void; -type voidp = *mut c_void; - -#[repr(C)] -pub struct struct_gz_header_s { - pub text: c_int, - pub time: uLong, - pub xflags: c_int, - pub os: c_int, - pub extra: *mut Bytef, - pub extra_len: uInt, - pub extra_max: uInt, - pub name: *mut Bytef, - pub name_max: uInt, - pub comment: *mut Bytef, - pub comm_max: uInt, - pub hcrc: c_int, - pub done: c_int, -} -pub type gz_header = struct_gz_header_s; -pub type gz_headerp = *mut gz_header; - -pub type in_func = Option c_uint>; -pub type out_func = Option ReturnCode>; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - pub fn deflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn deflateEnd(strm: z_streamp) -> ReturnCode; - pub fn inflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn inflateEnd(strm: z_streamp) -> ReturnCode; - pub fn deflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn deflateGetDictionary( - strm: z_streamp, - dictionary: *mut Bytef, - dictLength: *mut uInt, - ) -> ReturnCode; - pub fn deflateCopy(dest: z_streamp, source: z_streamp) -> ReturnCode; - pub fn deflateReset(strm: z_streamp) -> ReturnCode; - pub fn deflateParams(strm: z_streamp, level: c_int, strategy: c_int) -> ReturnCode; - pub fn deflateTune( - strm: z_streamp, - good_length: c_int, - max_lazy: c_int, - nice_length: c_int, - max_chain: c_int, - ) -> ReturnCode; - pub fn deflateBound(strm: z_streamp, sourceLen: uLong) -> uLong; - pub fn deflatePending(strm: z_streamp, pending: *mut c_uint, bits: *mut c_int) -> ReturnCode; - pub fn deflatePrime(strm: z_streamp, bits: c_int, value: c_int) -> ReturnCode; - pub fn deflateSetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn inflateGetDictionary( - strm: z_streamp, - dictionary: *mut Bytef, - dictLength: *mut uInt, - ) -> ReturnCode; - pub fn inflateSync(strm: z_streamp) -> ReturnCode; - pub fn inflateCopy(dest: z_streamp, source: z_streamp) -> ReturnCode; - pub fn inflateReset(strm: z_streamp) -> ReturnCode; - pub fn inflateReset2(strm: z_streamp, windowBits: c_int) -> ReturnCode; - pub fn inflatePrime(strm: z_streamp, bits: c_int, value: c_int) -> ReturnCode; - pub fn inflateMark(strm: z_streamp) -> c_long; - pub fn inflateGetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateBack( - strm: z_streamp, - in_: in_func, - in_desc: *mut c_void, - out: out_func, - out_desc: *mut c_void, - ) -> ReturnCode; - pub fn inflateBackEnd(strm: z_streamp) -> ReturnCode; - pub safe fn zlibCompileFlags() -> uLong; - pub fn compress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn compress2( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - level: c_int, - ) -> ReturnCode; - pub safe fn compressBound(sourceLen: uLong) -> uLong; - pub fn uncompress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn uncompress2( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: *mut uLong, - ) -> ReturnCode; - pub fn gzdopen(fd: c_int, mode: *const u8) -> gzFile; - pub fn gzbuffer(file: gzFile, size: c_uint) -> ReturnCode; - pub fn gzsetparams(file: gzFile, level: c_int, strategy: c_int) -> ReturnCode; - pub fn gzread(file: gzFile, buf: voidp, len: c_uint) -> ReturnCode; - pub fn gzfread(buf: voidp, size: z_size_t, nitems: z_size_t, file: gzFile) -> z_size_t; - pub fn gzwrite(file: gzFile, buf: voidpc, len: c_uint) -> ReturnCode; - pub fn gzfwrite(buf: voidpc, size: z_size_t, nitems: z_size_t, file: gzFile) -> z_size_t; - pub fn gzprintf(file: gzFile, format: *const u8, ...) -> ReturnCode; - pub fn gzputs(file: gzFile, s: *const u8) -> ReturnCode; - pub fn gzgets(file: gzFile, buf: *mut u8, len: c_int) -> *mut u8; - pub fn gzputc(file: gzFile, c: c_int) -> ReturnCode; - pub fn gzgetc(file: gzFile) -> ReturnCode; - pub fn gzungetc(c: c_int, file: gzFile) -> ReturnCode; - pub fn gzflush(file: gzFile, flush: FlushValue) -> ReturnCode; - pub fn gzrewind(file: gzFile) -> ReturnCode; - pub fn gzeof(file: gzFile) -> ReturnCode; - pub fn gzdirect(file: gzFile) -> ReturnCode; - pub fn gzclose(file: gzFile) -> ReturnCode; - pub fn gzclose_r(file: gzFile) -> ReturnCode; - pub fn gzclose_w(file: gzFile) -> ReturnCode; - pub fn gzerror(file: gzFile, errnum: *mut c_int) -> *const u8; - pub fn gzclearerr(file: gzFile); - pub fn adler32(adler: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn adler32_z(adler: uLong, buf: *const Bytef, len: z_size_t) -> uLong; - pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn crc32_z(crc: uLong, buf: *const Bytef, len: z_size_t) -> uLong; - pub safe fn crc32_combine_op(crc1: uLong, crc2: uLong, op: uLong) -> uLong; - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn gzgetc_(file: gzFile) -> ReturnCode; - pub fn gzopen(path: *const u8, mode: *const u8) -> gzFile; - pub fn gzseek(file: gzFile, offset: c_long, whence: c_int) -> c_long; - pub fn gztell(file: gzFile) -> c_long; - pub fn gzoffset(file: gzFile) -> c_long; - pub safe fn adler32_combine(a: uLong, b: uLong, len: c_long) -> uLong; - pub safe fn crc32_combine(a: uLong, b: uLong, len: c_long) -> uLong; - pub safe fn crc32_combine_gen(len: c_long) -> uLong; - pub safe fn zError(err: c_int) -> *const u8; - pub fn inflateSyncPoint(strm: z_streamp) -> ReturnCode; - // pub fn get_crc_table() -> *const z_crc_t; - pub fn inflateUndermine(strm: z_streamp, subvert: c_int) -> ReturnCode; - pub fn inflateValidate(strm: z_streamp, check: c_int) -> ReturnCode; - pub fn inflateCodesUsed(strm: z_streamp) -> c_ulong; - pub fn inflateResetKeep(strm: z_streamp) -> ReturnCode; - pub fn deflateResetKeep(strm: z_streamp) -> ReturnCode; -} - -pub type z_off_t = c_long; -pub const Z_NO_FLUSH: c_int = 0; -pub const Z_PARTIAL_FLUSH: c_int = 1; -pub const Z_SYNC_FLUSH: c_int = 2; -pub const Z_FULL_FLUSH: c_int = 3; -pub const Z_FINISH: c_int = 4; -pub const Z_BLOCK: c_int = 5; -pub const Z_TREES: c_int = 6; -pub const Z_OK: c_int = 0; -pub const Z_STREAM_END: c_int = 1; -pub const Z_NEED_DICT: c_int = 2; -pub const Z_ERRNO: c_int = -1; -pub const Z_STREAM_ERROR: c_int = -2; -pub const Z_DATA_ERROR: c_int = -3; -pub const Z_MEM_ERROR: c_int = -4; -pub const Z_BUF_ERROR: c_int = -5; -pub const Z_VERSION_ERROR: c_int = -6; -pub const Z_NO_COMPRESSION: c_int = 0; -pub const Z_BEST_SPEED: c_int = 1; -pub const Z_BEST_COMPRESSION: c_int = 9; -pub const Z_DEFAULT_COMPRESSION: c_int = -1; -pub const Z_FILTERED: c_int = 1; -pub const Z_HUFFMAN_ONLY: c_int = 2; -pub const Z_RLE: c_int = 3; -pub const Z_FIXED: c_int = 4; -pub const Z_DEFAULT_STRATEGY: c_int = 0; -pub const Z_BINARY: c_int = 0; -pub const Z_TEXT: c_int = 1; -pub const Z_ASCII: c_int = Z_TEXT; -pub const Z_UNKNOWN: c_int = 2; -pub const Z_DEFLATED: c_int = 8; -pub const Z_NULL: c_int = 0; - -#[inline] -pub unsafe fn deflate_init(strm: z_streamp, level: c_int) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer; zlib reads version/stream_size for ABI check. - unsafe { - deflateInit_( - strm, - level, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_init(strm: z_streamp) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - inflateInit_( - strm, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn deflate_init2( - strm: z_streamp, - level: c_int, - method: c_int, - window_bits: c_int, - mem_level: c_int, - strategy: c_int, -) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - deflateInit2_( - strm, - level, - method, - window_bits, - mem_level, - strategy, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_init2(strm: z_streamp, window_bits: c_int) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - inflateInit2_( - strm, - window_bits, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_back_init( - strm: z_streamp, - window_bits: c_int, - window: *mut u8, -) -> ReturnCode { - // SAFETY: caller guarantees `strm` and `window` are valid. - unsafe { - inflateBackInit_( - strm, - window_bits, - window, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} - -pub type gz_header_s = struct_gz_header_s; From 761446790e1ac3a020b055f53d877e91f3f80d55 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:11:36 +0000 Subject: [PATCH 02/11] ci: retrigger From c416281a3cd422d4624629a051f4e1be4b3639e9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:40:47 +0000 Subject: [PATCH 03/11] zlib: fix close() on never-initialized handles, reject 4 GiB inputs, test binding validation Context::close() for NativeZlib/NativeBrotli/NativeZstd now tolerates a handle whose init() never ran (failed argument validation, or init was never called): Brotli unwrapped a None state pointer, Zstd passed NULL to ZSTD_*Ctx_reset, and Zlib tripped its debug assert on deflateEnd's StreamError. GC finalization of such a handle crashed the process. ZlibReaderArrayList/ZlibCompressorArrayList init now rejects inputs whose length does not fit zlib's 32-bit avail_in instead of silently truncating (and panicking on the Windows deflateBound cast, where uLong is 32-bit). Adds native-handle tests pinning the argument validation contract of the consolidated helpers and the never-initialized lifecycle. --- src/runtime/node/zlib/NativeBrotli.rs | 7 +- src/runtime/node/zlib/NativeZlib.rs | 7 + src/runtime/node/zlib/NativeZstd.rs | 42 ++-- src/zlib/lib.rs | 29 ++- .../zlib/zlib-handle-bounds-check.test.ts | 199 ++++++++++++++++++ 5 files changed, 255 insertions(+), 29 deletions(-) diff --git a/src/runtime/node/zlib/NativeBrotli.rs b/src/runtime/node/zlib/NativeBrotli.rs index 9b6cc7b7dfc6..4d0e72826060 100644 --- a/src/runtime/node/zlib/NativeBrotli.rs +++ b/src/runtime/node/zlib/NativeBrotli.rs @@ -465,7 +465,12 @@ mod _impl { } pub fn close(&mut self) { - self.deinit_state(); + // `init()` may never have run (handle constructed but `init` + // failed argument validation or was never called); there is no + // encoder/decoder state to free then. + if self.state.is_some() { + self.deinit_state(); + } self.mode = bun_zlib::NodeMode::NONE; } diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index e46ddd5d8165..0587bd06cfdb 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -548,6 +548,13 @@ impl Context { pub fn close(&mut self) { use c::NodeMode::*; + // `init()` may never have run (handle constructed but `init` failed + // argument validation or was never called); `deflateEnd`/`inflateEnd` + // on the zeroed stream would report `StreamError`. + if self.state.internal_state.is_null() { + self.mode = NONE; + return; + } let mut status = c::ReturnCode::Ok; match self.mode { // SAFETY: FFI — state was initialized as a deflate stream by deflateInit2_. diff --git a/src/runtime/node/zlib/NativeZstd.rs b/src/runtime/node/zlib/NativeZstd.rs index ad4aab37f28b..b21309488be6 100644 --- a/src/runtime/node/zlib/NativeZstd.rs +++ b/src/runtime/node/zlib/NativeZstd.rs @@ -452,24 +452,30 @@ mod _impl { } pub fn close(&mut self) { - let _ = match self.mode { - // SAFETY: state is a valid CCtx/DCtx for this mode. - NodeMode::ZSTD_COMPRESS => unsafe { - c::ZSTD_CCtx_reset( - self.state_ptr().cast(), - c::ZSTD_reset_session_and_parameters, - ) - }, - // SAFETY: state is a valid DCtx set by init() for this mode. - NodeMode::ZSTD_DECOMPRESS => unsafe { - c::ZSTD_DCtx_reset( - self.state_ptr().cast(), - c::ZSTD_reset_session_and_parameters, - ) - }, - _ => unreachable!(), - }; - self.deinit_state(); + // `init()` may never have run (handle constructed but `init` + // failed argument validation or was never called); + // `ZSTD_*Ctx_reset` dereferences the context, so there is nothing + // to reset or free then. + if self.state.is_some() { + let _ = match self.mode { + // SAFETY: state is a valid CCtx/DCtx for this mode. + NodeMode::ZSTD_COMPRESS => unsafe { + c::ZSTD_CCtx_reset( + self.state_ptr().cast(), + c::ZSTD_reset_session_and_parameters, + ) + }, + // SAFETY: state is a valid DCtx set by init() for this mode. + NodeMode::ZSTD_DECOMPRESS => unsafe { + c::ZSTD_DCtx_reset( + self.state_ptr().cast(), + c::ZSTD_reset_session_and_parameters, + ) + }, + _ => unreachable!(), + }; + self.deinit_state(); + } self.mode = NodeMode::NONE; } diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index 5aeb7dd42854..b51252b0e8c9 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -242,6 +242,12 @@ impl<'a> ZlibReaderArrayList<'a> { list: &'a mut Vec, options: Options, ) -> Result, ZlibError> { + // zlib streams express byte counts as 32-bit (`uInt`); reject an input + // that would truncate `avail_in` and silently decode only a prefix. + let Ok(avail_in) = uInt::try_from(input.len()) else { + return Err(ZlibError::InvalidArgument); + }; + let mut zlib_reader = Box::new(Self { input, list_ptr: list, @@ -253,8 +259,8 @@ impl<'a> ZlibReaderArrayList<'a> { let list_len = zlib_reader.list_ptr.len(); zlib_reader.zlib = zStream_struct { next_in: input.as_ptr(), - avail_in: input.len() as uInt, - total_in: input.len() as _, + avail_in, + total_in: avail_in as _, next_out: zlib_reader.list_ptr.as_mut_ptr(), avail_out: list_len as uInt, @@ -722,6 +728,14 @@ impl<'a> ZlibCompressorArrayList<'a> { list: &'a mut Vec, options: Options, ) -> Result, ZlibError> { + // zlib streams express byte counts as 32-bit (`uInt`, and `uLong` is + // also 32-bit on Windows); reject an input that would truncate + // `avail_in` and silently compress only a prefix, or panic on the + // `deflateBound` cast below. + let Ok(avail_in) = uInt::try_from(input.len()) else { + return Err(ZlibError::InvalidArgument); + }; + let mut zlib_reader = Box::new(Self { input, list_ptr: list, @@ -732,8 +746,8 @@ impl<'a> ZlibCompressorArrayList<'a> { let list_len = zlib_reader.list_ptr.len(); zlib_reader.zlib = zStream_struct { next_in: input.as_ptr(), - avail_in: input.len() as uInt, - total_in: input.len() as _, + avail_in, + total_in: avail_in as _, next_out: zlib_reader.list_ptr.as_mut_ptr(), avail_out: list_len as uInt, @@ -770,12 +784,7 @@ impl<'a> ZlibCompressorArrayList<'a> { })?; // SAFETY: zlib initialized; deflateBound returns upper bound on output. - let bound = unsafe { - deflateBound( - &raw mut zlib_reader.zlib, - uLong::try_from(input.len()).expect("int cast"), - ) - }; + let bound = unsafe { deflateBound(&raw mut zlib_reader.zlib, uLong::from(avail_in)) }; // ensureTotalCapacityPrecise → reserve_exact let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); zlib_reader.list_ptr.reserve_exact(need); diff --git a/test/js/node/zlib/zlib-handle-bounds-check.test.ts b/test/js/node/zlib/zlib-handle-bounds-check.test.ts index ffac59ab607c..aa28f4572739 100644 --- a/test/js/node/zlib/zlib-handle-bounds-check.test.ts +++ b/test/js/node/zlib/zlib-handle-bounds-check.test.ts @@ -89,6 +89,205 @@ describe("zlib native handle bounds checking", () => { }); }); +describe("zlib native handle argument validation", () => { + const zlib = require("zlib"); + + function constructorOf(stream: any) { + const ctor = stream._handle.constructor; + stream.close(); + return ctor; + } + const NativeZlib = constructorOf(zlib.createDeflate()); + const NativeBrotli = constructorOf(zlib.createBrotliCompress()); + const NativeZstd = constructorOf(zlib.createZstdCompress()); + + function caught(fn: () => void) { + try { + fn(); + } catch (e: any) { + return { name: e.constructor.name, code: e.code, message: e.message }; + } + throw new Error("expected an error"); + } + + const modeCases: [string, any, number, number][] = [ + ["NativeZlib", NativeZlib, 1, 7], + ["NativeBrotli", NativeBrotli, 8, 9], + ["NativeZstd", NativeZstd, 10, 11], + ]; + + test.each(modeCases)("%s constructor validates mode", (_label, Class, min, max) => { + expect(caught(() => new Class("x"))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "mode" argument must be of type number. Received type string ('x')`, + }); + expect(caught(() => new Class(min + 0.5))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "mode" argument must be of type integer. Received type number (${min + 0.5})`, + }); + expect(caught(() => new Class(min - 1))).toEqual({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: `The value of "mode" is out of range. It must be >= ${min} and <= ${max}. Received ${min - 1}`, + }); + expect(caught(() => new Class(max + 1))).toEqual({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: `The value of "mode" is out of range. It must be >= ${min} and <= ${max}. Received ${max + 1}`, + }); + }); + + const cb = () => {}; + + test("NativeZlib.init validates the writeResult array", () => { + expect(caught(() => new NativeZlib(1).init(15, 6, 8, 0, "nope", cb, undefined))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "writeResult" argument must be of type Uint32Array. Received type string ('nope')`, + }); + expect(caught(() => new NativeZlib(1).init(15, 6, 8, 0, new Uint16Array(4), cb, undefined))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "writeResult" argument must be of type Uint32Array. Received an instance of Uint16Array`, + }); + expect(caught(() => new NativeZlib(1).init(15, 6, 8, 0, new Uint32Array(1), cb, undefined))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + message: "writeResult must be a Uint32Array with at least 2 elements", + }); + }); + + test("NativeBrotli.init validates the writeResult and params arrays", () => { + expect(caught(() => new NativeBrotli(8).init(new Uint32Array(0), new Uint32Array(1), cb))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + message: "writeResult must be a Uint32Array with at least 2 elements", + }); + expect(caught(() => new NativeBrotli(8).init(new Float64Array(2), new Uint32Array(2), cb))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "params" argument must be of type Uint32Array. Received an instance of Float64Array`, + }); + }); + + test("NativeZstd.init validates the writeState and initParamsArray arrays", () => { + expect(caught(() => new NativeZstd(10).init(new Uint32Array(0), 0, new Uint32Array(1), cb))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + message: "writeState must be a Uint32Array with at least 2 elements", + }); + expect(caught(() => new NativeZstd(10).init(new Float64Array(2), 0, new Uint32Array(2), cb))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "initParamsArray" argument must be of type Uint32Array. Received an instance of Float64Array`, + }); + }); + + test.each(["write", "writeSync"] as const)("%s validates its 7 arguments", method => { + const inBuf = new Uint8Array(4); + const outBuf = new Uint8Array(16); + + function withHandle(fn: (h: any) => any) { + const deflate = zlib.createDeflate(); + const h = deflate._handle; + try { + return fn(h); + } finally { + deflate.close(); + } + } + + expect(withHandle(h => caught(() => h[method]()))).toEqual({ + name: "TypeError", + code: "ERR_MISSING_ARGS", + message: `${method}(flush, in, in_off, in_len, out, out_off, out_len)`, + }); + expect(withHandle(h => caught(() => h[method](undefined, inBuf, 0, 4, outBuf, 0, 16)))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + message: "flush value is required", + }); + expect(withHandle(h => caught(() => h[method](99, inBuf, 0, 4, outBuf, 0, 16)))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + message: "Invalid flush value", + }); + expect(withHandle(h => caught(() => h[method](2, "zzz", 0, 4, outBuf, 0, 16)))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "in" argument must be a TypedArray or DataView`, + }); + expect(withHandle(h => caught(() => h[method](2, inBuf, 0, 4, "bad", 0, 16)))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "out" argument must be a TypedArray or DataView`, + }); + expect(withHandle(h => caught(() => h[method](2, inBuf, 2, 10, outBuf, 0, 16)))).toEqual({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: "in_off + in_len (12) exceeds input buffer length (4)", + }); + expect(withHandle(h => caught(() => h[method](2, inBuf, 0, 4, outBuf, 8, 16)))).toEqual({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: "out_off + out_len (24) exceeds output buffer length (16)", + }); + }); +}); + +describe("zlib native handle lifecycle", () => { + test("finalizing or closing a never-initialized handle does not crash", async () => { + // Constructing a handle and never running init() (or having init() fail + // argument validation) must not crash close() or the GC finalizer: the + // native compression state is only allocated by a successful init(). + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const zlib = require("zlib"); + function constructorOf(stream) { + const ctor = stream._handle.constructor; + stream.close(); + return ctor; + } + const NativeZlib = constructorOf(zlib.createDeflate()); + const NativeBrotli = constructorOf(zlib.createBrotliCompress()); + const NativeZstd = constructorOf(zlib.createZstdCompress()); + const cb = () => {}; + + // Constructed, init never called. + new NativeZlib(1); + new NativeBrotli(8); + new NativeBrotli(9); + new NativeZstd(10); + new NativeZstd(11); + + // Constructed, init failed argument validation. + try { new NativeZlib(1).init(15, 6, 8, 0, new Uint32Array(1), cb, undefined); } catch {} + try { new NativeBrotli(8).init(new Uint32Array(0), new Uint32Array(1), cb); } catch {} + try { new NativeZstd(10).init(new Uint32Array(0), 0, new Uint32Array(1), cb); } catch {} + + // Explicit close() on a never-initialized handle. + new NativeZlib(1).close(); + new NativeBrotli(8).close(); + new NativeZstd(10).close(); + + Bun.gc(true); + console.log("survived"); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), exitCode }).toEqual({ stdout: "survived", exitCode: 0 }); + }); +}); + describe("zlib native handle writeState", () => { test("writeSync updates the writeState array", () => { const zlib = require("zlib"); From c67f705b03d75d27af43cc1b19b5430a8fb02485 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:07:06 +0000 Subject: [PATCH 04/11] zlib: validate zstd initParamsArray before context allocation, assert subprocess stderr Moves the initParamsArray type check ahead of ZSTD context creation in NativeZstd::init so a type error cannot leave a half-initialized handle, and folds stderr into the lifecycle test's combined assertion so a crash regression shows the panic text in the failure diff. --- src/runtime/node/zlib/NativeZstd.rs | 7 +++++-- test/js/node/zlib/zlib-handle-bounds-check.test.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/runtime/node/zlib/NativeZstd.rs b/src/runtime/node/zlib/NativeZstd.rs index b21309488be6..adcf48669aa0 100644 --- a/src/runtime/node/zlib/NativeZstd.rs +++ b/src/runtime/node/zlib/NativeZstd.rs @@ -154,14 +154,17 @@ mod _impl { )?); } + // Validate before `s.init()` allocates the ZSTD context so a type + // error cannot leave a half-initialized handle behind. + let mut params_ = + validate_uint32_array(global, init_params_array_value, "initParamsArray")?; + let err = self.stream.with_mut(|s| s.init(pledged_src_size)); if err.is_error() { CompressionStream::::emit_error(self, global, this_value, err); return Ok(JSValue::FALSE); } - let mut params_ = - validate_uint32_array(global, init_params_array_value, "initParamsArray")?; for (i, &x) in params_.as_u32().iter().enumerate() { if x == u32::MAX { continue; diff --git a/test/js/node/zlib/zlib-handle-bounds-check.test.ts b/test/js/node/zlib/zlib-handle-bounds-check.test.ts index aa28f4572739..380c0288d014 100644 --- a/test/js/node/zlib/zlib-handle-bounds-check.test.ts +++ b/test/js/node/zlib/zlib-handle-bounds-check.test.ts @@ -284,7 +284,7 @@ describe("zlib native handle lifecycle", () => { }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout: stdout.trim(), exitCode }).toEqual({ stdout: "survived", exitCode: 0 }); + expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: "survived", stderr: "", exitCode: 0 }); }); }); From 21a40c400700d58b5d40cda0c79b026e634dc52d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:28:37 +0000 Subject: [PATCH 05/11] test: a rejected zstd initParamsArray must not leave a half-initialized handle After init() throws on an invalid initParamsArray, a write on the handle must move no bytes. Validating after ZSTD context creation left a usable encoder configured with default parameters behind the thrown error. --- .../zlib/zlib-handle-bounds-check.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/js/node/zlib/zlib-handle-bounds-check.test.ts b/test/js/node/zlib/zlib-handle-bounds-check.test.ts index 9ac43d090176..3d40e97f13d4 100644 --- a/test/js/node/zlib/zlib-handle-bounds-check.test.ts +++ b/test/js/node/zlib/zlib-handle-bounds-check.test.ts @@ -185,6 +185,28 @@ describe("zlib native handle argument validation", () => { }); }); + test("a rejected initParamsArray leaves the zstd handle un-initialized", () => { + const handle = new NativeZstd(10); + const writeState = new Uint32Array(2); + expect(caught(() => handle.init(new Float64Array(2), 0, writeState, cb))).toEqual({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "initParamsArray" argument must be of type Uint32Array. Received an instance of Float64Array`, + }); + // initParamsArray is validated before the ZSTD context is allocated, so a + // throwing init() must not leave a usable encoder configured with default + // parameters: a write on this handle moves no bytes, exactly like a write + // on a handle whose init() was never called. + const input = Buffer.from("hello hello hello hello hello hello"); + const out = Buffer.alloc(256); + handle.writeSync(2 /* ZSTD_e_end */, input, 0, input.length, out, 0, out.length); + expect({ availOut: writeState[0], availIn: writeState[1] }).toEqual({ + availOut: out.length, + availIn: input.length, + }); + handle.close(); + }); + test.each(["write", "writeSync"] as const)("%s validates its 7 arguments", method => { const inBuf = new Uint8Array(4); const outBuf = new Uint8Array(16); From 926eba090b194365b9e6f04c40cbaf016ad12764 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:22:54 +0000 Subject: [PATCH 06/11] test: rename describe block that collided with the one added on main --- test/js/node/zlib/zlib-handle-bounds-check.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/node/zlib/zlib-handle-bounds-check.test.ts b/test/js/node/zlib/zlib-handle-bounds-check.test.ts index 13d2f56c53be..6321d94890c5 100644 --- a/test/js/node/zlib/zlib-handle-bounds-check.test.ts +++ b/test/js/node/zlib/zlib-handle-bounds-check.test.ts @@ -89,7 +89,7 @@ describe("zlib native handle bounds checking", () => { }); }); -describe("zlib native handle argument validation", () => { +describe("zlib native handle constructor/init/write argument errors", () => { const zlib = require("zlib"); function constructorOf(stream: any) { From 242029de764c81d116d8e2e85e8e7d4ea733f1f0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:28:48 +0000 Subject: [PATCH 07/11] [autofix.ci] apply automated fixes --- src/runtime/node/node_zlib_binding.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runtime/node/node_zlib_binding.rs b/src/runtime/node/node_zlib_binding.rs index 741e7f96be97..7324a3a3d674 100644 --- a/src/runtime/node/node_zlib_binding.rs +++ b/src/runtime/node/node_zlib_binding.rs @@ -467,9 +467,7 @@ fn parse_write_args( return Err(global_this .err( ErrorCode::INVALID_ARG_VALUE, - format_args!( - "The \"out\" argument must not be backed by a resizable ArrayBuffer" - ), + format_args!("The \"out\" argument must not be backed by a resizable ArrayBuffer"), ) .throw()); } From 69ddbb789a9d49fe1144c0c2d1d9684247a8f3a8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:27:52 +0000 Subject: [PATCH 08/11] test: drop dead-symbol guards for the deleted zlib_sys/win32.rs The sweep lint read src/zlib_sys/win32.rs to check three dead externs had not come back; this branch removes the file, so the entries have nothing to read. --- src/zlib_sys/posix.rs | 43 ++++++ src/zlib_sys/win32.rs | 129 ++++++++++++++++++ .../dead-symbols-pub-exports-sweep.test.ts | 4 - 3 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/zlib_sys/posix.rs create mode 100644 src/zlib_sys/win32.rs diff --git a/src/zlib_sys/posix.rs b/src/zlib_sys/posix.rs new file mode 100644 index 000000000000..57f4919febd1 --- /dev/null +++ b/src/zlib_sys/posix.rs @@ -0,0 +1,43 @@ +#![allow(non_camel_case_types, non_snake_case)] + +use core::ffi::{c_char, c_int}; + +pub use crate::shared::{ + DataType, FlushValue, ReturnCode, alloc_func, free_func, struct_internal_state, z_alloc_fn, + z_free_fn, z_stream, z_streamp, zStream_struct, +}; + +unsafe extern "C" { + pub safe fn zlibVersion() -> *const c_char; + + pub fn deflateInit_( + strm: z_streamp, + level: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; + pub fn deflateInit2_( + strm: z_streamp, + level: c_int, + method: c_int, + windowBits: c_int, + memLevel: c_int, + strategy: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateInit2_( + strm: z_streamp, + windowBits: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateBackInit_( + strm: z_streamp, + windowBits: c_int, + window: *mut u8, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; +} diff --git a/src/zlib_sys/win32.rs b/src/zlib_sys/win32.rs new file mode 100644 index 000000000000..83a4a6a2968a --- /dev/null +++ b/src/zlib_sys/win32.rs @@ -0,0 +1,129 @@ +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] + +use core::ffi::{c_char, c_int, c_uint, c_void}; + +pub use crate::shared::{ + Bytef, DataType, FlushValue, ReturnCode, alloc_func, free_func, gzFile, gzFile_s, + internal_state, struct_gzFile_s, struct_internal_state, struct_z_stream_s, uInt, uLong, uLongf, + voidpf, z_alloc_func, z_free_func, z_stream, z_stream_s, z_streamp, zStream_struct, +}; + +type voidpc = *const c_void; +type voidp = *mut c_void; + +#[repr(C)] +pub struct struct_gz_header_s { + pub text: c_int, + pub time: uLong, + pub xflags: c_int, + pub os: c_int, + pub extra: *mut Bytef, + pub extra_len: uInt, + pub extra_max: uInt, + pub name: *mut Bytef, + pub name_max: uInt, + pub comment: *mut Bytef, + pub comm_max: uInt, + pub hcrc: c_int, + pub done: c_int, +} +pub(crate) type gz_header = struct_gz_header_s; +pub(crate) type gz_headerp = *mut gz_header; + +pub(crate) type in_func = Option c_uint>; +pub(crate) type out_func = Option ReturnCode>; + +unsafe extern "C" { + pub safe fn zlibVersion() -> *const c_char; + pub fn deflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; + pub fn deflateEnd(strm: z_streamp) -> ReturnCode; + pub fn inflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; + pub fn inflateEnd(strm: z_streamp) -> ReturnCode; + pub fn deflateSetDictionary( + strm: z_streamp, + dictionary: *const Bytef, + dictLength: uInt, + ) -> ReturnCode; + pub fn deflateReset(strm: z_streamp) -> ReturnCode; + pub fn deflateParams(strm: z_streamp, level: c_int, strategy: c_int) -> ReturnCode; + pub fn deflateBound(strm: z_streamp, sourceLen: uLong) -> uLong; + pub fn deflateSetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; + pub fn inflateSetDictionary( + strm: z_streamp, + dictionary: *const Bytef, + dictLength: uInt, + ) -> ReturnCode; + pub fn inflateSync(strm: z_streamp) -> ReturnCode; + pub fn inflateReset(strm: z_streamp) -> ReturnCode; + pub fn inflateReset2(strm: z_streamp, windowBits: c_int) -> ReturnCode; + pub fn inflateGetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; + pub fn inflateBack( + strm: z_streamp, + in_: in_func, + in_desc: *mut c_void, + out: out_func, + out_desc: *mut c_void, + ) -> ReturnCode; + pub fn compress( + dest: *mut Bytef, + destLen: *mut uLongf, + source: *const Bytef, + sourceLen: uLong, + ) -> ReturnCode; + pub fn compress2( + dest: *mut Bytef, + destLen: *mut uLongf, + source: *const Bytef, + sourceLen: uLong, + level: c_int, + ) -> ReturnCode; + pub safe fn compressBound(sourceLen: uLong) -> uLong; + pub fn uncompress( + dest: *mut Bytef, + destLen: *mut uLongf, + source: *const Bytef, + sourceLen: uLong, + ) -> ReturnCode; + pub fn gzdopen(fd: c_int, mode: *const u8) -> gzFile; + pub fn gzread(file: gzFile, buf: voidp, len: c_uint) -> ReturnCode; + pub fn gzwrite(file: gzFile, buf: voidpc, len: c_uint) -> ReturnCode; + pub fn gzgetc(file: gzFile) -> ReturnCode; + pub fn gzclose(file: gzFile) -> ReturnCode; + pub fn gzerror(file: gzFile, errnum: *mut c_int) -> *const u8; + pub fn adler32(adler: uLong, buf: *const Bytef, len: uInt) -> uLong; + pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; + pub fn deflateInit_( + strm: z_streamp, + level: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; + pub fn deflateInit2_( + strm: z_streamp, + level: c_int, + method: c_int, + windowBits: c_int, + memLevel: c_int, + strategy: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateInit2_( + strm: z_streamp, + windowBits: c_int, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn inflateBackInit_( + strm: z_streamp, + windowBits: c_int, + window: *mut u8, + version: *const c_char, + stream_size: c_int, + ) -> ReturnCode; + pub fn gzopen(path: *const u8, mode: *const u8) -> gzFile; + // pub fn get_crc_table() -> *const z_crc_t; + pub fn inflateResetKeep(strm: z_streamp) -> ReturnCode; + pub fn deflateResetKeep(strm: z_streamp) -> ReturnCode; +} diff --git a/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts b/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts index 296c813e66f8..13f72273d5f9 100644 --- a/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts +++ b/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts @@ -63,10 +63,6 @@ test("dead FFI declarations (sys crates) do not reappear", () => { ["src/mimalloc_sys/mimalloc.rs", /\bfn mi_reserve_huge_os_pages_interleave\b/], ["src/mimalloc_sys/mimalloc.rs", /\bfn mi_heap_recalloc_aligned_at\b/], ["src/mimalloc_sys/mimalloc.rs", /\bfn mi_wdupenv_s\b/], - // zlib_sys/win32 — unused gz* file API and introspection entry points. - ["src/zlib_sys/win32.rs", /\bfn gzprintf\b/], - ["src/zlib_sys/win32.rs", /\bfn inflateUndermine\b/], - ["src/zlib_sys/win32.rs", /\bfn deflateTune\b/], // cares_sys — unused configuration/parsing surface. ["src/cares_sys/c_ares.rs", /\bfn ares_mkquery\b/], ["src/cares_sys/c_ares.rs", /\bfn ares_set_sortlist\b/], From c072c5efb61998489e0efb8a8d9e4d884673a4cd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:07:09 +0000 Subject: [PATCH 09/11] zlib_sys: remove posix.rs and win32.rs again They came back in 69ddbb7 by way of a stale index; lib.rs only declares shared, so nothing referenced them. --- src/zlib_sys/posix.rs | 43 -------------- src/zlib_sys/win32.rs | 129 ------------------------------------------ 2 files changed, 172 deletions(-) delete mode 100644 src/zlib_sys/posix.rs delete mode 100644 src/zlib_sys/win32.rs diff --git a/src/zlib_sys/posix.rs b/src/zlib_sys/posix.rs deleted file mode 100644 index 57f4919febd1..000000000000 --- a/src/zlib_sys/posix.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case)] - -use core::ffi::{c_char, c_int}; - -pub use crate::shared::{ - DataType, FlushValue, ReturnCode, alloc_func, free_func, struct_internal_state, z_alloc_fn, - z_free_fn, z_stream, z_streamp, zStream_struct, -}; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; -} diff --git a/src/zlib_sys/win32.rs b/src/zlib_sys/win32.rs deleted file mode 100644 index 83a4a6a2968a..000000000000 --- a/src/zlib_sys/win32.rs +++ /dev/null @@ -1,129 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] - -use core::ffi::{c_char, c_int, c_uint, c_void}; - -pub use crate::shared::{ - Bytef, DataType, FlushValue, ReturnCode, alloc_func, free_func, gzFile, gzFile_s, - internal_state, struct_gzFile_s, struct_internal_state, struct_z_stream_s, uInt, uLong, uLongf, - voidpf, z_alloc_func, z_free_func, z_stream, z_stream_s, z_streamp, zStream_struct, -}; - -type voidpc = *const c_void; -type voidp = *mut c_void; - -#[repr(C)] -pub struct struct_gz_header_s { - pub text: c_int, - pub time: uLong, - pub xflags: c_int, - pub os: c_int, - pub extra: *mut Bytef, - pub extra_len: uInt, - pub extra_max: uInt, - pub name: *mut Bytef, - pub name_max: uInt, - pub comment: *mut Bytef, - pub comm_max: uInt, - pub hcrc: c_int, - pub done: c_int, -} -pub(crate) type gz_header = struct_gz_header_s; -pub(crate) type gz_headerp = *mut gz_header; - -pub(crate) type in_func = Option c_uint>; -pub(crate) type out_func = Option ReturnCode>; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - pub fn deflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn deflateEnd(strm: z_streamp) -> ReturnCode; - pub fn inflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn inflateEnd(strm: z_streamp) -> ReturnCode; - pub fn deflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn deflateReset(strm: z_streamp) -> ReturnCode; - pub fn deflateParams(strm: z_streamp, level: c_int, strategy: c_int) -> ReturnCode; - pub fn deflateBound(strm: z_streamp, sourceLen: uLong) -> uLong; - pub fn deflateSetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn inflateSync(strm: z_streamp) -> ReturnCode; - pub fn inflateReset(strm: z_streamp) -> ReturnCode; - pub fn inflateReset2(strm: z_streamp, windowBits: c_int) -> ReturnCode; - pub fn inflateGetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateBack( - strm: z_streamp, - in_: in_func, - in_desc: *mut c_void, - out: out_func, - out_desc: *mut c_void, - ) -> ReturnCode; - pub fn compress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn compress2( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - level: c_int, - ) -> ReturnCode; - pub safe fn compressBound(sourceLen: uLong) -> uLong; - pub fn uncompress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn gzdopen(fd: c_int, mode: *const u8) -> gzFile; - pub fn gzread(file: gzFile, buf: voidp, len: c_uint) -> ReturnCode; - pub fn gzwrite(file: gzFile, buf: voidpc, len: c_uint) -> ReturnCode; - pub fn gzgetc(file: gzFile) -> ReturnCode; - pub fn gzclose(file: gzFile) -> ReturnCode; - pub fn gzerror(file: gzFile, errnum: *mut c_int) -> *const u8; - pub fn adler32(adler: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn gzopen(path: *const u8, mode: *const u8) -> gzFile; - // pub fn get_crc_table() -> *const z_crc_t; - pub fn inflateResetKeep(strm: z_streamp) -> ReturnCode; - pub fn deflateResetKeep(strm: z_streamp) -> ReturnCode; -} From 4562fd945554cb21dc10867d08d37b242171b894 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:34:02 +0000 Subject: [PATCH 10/11] zlib: clamp avail_out to 32 bits; tighten mode and failed-init tests reserve_expand_tail returns the Vec's whole slack, so a buffer with 4 GiB or more spare truncated avail_out and could make a valid compress fail with ShortRead. Clamp at the two live sites (the regrow helper and the compressor's post-deflateBound setup). Tests: cover undefined, 0, NaN and the infinities for every constructor, and make the lifecycle test assert each invalid init() really threw. --- src/zlib/lib.rs | 10 ++++- .../zlib/zlib-handle-bounds-check.test.ts | 43 ++++++++++++------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index aff2defbdf63..93cf2d0da199 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -175,13 +175,19 @@ fn map_init_return_code(rc: ReturnCode) -> Result<(), ZlibError> { } } +/// Largest byte count a single zlib call can be offered; `reserve_expand_tail` +/// hands back the Vec's whole slack, which can exceed 32 bits. +fn avail_out_for(len: usize) -> uInt { + len.min(uInt::MAX as usize) as uInt +} + /// Point the stream's output at freshly reserved tail capacity of `list`, /// capping `avail_out` at `budget` bytes (`usize::MAX` = unbounded). fn regrow_output_tail(zlib: &mut zStream_struct, list: &mut Vec, budget: usize) { // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. let (next_out, avail_out) = unsafe { list.reserve_expand_tail(budget.min(4096)) }; zlib.next_out = next_out; - zlib.avail_out = avail_out.min(budget) as uInt; + zlib.avail_out = avail_out_for(avail_out.min(budget)); } // zlib `alloc_func`/`free_func` thunks → mimalloc, used by @@ -791,7 +797,7 @@ impl<'a> ZlibCompressorArrayList<'a> { // ensureTotalCapacityPrecise → reserve_exact let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); zlib_reader.list_ptr.reserve_exact(need); - zlib_reader.zlib.avail_out = zlib_reader.list_ptr.capacity() as uInt; + zlib_reader.zlib.avail_out = avail_out_for(zlib_reader.list_ptr.capacity()); zlib_reader.zlib.next_out = zlib_reader.list_ptr.as_mut_ptr(); Ok(zlib_reader) diff --git a/test/js/node/zlib/zlib-handle-bounds-check.test.ts b/test/js/node/zlib/zlib-handle-bounds-check.test.ts index 6321d94890c5..fe1620c0cdf7 100644 --- a/test/js/node/zlib/zlib-handle-bounds-check.test.ts +++ b/test/js/node/zlib/zlib-handle-bounds-check.test.ts @@ -117,26 +117,31 @@ describe("zlib native handle constructor/init/write argument errors", () => { ]; test.each(modeCases)("%s constructor validates mode", (_label, Class, min, max) => { - expect(caught(() => new Class("x"))).toEqual({ + const notANumber = (received: string) => ({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", - message: `The "mode" argument must be of type number. Received type string ('x')`, + message: `The "mode" argument must be of type number. Received ${received}`, }); - expect(caught(() => new Class(min + 0.5))).toEqual({ + const notAnInteger = (value: number) => ({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", - message: `The "mode" argument must be of type integer. Received type number (${min + 0.5})`, + message: `The "mode" argument must be of type integer. Received type number (${value})`, }); - expect(caught(() => new Class(min - 1))).toEqual({ + const outOfRange = (value: number) => ({ name: "RangeError", code: "ERR_OUT_OF_RANGE", - message: `The value of "mode" is out of range. It must be >= ${min} and <= ${max}. Received ${min - 1}`, - }); - expect(caught(() => new Class(max + 1))).toEqual({ - name: "RangeError", - code: "ERR_OUT_OF_RANGE", - message: `The value of "mode" is out of range. It must be >= ${min} and <= ${max}. Received ${max + 1}`, + message: `The value of "mode" is out of range. It must be >= ${min} and <= ${max}. Received ${value}`, }); + + expect(caught(() => new Class())).toEqual(notANumber("undefined")); + expect(caught(() => new Class("x"))).toEqual(notANumber("type string ('x')")); + expect(caught(() => new Class(min + 0.5))).toEqual(notAnInteger(min + 0.5)); + expect(caught(() => new Class(NaN))).toEqual(notAnInteger(NaN)); + expect(caught(() => new Class(Infinity))).toEqual(notAnInteger(Infinity)); + expect(caught(() => new Class(-Infinity))).toEqual(notAnInteger(-Infinity)); + expect(caught(() => new Class(0))).toEqual(outOfRange(0)); + expect(caught(() => new Class(min - 1))).toEqual(outOfRange(min - 1)); + expect(caught(() => new Class(max + 1))).toEqual(outOfRange(max + 1)); }); const cb = () => {}; @@ -287,10 +292,18 @@ describe("zlib native handle lifecycle", () => { new NativeZstd(10); new NativeZstd(11); - // Constructed, init failed argument validation. - try { new NativeZlib(1).init(15, 6, 8, 0, new Uint32Array(1), cb, undefined); } catch {} - try { new NativeBrotli(8).init(new Uint32Array(0), new Uint32Array(1), cb); } catch {} - try { new NativeZstd(10).init(new Uint32Array(0), 0, new Uint32Array(1), cb); } catch {} + // Constructed, init failed argument validation. Each init() must + // actually throw, otherwise this would be finalizing healthy handles. + function rejected(init) { + try { init(); } catch (e) { return e.code; } + throw new Error("init() unexpectedly succeeded"); + } + const codes = [ + rejected(() => new NativeZlib(1).init(15, 6, 8, 0, new Uint32Array(1), cb, undefined)), + rejected(() => new NativeBrotli(8).init(new Uint32Array(0), new Uint32Array(1), cb)), + rejected(() => new NativeZstd(10).init(new Uint32Array(0), 0, new Uint32Array(1), cb)), + ]; + if (codes.some(c => c !== "ERR_INVALID_ARG_VALUE")) throw new Error("unexpected codes: " + codes); // Explicit close() on a never-initialized handle. new NativeZlib(1).close(); From 87a9140c0f33a3da8a64f170a0959caf9069be96 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:57:29 +0000 Subject: [PATCH 11/11] test: guard against zlib_sys/posix.rs and win32.rs coming back The orphaned-files lint reads the committed tree, so it catches the stale-index resurrection that happened once already on this branch. --- .../source-lints/dead-symbols-pub-exports-sweep.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts b/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts index 13f72273d5f9..29bac30fe38d 100644 --- a/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts +++ b/test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts @@ -108,6 +108,9 @@ test("orphaned files stay deleted", () => { "src/fixtures_example.com.html", "src/logo.svg", "src/favicon.png", + // zlib_sys declares only `shared`; these were re-exports nothing imported + "src/zlib_sys/posix.rs", + "src/zlib_sys/win32.rs", ]; const tree = headTree(); const resurrected = gone.filter(p => tree.has(p));