diff --git a/mordant-baseline.toml b/mordant-baseline.toml index 53d1145b9821..c20796e9d0b5 100644 --- a/mordant-baseline.toml +++ b/mordant-baseline.toml @@ -34,7 +34,6 @@ [bun_runtime] "bare_bool_args:src/runtime/api.rs" = 2 -"bare_bool_args:src/runtime/node/types.rs" = 4 "defaulted_failure:src/runtime/ffi/ffi_body.rs" = 1 "defaulted_failure:src/runtime/server/RequestContext.rs" = 1 "defaulted_failure:src/runtime/test_runner/pretty_format.rs" = 4 diff --git a/src/jsc/node_path.rs b/src/jsc/node_path.rs index cae9c21ddb74..e00df5bb73c2 100644 --- a/src/jsc/node_path.rs +++ b/src/jsc/node_path.rs @@ -46,7 +46,7 @@ pub struct ThreadSafe(T); impl ThreadSafe { /// Wrap an **already-protected** `T`. Use when the protect was taken - /// elsewhere (e.g. inside `from_js_maybe_async(.., is_async=true)`). + /// elsewhere (e.g. inside `from_js_maybe_async(.., Flavor::Async, ..)`). #[inline] pub fn adopt(value: T) -> Self { Self(value) diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 199d2448adc8..25189b61d259 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -2726,12 +2726,11 @@ pub mod JSZstd { let level = get_level(global_this, options_val)?; - let allow_string_object = true; if let Some(buffer) = node::StringOrBuffer::from_js_maybe_async( global_this, buffer_value, - true, - allow_string_object, + node::Flavor::Async, + node::StringObjects::Allow, )? { return Ok((buffer, options_val, level)); } @@ -2807,7 +2806,7 @@ pub mod JSZstd { /// `Bun.zstdCompress` / `Bun.zstdDecompress` off the JS thread. pub(crate) struct ZstdJob { - /// Created with `is_async=true` (JS-backed buffer protected); the + /// Created with `Flavor::Async` (JS-backed buffer protected); the /// [`bun_jsc::ThreadSafe`] releases that with the job. pub buffer: bun_jsc::ThreadSafe, pub is_compress: bool, diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index f2215c727aa6..67d0ef9e5155 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -5,7 +5,7 @@ use bun_options_types::TargetExt as _; use std::io::Write as _; use crate::Error; -use crate::node::{Encoding, StringOrBuffer}; +use crate::node::{Encoding, Flavor, StringObjects, StringOrBuffer}; use bun_alloc::{Arena, ArenaVec}; // bumpalo::Bump / bumpalo::collections::Vec re-exports use bun_ast::Expr; use bun_ast::Loader; @@ -1392,13 +1392,12 @@ impl JSTranspiler { )); }; - let allow_string_object = true; let Some(code) = StringOrBuffer::from_js_with_encoding_maybe_async( global, code_arg, Encoding::Utf8, - true, - allow_string_object, + Flavor::Async, + StringObjects::Allow, )? else { return Err(global.throw_invalid_argument_type( @@ -1415,7 +1414,7 @@ impl JSTranspiler { code = StringOrBuffer::EncodedSlice(bun_core::ZigStringSlice::init_owned(bytes)); } // `errdefer code.deinitAndUnprotect()` — `from_js_with_encoding_maybe_async` - // (is_async=true) already protected; adopt into a `ThreadSafe` so any + // (`Flavor::Async`) already protected; adopt into a `ThreadSafe` so any // early-return drop unprotects. `TransformTask::create` takes the guard. let code = bun_jsc::ThreadSafe::adopt(code); diff --git a/src/runtime/crypto/PBKDF2.rs b/src/runtime/crypto/PBKDF2.rs index 6c8a8e0b93c1..ab6044ebf277 100644 --- a/src/runtime/crypto/PBKDF2.rs +++ b/src/runtime/crypto/PBKDF2.rs @@ -6,7 +6,7 @@ use bun_jsc::{ JsThread, }; -use crate::node::StringOrBuffer; +use crate::node::{Flavor, StringObjects, StringOrBuffer}; use crate::crypto::evp::{self, Algorithm}; @@ -68,7 +68,7 @@ impl PBKDF2 { pub(crate) fn from_js( global_this: &JSGlobalObject, call_frame: &CallFrame, - is_async: bool, + flavor: Flavor, ) -> JsResult { let [arg0, arg1, arg2, arg3, arg4, arg5] = call_frame.arguments_as_array::<6>(); @@ -181,17 +181,16 @@ impl PBKDF2 { }; // Non-async path: `StringOrBuffer` fields drop with `out` on early return — no explicit call needed. let mut guard = scopeguard::guard(&mut out, |out| { - if global_this.has_exception() && is_async { + if global_this.has_exception() && flavor == Flavor::Async { bun_jsc::Unprotect::unprotect(out); } }); - let allow_string_object = true; guard.salt = match StringOrBuffer::from_js_maybe_async( global_this, arg1, - is_async, - allow_string_object, + flavor, + StringObjects::Allow, )? { Some(v) => v, None => { @@ -210,8 +209,8 @@ impl PBKDF2 { guard.password = match StringOrBuffer::from_js_maybe_async( global_this, arg0, - is_async, - allow_string_object, + flavor, + StringObjects::Allow, )? { Some(v) => v, None => { @@ -227,13 +226,13 @@ impl PBKDF2 { return Err(global_this.throw_invalid_arguments(format_args!("password is too long"))); } - if !is_async { + if flavor == Flavor::Sync { if let StringOrBuffer::Buffer(buffer) = &mut guard.salt { buffer.buffer = ArrayBuffer::from_typed_array(global_this, buffer.buffer.value); } } - if is_async { + if flavor == Flavor::Async { if !arg5.is_function() { return Err(global_this.throw_invalid_argument_type_value( b"callback", @@ -260,7 +259,7 @@ impl bun_jsc::Unprotect for PBKDF2 { /// `crypto.pbkdf2` off the JS thread. pub(crate) struct Pbkdf2Job { - /// `from_js(.., is_async=true)` protected the input buffers; the + /// `from_js(.., Flavor::Async)` protected the input buffers; the /// [`bun_jsc::ThreadSafe`] releases that with the job. pub pbkdf2: bun_jsc::ThreadSafe, pub output: Vec, @@ -316,7 +315,7 @@ pub(crate) fn create_job(global_this: &JSGlobalObject, data: PBKDF2) -> JSValue Job::::schedule( &cx, Pbkdf2Job { - // `from_js(.., is_async=true)` already protected — adopt, don't re-protect. + // `from_js(.., Flavor::Async)` already protected — adopt, don't re-protect. pbkdf2: bun_jsc::ThreadSafe::adopt(data), output: Vec::new(), err: false, diff --git a/src/runtime/node.rs b/src/runtime/node.rs index 16fa3d73220b..e1834746a6f3 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -16,8 +16,9 @@ pub mod assert { #[path = "node/types.rs"] pub mod types; pub use types::{ - BlobOrStringOrBuffer, Dirent, Encoding, FileSystemFlags, PathLike, PathOrBlob, - PathOrFileDescriptor, StringOrBuffer, Valid, VectorArrayBuffer, mode_from_js, + BlobOrStringOrBuffer, Dirent, Encoding, FileBlobs, FileSystemFlags, Flavor, PathLike, + PathOrBlob, PathOrFileDescriptor, StringObjects, StringOrBuffer, Valid, VectorArrayBuffer, + mode_from_js, }; pub use bun_jsc::MarkedArrayBuffer as Buffer; diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 60980d9cd2e5..fb91d000cdd6 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -10,7 +10,7 @@ use bun_jsc::{ JsThread, Protected, Strong, }; -use crate::node::StringOrBuffer; +use crate::node::{Flavor, StringObjects, StringOrBuffer}; // `&JSGlobalObject` is ABI-identical to a non-null pointer; remaining params // are by-value `JSValue`, so no caller-side preconditions remain. @@ -735,7 +735,8 @@ pub mod random { pub(crate) struct Scrypt { // Plain `StringOrBuffer` — NOT `ThreadSafe<_>`. The struct serves both // `scryptSync` (no protect taken) and async `scrypt` (protect taken in - // `from_js_maybe_async(.., true)`, adopted into a `ThreadSafe` by the job). + // `from_js_maybe_async(.., Flavor::Async, ..)`, adopted into a `ThreadSafe` + // by the job). password: StringOrBuffer, salt: StringOrBuffer, n: u32, @@ -768,6 +769,11 @@ mod _impl { ] = call_frame.arguments_as_array::<5>(); let mut maybe_options_value: Option = Some(options_arg); let mut callback = callback_arg; + let flavor = if IS_ASYNC { + Flavor::Async + } else { + Flavor::Sync + }; if IS_ASYNC { if callback.is_undefined() { @@ -776,8 +782,12 @@ mod _impl { } } - let Some(password) = - StringOrBuffer::from_js_maybe_async(global, password_value, IS_ASYNC, true)? + let Some(password) = StringOrBuffer::from_js_maybe_async( + global, + password_value, + flavor, + StringObjects::Allow, + )? else { return Err(global.throw_invalid_argument_type_value( b"password", @@ -795,8 +805,12 @@ mod _impl { } }); - let Some(salt) = - StringOrBuffer::from_js_maybe_async(global, salt_value, IS_ASYNC, true)? + let Some(salt) = StringOrBuffer::from_js_maybe_async( + global, + salt_value, + flavor, + StringObjects::Allow, + )? else { return Err(global.throw_invalid_argument_type_value( b"salt", @@ -1028,8 +1042,8 @@ mod _impl { } impl bun_jsc::Unprotect for Scrypt { - /// Release the `protect()` taken by `from_js_maybe_async(.., true)` on the - /// async path (via the job's `ThreadSafe`). The sync path never calls this. + /// Release the `protect()` taken by `from_js_maybe_async(.., Flavor::Async, ..)` + /// on the async path (via the job's `ThreadSafe`). The sync path never calls this. #[inline] fn unprotect(&mut self) { bun_jsc::Unprotect::unprotect(&mut self.password); @@ -1109,14 +1123,14 @@ mod _impl { #[bun_jsc::host_fn] fn pbkdf2(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let data = PBKDF2::from_js(global_this, call_frame, true)?; + let data = PBKDF2::from_js(global_this, call_frame, Flavor::Async)?; Ok(pbkdf2::create_job(global_this, data)) } #[bun_jsc::host_fn] fn pbkdf2_sync(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let data = PBKDF2::from_js(global_this, call_frame, false)?; + let data = PBKDF2::from_js(global_this, call_frame, Flavor::Sync)?; // `PBKDF2`'s `StringOrBuffer` fields release on `Drop`, so the local // just goes out of scope. let mut data = data; diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 8fa740dc3c7b..dd6773bdc442 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -154,11 +154,13 @@ use super::stat::Stats; use super::time_like::TimeLike; use super::types::{ ArgumentsSlice, Dirent, Encoding, FdArgExt as _, FileSystemFlags, FileSystemFlagsKind, - NameTooLong, PathLike, PathLikeExt as _, PathOrFdExt as _, StringOrBuffer, VectorArrayBuffer, + NameTooLong, PathLike, PathLikeExt as _, PathOrFdExt as _, StringObjects, StringOrBuffer, + VectorArrayBuffer, }; // Re-exported publicly: `crate::node::fs::PathOrFileDescriptor` is the -// canonical path used by `cli/build_command.rs` et al. -pub use super::types::PathOrFileDescriptor; +// canonical path used by `cli/build_command.rs` et al., and `node_fs::Flavor` +// by every caller that runs an operation directly (`read_file(.., Flavor::Sync)`). +pub use super::types::{Flavor, PathOrFileDescriptor}; /// Local alias for the many `node::foo` call sites below, routing to `super::*`. mod node { @@ -473,15 +475,6 @@ pub(crate) const DEFAULT_PERMISSION: Mode = 0; // `&AbortSignal` inherent methods — the former `AbortSignalRefExt` shim with // per-call `unsafe { self.as_ref() }` is gone. `unref()` is handled by `Drop`. -/// All async FS functions are run in a thread pool, but some implementations may -/// decide to do something slightly different. For example, reading a file has -/// an extra stack buffer in the async case. -#[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] -pub enum Flavor { - Sync, - Async, -} - // ────────────────────────────────────────────────────────────────────────── // Async task type aliases // ────────────────────────────────────────────────────────────────────────── @@ -4213,11 +4206,14 @@ pub mod args { } } } + let flavor = if arguments.will_be_async { + Flavor::Async + } else { + Flavor::Sync + }; // String objects not allowed (typeof new String("hi") === "object") // https://github.com/nodejs/node/blob/6f946c95b9da75c70e868637de8161bc8d048379/lib/internal/fs/utils.js#L916 - let allow_string_object = false; - let is_async = arguments.will_be_async; - let data = StringOrBuffer::from_js_with_encoding_maybe_async(ctx, data_value, encoding, is_async, allow_string_object)? + let data = StringOrBuffer::from_js_with_encoding_maybe_async(ctx, data_value, encoding, flavor, StringObjects::Reject)? .ok_or_else(|| validators::throw_err_invalid_arg_type_with_message(ctx, format_args!("The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView")))?; let abort_signal = scopeguard::ScopeGuard::into_inner(abort_signal); Ok(WriteFile { diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 1cafda42a21c..465c7c6f07b1 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -59,6 +59,42 @@ pub use bun_sys::PlatformIoVec; // ────────────────────────────────────────────────────────────────────────── +/// Whether a call is serviced on the JS thread before it returns (`Sync`) or +/// handed to the thread pool (`Async`). A few FS operations take a different +/// path per flavor (`read_file`'s scratch buffer, recursive `readdir`), and +/// the arguments parsed for an async call must outlive it off the JS thread: +/// strings are copied or re-referenced thread-safely, buffers are pinned and +/// `protect()`ed until the owner calls [`bun_jsc::Unprotect::unprotect`]. +#[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] +pub enum Flavor { + Sync, + Async, +} + +/// Whether a `String` wrapper object (`new String("..")`) counts as a string +/// when parsing a string-or-buffer argument. Node's `fs.writeFile` family +/// rejects wrapper objects; everything else unwraps them. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum StringObjects { + Allow, + /// Only primitive strings match; a wrapper object parses as "not a string + /// or buffer", so the caller throws its own type error. + Reject, +} + +/// What [`BlobOrStringOrBuffer`] parsing does with a file-backed `Blob` +/// (`Bun.file(..)`). Nothing here reads the file: an allowed one is returned +/// as [`BlobOrStringOrBuffer::Blob`] like an in-memory blob, and its `slice()` +/// is empty. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum FileBlobs { + Allow, + /// Throws "File blob cannot be used here". + Reject, +} + +// ────────────────────────────────────────────────────────────────────────── + pub enum BlobOrStringOrBuffer { Blob(Box), StringOrBuffer(StringOrBuffer), @@ -92,11 +128,12 @@ impl BlobOrStringOrBuffer { pub(crate) fn from_js_maybe_file_maybe_async( global: &JSGlobalObject, value: JSValue, - allow_file: bool, - is_async: bool, + file_blobs: FileBlobs, + flavor: Flavor, ) -> JsResult> { // Check StringOrBuffer first because it's more common and cheaper. - let str = match StringOrBuffer::from_js_maybe_async(global, value, is_async, true)? { + let str = StringOrBuffer::from_js_maybe_async(global, value, flavor, StringObjects::Allow)?; + let str = match str { Some(s) => s, None => { // `as_class_ref` is the safe shared-borrow downcast (centralised @@ -106,12 +143,12 @@ impl BlobOrStringOrBuffer { let Some(blob) = value.as_class_ref::() else { return Ok(None); }; - if allow_file && blob.needs_to_read_file() { + if file_blobs == FileBlobs::Reject && blob.needs_to_read_file() { return Err(global .throw_invalid_arguments(format_args!("File blob cannot be used here"))); } - if is_async { + if flavor == Flavor::Async { // For async/cross-thread usage, copy the blob data to an owned slice // rather than referencing the store which isn't thread-safe let blob_data = blob.shared_view(); @@ -132,23 +169,23 @@ impl BlobOrStringOrBuffer { pub(crate) fn from_js_maybe_file( global: &JSGlobalObject, value: JSValue, - allow_file: bool, + file_blobs: FileBlobs, ) -> JsResult> { - Self::from_js_maybe_file_maybe_async(global, value, allow_file, false) + Self::from_js_maybe_file_maybe_async(global, value, file_blobs, Flavor::Sync) } pub fn from_js( global: &JSGlobalObject, value: JSValue, ) -> JsResult> { - Self::from_js_maybe_file(global, value, true) + Self::from_js_maybe_file(global, value, FileBlobs::Reject) } pub(crate) fn from_js_async( global: &JSGlobalObject, value: JSValue, ) -> JsResult> { - Self::from_js_maybe_file_maybe_async(global, value, true, true) + Self::from_js_maybe_file_maybe_async(global, value, FileBlobs::Reject, Flavor::Async) } /// Like [`from_js_with_encoding_value_allow_request_response`] but takes an @@ -220,12 +257,11 @@ impl BlobOrStringOrBuffer { _ => {} } - let allow_string_object = true; match StringOrBuffer::from_js_with_encoding_value_allow_string_object( global, value, encoding_value, - allow_string_object, + StringObjects::Allow, )? { Some(s) => Ok(Some(Self::StringOrBuffer(s))), None => Ok(None), @@ -294,8 +330,8 @@ impl bun_jsc::Unprotect for BlobOrStringOrBuffer { impl bun_jsc::Unprotect for StringOrBuffer { /// JS-side half of cleanup — undo the /// `protect()` taken by [`StringOrBuffer::to_thread_safe`] / - /// `from_js_maybe_async(.., is_async=true)`. Owned slices are released by - /// `Drop`. + /// `from_js_maybe_async(.., Flavor::Async, ..)`. Owned slices are released + /// by `Drop`. #[inline] fn unprotect(&mut self) { if let Self::Buffer(buffer) = self { @@ -385,17 +421,17 @@ impl StringOrBuffer { out: &mut StringOrBuffer, global: &JSGlobalObject, value: JSValue, - is_async: bool, - allow_string_object: bool, + flavor: Flavor, + string_objects: StringObjects, ) -> JsResult { use jsc::JSType; match value.js_type() { str_type @ (JSType::String | JSType::StringObject | JSType::DerivedStringObject) => { - if !allow_string_object && str_type != JSType::String { + if string_objects == StringObjects::Reject && str_type != JSType::String { return Ok(false); } let mut str = bun_core::String::from_js(value, global)?; - if is_async { + if flavor == Flavor::Async { let mut possible_clone = str; let mut sliced = possible_clone.to_thread_safe_slice(); sliced.report_extra_memory(global.vm()); @@ -437,14 +473,14 @@ impl StringOrBuffer { | JSType::BigInt64Array | JSType::BigUint64Array | JSType::DataView => { - let buffer = if is_async { + let buffer = if flavor == Flavor::Async { Buffer::from_js_pinned(global, value) .unwrap_or_else(|| Buffer::from_array_buffer(global, value)) } else { Buffer::from_array_buffer(global, value) }; - if is_async { + if flavor == Flavor::Async { buffer.buffer.value.protect(); } @@ -459,11 +495,11 @@ impl StringOrBuffer { pub(crate) fn from_js_maybe_async( global: &JSGlobalObject, value: JSValue, - is_async: bool, - allow_string_object: bool, + flavor: Flavor, + string_objects: StringObjects, ) -> JsResult> { let mut out = Self::EMPTY; - if Self::from_js_maybe_async_into(&mut out, global, value, is_async, allow_string_object)? { + if Self::from_js_maybe_async_into(&mut out, global, value, flavor, string_objects)? { Ok(Some(out)) } else { Ok(None) @@ -472,7 +508,7 @@ impl StringOrBuffer { #[inline] pub fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult> { - Self::from_js_maybe_async(global, value, false, true) + Self::from_js_maybe_async(global, value, Flavor::Sync, StringObjects::Allow) } #[inline] @@ -481,7 +517,13 @@ impl StringOrBuffer { value: JSValue, encoding: Encoding, ) -> JsResult> { - Self::from_js_with_encoding_maybe_async(global, value, encoding, false, true) + Self::from_js_with_encoding_maybe_async( + global, + value, + encoding, + Flavor::Sync, + StringObjects::Allow, + ) } /// Out-param convenience wrapper — see [`from_js_with_encoding_maybe_async_into`]. @@ -492,7 +534,14 @@ impl StringOrBuffer { value: JSValue, encoding: Encoding, ) -> JsResult { - Self::from_js_with_encoding_maybe_async_into(out, global, value, encoding, false, true) + Self::from_js_with_encoding_maybe_async_into( + out, + global, + value, + encoding, + Flavor::Sync, + StringObjects::Allow, + ) } /// Out-param core of [`from_js_with_encoding_maybe_async`]. Writes into @@ -504,17 +553,17 @@ impl StringOrBuffer { global: &JSGlobalObject, value: JSValue, encoding: Encoding, - is_async: bool, - allow_string_object: bool, + flavor: Flavor, + string_objects: StringObjects, ) -> JsResult { if value.is_cell() && value.js_type().is_array_buffer_like() { - let buffer = if is_async { + let buffer = if flavor == Flavor::Async { Buffer::from_js_pinned(global, value) .unwrap_or_else(|| Buffer::from_array_buffer(global, value)) } else { Buffer::from_array_buffer(global, value) }; - if is_async { + if flavor == Flavor::Async { buffer.buffer.value.protect(); } *out = Self::Buffer(buffer); @@ -522,25 +571,13 @@ impl StringOrBuffer { } if encoding == Encoding::Utf8 { - return Self::from_js_maybe_async_into( - out, - global, - value, - is_async, - allow_string_object, - ); + return Self::from_js_maybe_async_into(out, global, value, flavor, string_objects); } if value.is_string() { let str = bun_core::OwnedString::new(bun_core::String::from_js(value, global)?); if str.is_empty() { - return Self::from_js_maybe_async_into( - out, - global, - value, - is_async, - allow_string_object, - ); + return Self::from_js_maybe_async_into(out, global, value, flavor, string_objects); } use crate::webcore::encoding::BunStringEncode as _; @@ -559,8 +596,8 @@ impl StringOrBuffer { global: &JSGlobalObject, value: JSValue, encoding: Encoding, - is_async: bool, - allow_string_object: bool, + flavor: Flavor, + string_objects: StringObjects, ) -> JsResult> { let mut out = Self::EMPTY; if Self::from_js_with_encoding_maybe_async_into( @@ -568,8 +605,8 @@ impl StringOrBuffer { global, value, encoding, - is_async, - allow_string_object, + flavor, + string_objects, )? { Ok(Some(out)) } else { @@ -581,7 +618,7 @@ impl StringOrBuffer { global: &JSGlobalObject, value: JSValue, encoding_value: JSValue, - allow_string_object: bool, + string_objects: StringObjects, ) -> JsResult> { let encoding: Encoding = 'brk: { if !encoding_value.is_cell() { @@ -589,13 +626,12 @@ impl StringOrBuffer { } break 'brk Encoding::from_js(encoding_value, global)?.unwrap_or(Encoding::Utf8); }; - let is_async = false; Self::from_js_with_encoding_maybe_async( global, value, encoding, - is_async, - allow_string_object, + Flavor::Sync, + string_objects, ) } } diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index f125b595d3a6..967d900f0fd7 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -24,7 +24,7 @@ use bun_jsc::SysErrorJsc; // struct directly so `VirtualMachine::get()` resolves as an associated fn. use super::upgraded_duplex::{Handlers as UpgradedDuplexHandlers, UpgradedDuplex}; use crate::crypto::boringssl_jsc::err_to_js as boringssl_err_to_js; -use crate::node::{BlobOrStringOrBuffer, StringOrBuffer}; +use crate::node::{BlobOrStringOrBuffer, StringObjects, StringOrBuffer}; use crate::socket::{SSLConfig, SSLConfigFromJs}; use bun_boringssl_sys as boringssl_sys; use bun_cares_sys::c_ares_draft as c_ares; @@ -2594,7 +2594,6 @@ impl NewSocket { return self.write_or_end::(global, &mut values, true); } - let allow_string_object = true; let buffer: StringOrBuffer = if data_value.is_undefined() { StringOrBuffer::EMPTY } else { @@ -2603,7 +2602,7 @@ impl NewSocket { // allocator dropped (global mimalloc) data_value, encoding_value, - allow_string_object, + StringObjects::Allow, ) { Ok(Some(b)) => b, Ok(None) => { diff --git a/src/runtime/valkey_jsc/js_valkey_functions.rs b/src/runtime/valkey_jsc/js_valkey_functions.rs index 468ba9104e57..533d24b7c4a2 100644 --- a/src/runtime/valkey_jsc/js_valkey_functions.rs +++ b/src/runtime/valkey_jsc/js_valkey_functions.rs @@ -1,4 +1,4 @@ -use crate::node::BlobOrStringOrBuffer as JSArgument; +use crate::node::{BlobOrStringOrBuffer as JSArgument, FileBlobs}; use bun_collections::VecExt as _; use bun_core::OwnedString; use bun_jsc::{ @@ -70,10 +70,10 @@ fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult { expect(output).toContain("hello from blob"); }); + test("in-memory file with a file-backed Blob is rejected", () => { + // Only in-memory blobs are accepted as content; a Bun.file() blob would + // have to be read from disk and is rejected like every other + // string-or-blob argument, instead of being treated as empty content. + // Like the other invalid options, this throws from Bun.build() itself. + using dir = tempDir("bundler-files-bun-file", { + "entry.js": `console.log("from disk");`, + }); + + expect(() => + Bun.build({ + entrypoints: ["/entry.js"], + files: { + "/entry.js": Bun.file(`${dir}/entry.js`), + }, + }), + ).toThrow("File blob cannot be used here"); + }); + test("in-memory file with Uint8Array content", async () => { const encoder = new TextEncoder(); const result = await Bun.build({ diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 746637c7eba1..7fd0be69245b 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2740,6 +2740,29 @@ describe("writeFileSync", () => { }); }); +describe("writeFile/appendFile data argument", () => { + it("rejects a String wrapper object on the sync and callback paths, like node", () => { + // Node only accepts primitive strings here (`new String("x")` is an + // object), unlike most other string-or-buffer arguments, which unwrap it. + // The callback forms validate before scheduling anything, so they throw + // synchronously too. (fs.promises.* is different: there node accepts any + // iterable, which a String object is.) + using dir = tempDir("fs-data-string-object", {}); + const file = (name: string) => join(String(dir), name); + const data = new String("data") as any; + + expect(() => writeFileSync(file("write-sync.txt"), data)).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + expect(() => fs.appendFileSync(file("append-sync.txt"), data)).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + expect(() => fs.writeFile(file("write-cb.txt"), data, () => {})).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + expect(() => fs.appendFile(file("append-cb.txt"), data, () => {})).toThrowWithCode( + TypeError, + "ERR_INVALID_ARG_TYPE", + ); + + expect(readdirSync(String(dir))).toEqual([]); + }); +}); + function triggerDOMJIT(target: fs.Stats, fn: (..._: any[]) => any, result: any) { for (let i = 0; i < 9999; i++) { if (fn.apply(target) !== result) {