diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index 7daa679604e..ce0cf1b8b00 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -596,9 +596,7 @@ impl<'a> Run<'a> { pub fn run(&mut self, value: JSValue) -> Result { use ConsoleObject::formatter::Tag as T; - // `Tag::get` returns `TagResult { tag: TagPayload, .. }`; - // collapse the payload to its discriminant via `.tag()`. - match T::get(value, self.global)?.tag.tag() { + match T::get(value, self.global)?.tag { T::Error => self.coerce(T::Error, value), T::Undefined => self.coerce(T::Undefined, value), T::Null => self.coerce(T::Null, value), diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index 12e6fb8b3a2..84bfb46aec0 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -4,7 +4,7 @@ use bun_ast::{E, Expr, ExprData, G, ToJSError}; use bun_collections::VecExt; -use bun_core::{StackCheck, String as BunString, strings}; +use bun_core::{StackCheck, String as BunString}; use bun_jsc::{JSGlobalObject, JSValue, JsError, bun_string_jsc}; /// Map a `bun_jsc::JsError` into the AST-layer `ToJSError`. Orphan rules forbid @@ -143,20 +143,11 @@ pub(crate) fn object_to_js( Ok(obj) } -/// Serialize UTF-8 bytes to a JS string, transcoding to UTF-16 only when the -/// bytes are not pure ASCII (`to_utf16_alloc` returns `Ok(None)` for -/// pure-ASCII, in which case the 8-bit Latin-1 form is kept). +/// Serialize UTF-8 bytes to a JS string. `createUTF8ForJS` allocates the final +/// WTF string in one pass (Latin-1 for pure-ASCII, UTF-16 with U+FFFD +/// replacement of invalid sequences otherwise). fn utf8_bytes_to_js(bytes: &[u8], global: &JSGlobalObject) -> Result { - let utf16 = strings::to_utf16_alloc(bytes, false, false).map_err(|_| ToJSError::OutOfMemory)?; - if let Some(utf16) = utf16 { - let (mut out, chars) = BunString::create_uninitialized_utf16(utf16.len()); - chars.copy_from_slice(&utf16); - bun_string_jsc::transfer_to_js(&mut out, global).map_err(js_err) - } else { - let (mut out, chars) = BunString::create_uninitialized_latin1(bytes.len()); - chars.copy_from_slice(bytes); - bun_string_jsc::transfer_to_js(&mut out, global).map_err(js_err) - } + bun_string_jsc::create_utf8_for_js(global, bytes).map_err(js_err) } /// `E.String` → JS string conversion. diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index c2741524f6a..3a842eb9b53 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -751,6 +751,116 @@ impl AsyncModule { drop(unsafe { bun_core::heap::take(this) }); } + /// Shared builder for the package resolve/download error objects: creates + /// the error instance from `msg` and sets the `url` (when present), + /// `name`, and `pkg` properties. + fn package_error_instance( + global_this: &JSGlobalObject, + msg: &[u8], + name: &[u8], + url: &[u8], + pkg: &[u8], + ) -> JSValue { + let error_instance = ZigString::from_bytes(msg) + .with_encoding() + .to_error_instance(global_this); + if !url.is_empty() { + error_instance.put( + global_this, + b"url", + ZigString::from_bytes(url) + .with_encoding() + .to_js(global_this), + ); + } + error_instance.put( + global_this, + b"name", + ZigString::from_bytes(name) + .with_encoding() + .to_js(global_this), + ); + error_instance.put( + global_this, + b"pkg", + ZigString::from_bytes(pkg) + .with_encoding() + .to_js(global_this), + ); + error_instance + } + + fn put_referrer(global_this: &JSGlobalObject, error_instance: JSValue, referrer: &[u8]) { + if !referrer.is_empty() && referrer != b"undefined" { + error_instance.put( + global_this, + b"referrer", + ZigString::from_bytes(referrer) + .with_encoding() + .to_js(global_this), + ); + } + } + + /// Sets `sourceURL`/`line`/`lineText`/`column` from the import record's + /// source location. + fn put_import_location( + &self, + global_this: &JSGlobalObject, + error_instance: JSValue, + import_record_id: u32, + ) { + let location = bun_ast::range_data( + Some(&self.parse_result.source), + self.parse_result.ast.import_records[import_record_id as usize].range, + b"", + ) + .location + .unwrap(); + error_instance.put( + global_this, + b"sourceURL", + ZigString::from_bytes(self.parse_result.source.path.text) + .with_encoding() + .to_js(global_this), + ); + error_instance.put( + global_this, + b"line", + JSValue::js_number(location.line as f64), + ); + if let Some(line_text) = location.line_text.as_deref() { + error_instance.put( + global_this, + b"lineText", + ZigString::from_bytes(line_text) + .with_encoding() + .to_js(global_this), + ); + } + error_instance.put( + global_this, + b"column", + JSValue::js_number(location.column as f64), + ); + } + + /// Rejects the module's promise with `error_instance` and drops the event + /// loop keepalive. The caller (`Queue::retain_mut`) returns `false` and + /// Vec drops the element, running Drop. + fn reject_with(&mut self, global_this: &JSGlobalObject, error_instance: JSValue) { + let promise_value = self.promise.swap(); + let promise = promise_value.as_internal_promise().unwrap(); + promise_value.ensure_still_alive(); + self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( + bun_io::AllocatorType::Js, + )); + // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is + // the centralised non-null deref proof. + let _ = + JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + } + // write! into Vec // is infallible here; `.ok()` collapses the `fmt::Result`, so this never // actually returns Err — the wide Result is kept for call-site uniformity. @@ -870,32 +980,8 @@ impl AsyncModule { b"PackageResolveError" }; - let error_instance = ZigString::from_bytes(&msg) - .with_encoding() - .to_error_instance(global_this); - if !result.url.is_empty() { - error_instance.put( - global_this, - b"url", - ZigString::from_bytes(result.url) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"name", - ZigString::from_bytes(name) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"pkg", - ZigString::from_bytes(result.name) - .with_encoding() - .to_js(global_this), - ); + let error_instance = + Self::package_error_instance(global_this, &msg, name, result.url, result.name); error_instance.put( global_this, b"specifier", @@ -903,63 +989,11 @@ impl AsyncModule { .with_encoding() .to_js(global_this), ); - let location = bun_ast::range_data( - Some(&self.parse_result.source), - self.parse_result.ast.import_records[import_record_id as usize].range, - b"", - ) - .location - .unwrap(); - error_instance.put( - global_this, - b"sourceURL", - ZigString::from_bytes(self.parse_result.source.path.text) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"line", - JSValue::js_number(location.line as f64), - ); - if let Some(line_text) = location.line_text.as_deref() { - error_instance.put( - global_this, - b"lineText", - ZigString::from_bytes(line_text) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"column", - JSValue::js_number(location.column as f64), - ); - let referrer = self.referrer(); - if !referrer.is_empty() && referrer != b"undefined" { - error_instance.put( - global_this, - b"referrer", - ZigString::from_bytes(referrer) - .with_encoding() - .to_js(global_this), - ); - } + self.put_import_location(global_this, error_instance, import_record_id); + Self::put_referrer(global_this, error_instance, self.referrer()); - let promise_value = self.promise.swap(); - let promise = promise_value.as_internal_promise().unwrap(); - promise_value.ensure_still_alive(); let _ = vm; - self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( - bun_io::AllocatorType::Js, - )); - // The caller (Queue::retain_mut) returns `false` and Vec drops the - // element, running Drop. - // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null deref proof. - let _ = - JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + self.reject_with(global_this, error_instance); Ok(()) } @@ -1085,50 +1119,12 @@ impl AsyncModule { b"TarballDownloadError" }; - let error_instance = ZigString::from_bytes(&msg) - .with_encoding() - .to_error_instance(global_this); - if !result.url.is_empty() { - error_instance.put( - global_this, - b"url", - ZigString::from_bytes(result.url) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"name", - ZigString::from_bytes(name) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"pkg", - ZigString::from_bytes(result.name) - .with_encoding() - .to_js(global_this), - ); - let specifier = self.specifier(); - if !specifier.is_empty() && specifier != b"undefined" { - error_instance.put( - global_this, - b"referrer", - ZigString::from_bytes(specifier) - .with_encoding() - .to_js(global_this), - ); - } - - let location = bun_ast::range_data( - Some(&self.parse_result.source), - self.parse_result.ast.import_records[import_record_id as usize].range, - b"", - ) - .location - .unwrap(); + let error_instance = + Self::package_error_instance(global_this, &msg, name, result.url, result.name); + Self::put_referrer(global_this, error_instance, self.specifier()); + // `sourceURL` et al. follow `specifier` here (the resolve-error path + // puts `specifier` first), so the helper runs after this put; the + // location computation itself is pure. error_instance.put( global_this, b"specifier", @@ -1140,45 +1136,10 @@ impl AsyncModule { .with_encoding() .to_js(global_this), ); - error_instance.put( - global_this, - b"sourceURL", - ZigString::from_bytes(self.parse_result.source.path.text) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"line", - JSValue::js_number(location.line as f64), - ); - if let Some(line_text) = location.line_text.as_deref() { - error_instance.put( - global_this, - b"lineText", - ZigString::from_bytes(line_text) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"column", - JSValue::js_number(location.column as f64), - ); + self.put_import_location(global_this, error_instance, import_record_id); - let promise_value = self.promise.swap(); - let promise = promise_value.as_internal_promise().unwrap(); - promise_value.ensure_still_alive(); let _ = vm; - self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( - bun_io::AllocatorType::Js, - )); - // Caller drops via retain_mut → false. - // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null deref proof. - let _ = - JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + self.reject_with(global_this, error_instance); Ok(()) } diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 96455a9a651..6ae32f80169 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -701,10 +701,8 @@ impl<'a> TablePrinter<'a> { let mut value_formatter = self.value_formatter.shallow_clone(); let tag = formatter::Tag::get(value, self.global_object)?; - value_formatter.quote_strings = !(matches!( - tag.tag, - TagPayload::String | TagPayload::StringPossiblyFormatted - )); + value_formatter.quote_strings = + !(matches!(tag.tag, Tag::String | Tag::StringPossiblyFormatted)); let _ = value_formatter.format::(tag, &mut counter, value, self.global_object); // VisibleCharacterCounter write cannot fail. let _ = bun_io::Write::flush(&mut counter); @@ -882,10 +880,8 @@ impl<'a> TablePrinter<'a> { let tag = formatter::Tag::get(value, self.global_object)?; let mut value_formatter = self.value_formatter.shallow_clone(); - value_formatter.quote_strings = !(matches!( - tag.tag, - TagPayload::String | TagPayload::StringPossiblyFormatted - )); + value_formatter.quote_strings = + !(matches!(tag.tag, Tag::String | Tag::StringPossiblyFormatted)); // Release pooled visit map after formatting. // `shallow_clone()` guarantees the source's `map_node` is @@ -1485,7 +1481,7 @@ pub fn format2( return Ok(()); } - if matches!(tag.tag, TagPayload::String) { + if matches!(tag.tag, Tag::String) { if options.enable_colors { if level == MessageLevel::Error { let _ = writer.write_all(pfmt!("", true).as_bytes()); @@ -1561,8 +1557,8 @@ pub fn format2( any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1583,8 +1579,8 @@ pub fn format2( } any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1613,7 +1609,7 @@ pub struct CustomFormattedObject { // Formatter // ─────────────────────────────────────────────────────────────────────────── -pub use formatter::{Formatter, Tag, TagOptions, TagPayload, TagResult, visited}; +pub use formatter::{Formatter, Tag, TagOptions, TagResult, visited}; pub mod formatter { use super::*; @@ -2055,156 +2051,20 @@ pub mod formatter { } } - /// Only `CustomFormattedObject` carries a payload. - #[derive(Copy, Clone, PartialEq, Eq)] - pub enum TagPayload { - StringPossiblyFormatted, - String, - Undefined, - Double, - Integer, - Null, - Boolean, - Array, - Object, - Function, - Class, - Error, - TypedArray, - Map, - MapIterator, - SetIterator, - Set, - BigInt, - Symbol, - CustomFormattedObject(CustomFormattedObject), - GlobalObject, - Private, - Promise, - JSON, - ToJSON, - NativeCode, - JSX, - Event, - GetterSetter, - CustomGetterSetter, - Proxy, - RevokedProxy, - } - - impl TagPayload { - /// The constructor lives here as well as on the bare - /// discriminant `Tag`. Callers in sibling modules use either name. - #[inline] - pub fn get(value: JSValue, global_this: &JSGlobalObject) -> JsResult { - Tag::get(value, global_this) - } - /// Delegates to `Tag::get_advanced`. - #[inline] - pub fn get_advanced( - value: JSValue, - global_this: &JSGlobalObject, - opts: TagOptions, - ) -> JsResult { - Tag::get_advanced(value, global_this, opts) - } - pub fn is_primitive(self) -> bool { - self.tag().is_primitive() - } - pub fn tag(self) -> Tag { - match self { - TagPayload::StringPossiblyFormatted => Tag::StringPossiblyFormatted, - TagPayload::String => Tag::String, - TagPayload::Undefined => Tag::Undefined, - TagPayload::Double => Tag::Double, - TagPayload::Integer => Tag::Integer, - TagPayload::Null => Tag::Null, - TagPayload::Boolean => Tag::Boolean, - TagPayload::Array => Tag::Array, - TagPayload::Object => Tag::Object, - TagPayload::Function => Tag::Function, - TagPayload::Class => Tag::Class, - TagPayload::Error => Tag::Error, - TagPayload::TypedArray => Tag::TypedArray, - TagPayload::Map => Tag::Map, - TagPayload::MapIterator => Tag::MapIterator, - TagPayload::SetIterator => Tag::SetIterator, - TagPayload::Set => Tag::Set, - TagPayload::BigInt => Tag::BigInt, - TagPayload::Symbol => Tag::Symbol, - TagPayload::CustomFormattedObject(_) => Tag::CustomFormattedObject, - TagPayload::GlobalObject => Tag::GlobalObject, - TagPayload::Private => Tag::Private, - TagPayload::Promise => Tag::Promise, - TagPayload::JSON => Tag::JSON, - TagPayload::ToJSON => Tag::ToJSON, - TagPayload::NativeCode => Tag::NativeCode, - TagPayload::JSX => Tag::JSX, - TagPayload::Event => Tag::Event, - TagPayload::GetterSetter => Tag::GetterSetter, - TagPayload::CustomGetterSetter => Tag::CustomGetterSetter, - TagPayload::Proxy => Tag::Proxy, - TagPayload::RevokedProxy => Tag::RevokedProxy, - } - } - } - - /// Reverse of [`TagPayload::tag`]. The `CustomFormattedObject` arm gets a - /// default (zero) payload — used by the `ConsoleFormatter` trait bridge in - /// `lib.rs`, which never passes that tag (write_format hooks pick concrete - /// tags like `Double` / `Boolean` / `Object` / `Private`). - impl From for TagPayload { - fn from(t: Tag) -> Self { - match t { - Tag::StringPossiblyFormatted => TagPayload::StringPossiblyFormatted, - Tag::String => TagPayload::String, - Tag::Undefined => TagPayload::Undefined, - Tag::Double => TagPayload::Double, - Tag::Integer => TagPayload::Integer, - Tag::Null => TagPayload::Null, - Tag::Boolean => TagPayload::Boolean, - Tag::Array => TagPayload::Array, - Tag::Object => TagPayload::Object, - Tag::Function => TagPayload::Function, - Tag::Class => TagPayload::Class, - Tag::Error => TagPayload::Error, - Tag::TypedArray => TagPayload::TypedArray, - Tag::Map => TagPayload::Map, - Tag::MapIterator => TagPayload::MapIterator, - Tag::SetIterator => TagPayload::SetIterator, - Tag::Set => TagPayload::Set, - Tag::BigInt => TagPayload::BigInt, - Tag::Symbol => TagPayload::Symbol, - Tag::CustomFormattedObject => { - TagPayload::CustomFormattedObject(CustomFormattedObject::default()) - } - Tag::GlobalObject => TagPayload::GlobalObject, - Tag::Private => TagPayload::Private, - Tag::Promise => TagPayload::Promise, - Tag::JSON => TagPayload::JSON, - Tag::ToJSON => TagPayload::ToJSON, - Tag::NativeCode => TagPayload::NativeCode, - Tag::JSX => TagPayload::JSX, - Tag::Event => TagPayload::Event, - Tag::GetterSetter => TagPayload::GetterSetter, - Tag::CustomGetterSetter => TagPayload::CustomGetterSetter, - Tag::Proxy => TagPayload::Proxy, - Tag::RevokedProxy => TagPayload::RevokedProxy, - } - } - } - #[derive(Copy, Clone)] pub struct TagResult { - pub tag: TagPayload, + pub tag: Tag, pub cell: jsc::JSType, + /// Set only when `tag` is [`Tag::CustomFormattedObject`]. + pub custom: Option, } impl Default for TagResult { fn default() -> Self { Self { - tag: TagPayload::Undefined, + tag: Tag::Undefined, cell: jsc::JSType::Cell, + custom: None, } } } @@ -2231,37 +2091,37 @@ pub mod formatter { ) -> JsResult { if value.is_empty() || value == JSValue::UNDEFINED { return Ok(TagResult { - tag: TagPayload::Undefined, + tag: Tag::Undefined, ..Default::default() }); } if value == JSValue::NULL { return Ok(TagResult { - tag: TagPayload::Null, + tag: Tag::Null, ..Default::default() }); } if value.is_int32() { return Ok(TagResult { - tag: TagPayload::Integer, + tag: Tag::Integer, ..Default::default() }); } else if value.is_number() { return Ok(TagResult { - tag: TagPayload::Double, + tag: Tag::Double, ..Default::default() }); } else if value.is_boolean() { return Ok(TagResult { - tag: TagPayload::Boolean, + tag: Tag::Boolean, ..Default::default() }); } if !value.is_cell() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, ..Default::default() }); } @@ -2270,15 +2130,17 @@ pub mod formatter { if js_type.is_hidden() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } if js_type == jsc::JSType::Cell { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } @@ -2290,17 +2152,18 @@ pub mod formatter { match value.fast_get(global_this, jsc::BuiltinName::InspectCustom) { Err(_) => { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, ..Default::default() }); } Ok(Some(callback_value)) if callback_value.is_callable() => { return Ok(TagResult { - tag: TagPayload::CustomFormattedObject(CustomFormattedObject { + tag: Tag::CustomFormattedObject, + cell: js_type, + custom: Some(CustomFormattedObject { function: callback_value, this: value, }), - cell: js_type, }); } _ => {} @@ -2309,8 +2172,9 @@ pub mod formatter { if js_type == jsc::JSType::DOMWrapper { return Ok(TagResult { - tag: TagPayload::Private, + tag: Tag::Private, cell: js_type, + custom: None, }); } @@ -2321,8 +2185,9 @@ pub mod formatter { { if value.is_class(global_this) { return Ok(TagResult { - tag: TagPayload::Class, + tag: Tag::Class, cell: js_type, + custom: None, }); } @@ -2334,11 +2199,12 @@ pub mod formatter { // handle the prefix in the .Object formatter. return Ok(TagResult { tag: if js_type == jsc::JSType::InternalFunction { - TagPayload::Object + Tag::Object } else { - TagPayload::Function + Tag::Function }, cell: js_type, + custom: None, }); } @@ -2353,8 +2219,9 @@ pub mod formatter { return Tag::get(target, global_this); } return Ok(TagResult { - tag: TagPayload::GlobalObject, + tag: Tag::GlobalObject, cell: js_type, + custom: None, }); } @@ -2379,8 +2246,9 @@ pub mod formatter { global_this, )? { return Ok(TagResult { - tag: TagPayload::JSX, + tag: Tag::JSX, cell: js_type, + custom: None, }); } } @@ -2388,24 +2256,24 @@ pub mod formatter { use jsc::JSType as T; let tag = match js_type { - T::ErrorInstance => TagPayload::Error, - T::NumberObject => TagPayload::Double, + T::ErrorInstance => Tag::Error, + T::NumberObject => Tag::Double, T::DerivedArray | T::Array | T::DirectArguments | T::ScopedArguments - | T::ClonedArguments => TagPayload::Array, - T::DerivedStringObject | T::String | T::StringObject => TagPayload::String, - T::RegExpObject => TagPayload::String, - T::Symbol => TagPayload::Symbol, - T::BooleanObject => TagPayload::Boolean, - T::JSFunction => TagPayload::Function, - T::WeakMap | T::Map => TagPayload::Map, - T::MapIterator => TagPayload::MapIterator, - T::SetIterator => TagPayload::SetIterator, - T::WeakSet | T::Set => TagPayload::Set, - T::JSDate => TagPayload::JSON, - T::JSPromise => TagPayload::Promise, + | T::ClonedArguments => Tag::Array, + T::DerivedStringObject | T::String | T::StringObject => Tag::String, + T::RegExpObject => Tag::String, + T::Symbol => Tag::Symbol, + T::BooleanObject => Tag::Boolean, + T::JSFunction => Tag::Function, + T::WeakMap | T::Map => Tag::Map, + T::MapIterator => Tag::MapIterator, + T::SetIterator => Tag::SetIterator, + T::WeakSet | T::Set => Tag::Set, + T::JSDate => Tag::JSON, + T::JSPromise => Tag::Promise, T::WrapForValidIterator | T::RegExpStringIterator @@ -2414,43 +2282,31 @@ pub mod formatter { | T::IteratorHelper | T::Object | T::FinalObject - | T::ModuleNamespaceObject => TagPayload::Object, + | T::ModuleNamespaceObject => Tag::Object, T::ProxyObject => { let handler = value.get_proxy_internal_field(jsc::ProxyField::Handler); if handler.is_empty() || handler.is_undefined_or_null() { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, cell: js_type, + custom: None, }); } - TagPayload::Proxy + Tag::Proxy } T::GlobalObject => { if !opts.contains(TagOptions::HIDE_GLOBAL) { - TagPayload::Object + Tag::Object } else { - TagPayload::GlobalObject + Tag::GlobalObject } } - T::ArrayBuffer - | T::Int8Array - | T::Uint8Array - | T::Uint8ClampedArray - | T::Int16Array - | T::Uint16Array - | T::Int32Array - | T::Uint32Array - | T::Float16Array - | T::Float32Array - | T::Float64Array - | T::BigInt64Array - | T::BigUint64Array - | T::DataView => TagPayload::TypedArray, - - T::HeapBigInt => TagPayload::BigInt, + t if t.is_array_buffer_like() => Tag::TypedArray, + + T::HeapBigInt => Tag::BigInt, // None of these should ever exist here // But we're going to check anyway @@ -2473,18 +2329,22 @@ pub mod formatter { | T::LexicalEnvironment | T::ModuleEnvironment | T::StrictEvalActivation - | T::WithScope => TagPayload::NativeCode, + | T::WithScope => Tag::NativeCode, - T::Event => TagPayload::Event, + T::Event => Tag::Event, - T::GetterSetter => TagPayload::GetterSetter, - T::CustomGetterSetter => TagPayload::CustomGetterSetter, + T::GetterSetter => Tag::GetterSetter, + T::CustomGetterSetter => Tag::CustomGetterSetter, - T::JSAsJSONType => TagPayload::ToJSON, + T::JSAsJSONType => Tag::ToJSON, - _ => TagPayload::JSON, + _ => Tag::JSON, }; - Ok(TagResult { tag, cell: js_type }) + Ok(TagResult { + tag, + cell: js_type, + custom: None, + }) } } @@ -2507,11 +2367,7 @@ pub mod formatter { slice_: &[u8], global: &'a JSGlobalObject, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut slice = slice_; let mut i: u32 = 0; let mut len: u32 = slice.len() as u32; @@ -2588,11 +2444,8 @@ pub mod formatter { next_value, next_value.js_type(), )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::I => { // 1. If Type(current) is Symbol, let converted be NaN @@ -2740,11 +2593,8 @@ pub mod formatter { next_value, global, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::C => { @@ -2789,6 +2639,14 @@ pub mod formatter { impl<'w> WrappedWriter<'w> { pub const IS_WRAPPED_WRITER: bool = true; + pub fn new(ctx: &'w mut dyn bun_io::Write, estimated_line_length: &'w mut usize) -> Self { + Self { + ctx, + failed: false, + estimated_line_length, + } + } + /// Mirror of `Formatter::add_for_new_line` routed through the borrowed /// `estimated_line_length` so callers don't need a second `&mut self` /// on the parent `Formatter` while a `WrappedWriter` is live. @@ -3141,11 +2999,8 @@ pub mod formatter { value: JSValue, ) -> JsResult<()> { if value.is_cell() && !value.js_type().is_function() { - let mut writer = WrappedWriter { - ctx: self.writer, - failed: false, - estimated_line_length: &mut self.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(self.writer, &mut self.formatter.estimated_line_length); if let Some(name_str) = get_object_name(global_this, value)? { writer.print(format_args!("{name_str} ")); @@ -3301,11 +3156,8 @@ pub mod formatter { } } - let mut writer = WrappedWriter { - ctx: &mut *ctx.writer, - failed: false, - estimated_line_length: &mut ctx.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(&mut *ctx.writer, &mut ctx.formatter.estimated_line_length); if ctx.i > 0 { writer.print_comma::(); } @@ -3592,11 +3444,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(9); writer.print(format_args!( "{}undefined{}", @@ -3611,11 +3459,7 @@ pub mod formatter { #[inline(never)] fn print_null(&mut self, writer_: &mut dyn bun_io::Write) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(4); writer.print(format_args!( "{}null{}", @@ -3634,11 +3478,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if let Some(class_name) = value.get_class_info_name() { writer.add_for_new_line("[native code: ]".len() + class_name.len()); writer.write_all(b"[native code: "); @@ -3659,11 +3499,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); const FMT: &str = "[Global Object]"; writer.add_for_new_line(FMT.len()); writer.write_all(pfmt!(concat!("", "[Global Object]", ""), C).as_bytes()); @@ -3678,11 +3514,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line("".len()); writer.print(format_args!( "{}{}", @@ -3737,11 +3569,7 @@ pub mod formatter { // This is called from the '%s' formatter, so it can actually be any value use crate::StringJsc as _; let str = OwnedString::new(BunString::from_js(value, self.global_this)?); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(str.length()); if self.quote_strings && js_type != jsc::JSType::RegExpObject { @@ -3791,11 +3619,7 @@ pub mod formatter { self.failed = true; } self.print_as::(Tag::JSON, writer_, value, jsc::JSType::StringObject)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } else { JSPrinter::write_json_string( str.latin1(), @@ -3849,11 +3673,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let int = value.coerce_to_int64(self.global_this)?; writer.add_for_new_line(bun_core::fmt::digit_count(int)); writer.print(format_args!( @@ -3874,11 +3694,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let zstr = value.get_zig_string(self.global_this)?; let out_str = zstr.slice(); writer.add_for_new_line(out_str.len()); @@ -3900,16 +3716,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut number_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut number_name)?; @@ -3923,10 +3730,10 @@ pub mod formatter { ); writer.print(format_args!( "{}[Number ({}): {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3937,10 +3744,10 @@ pub mod formatter { writer.add_for_new_line(number_name.len + number_value.len + 4); writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3952,26 +3759,34 @@ pub mod formatter { if num.is_infinite() && num > 0.0 { writer.add_for_new_line("Infinity".len()); - writer.print(format_args!("{}Infinity{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}Infinity{}", + pfmt!("", C), + pfmt!("", C) + )); } else if num.is_infinite() && num < 0.0 { writer.add_for_new_line("-Infinity".len()); writer.print(format_args!( "{}-Infinity{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else if num.is_nan() { writer.add_for_new_line("NaN".len()); - writer.print(format_args!("{}NaN{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}NaN{}", + pfmt!("", C), + pfmt!("", C) + )); } else { let mut buf = [0u8; 124]; let formatted = bun_core::fmt::FormatDouble::dtoa_with_negative_zero(&mut buf, num); writer.add_for_new_line(formatted.len()); writer.print(format_args!( "{}{}{}", - pf!(""), + pfmt!("", C), bstr::BStr::new(formatted), - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4024,11 +3839,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let description = value.get_description(self.global_this); writer.add_for_new_line("Symbol".len()); @@ -4091,16 +3902,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // Prefer the constructor's own `.name` property over // `getClassName` / `calculatedClassName`. For DOM / WebCore // InternalFunction constructors like `ReadableStreamBYOBReader`, @@ -4130,31 +3932,31 @@ pub mod formatter { if printable_proto.is_empty() { writer.print(format_args!( "{}[class (anonymous)]{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class (anonymous) extends {}]{}", - pf!(""), + pfmt!("", C), printable_proto, - pf!("") + pfmt!("", C) )); } } else if printable_proto.is_empty() { writer.print(format_args!( "{}[class {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class {} extends {}]{}", - pf!(""), + pfmt!("", C), printable, printable_proto, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4169,16 +3971,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let printable = OwnedString::new(value.get_name(self.global_this)?); let proto = value.get_prototype(self.global_this); @@ -4187,29 +3980,33 @@ pub mod formatter { if printable.is_empty() || func_name.eql(&printable) { if func_name.is_empty() { - writer.print(format_args!("{}[Function]{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}[Function]{}", + pfmt!("", C), + pfmt!("", C) + )); } else { writer.print(format_args!( "{}[{}]{}", - pf!(""), + pfmt!("", C), func_name, - pf!("") + pfmt!("", C) )); } } else if func_name.is_empty() { writer.print(format_args!( "{}[Function: {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), func_name, printable, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4224,11 +4021,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // `JSCell` is an `opaque_ffi!` ZST handle; `opaque_ref` is the // centralised non-null deref proof (tag only produced for cells). let cell = jsc::JSCell::opaque_ref(value.to_cell().expect("GetterSetter is a cell")); @@ -4270,11 +4063,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if !self.single_line && writer.good_time_for_a_new_line(self.indent) { writer.write_all(b"\n"); writer.write_indent(self.indent); @@ -4307,16 +4096,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut bool_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut bool_name)?; @@ -4328,10 +4108,10 @@ pub mod formatter { .add_for_new_line(bool_value.len + bool_name.len + "[Boolean (): ]".len()); writer.print(format_args!( "{}[Boolean ({}): {}]{}", - pf!(""), + pfmt!("", C), bool_name, bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4341,9 +4121,9 @@ pub mod formatter { writer.add_for_new_line(bool_value.len + "[Boolean: ]".len()); writer.print(format_args!( "{}[Boolean: {}]{}", - pf!(""), + pfmt!("", C), bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4352,10 +4132,10 @@ pub mod formatter { } if value.to_boolean() { writer.add_for_new_line(4); - writer.write_all(pf!("true").as_bytes()); + writer.write_all(pfmt!("true", C).as_bytes()); } else { writer.add_for_new_line(5); - writer.write_all(pf!("false").as_bytes()); + writer.write_all(pfmt!("false", C).as_bytes()); } if writer.failed { self.failed = true; @@ -4397,11 +4177,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut str = OwnedString::new(BunString::empty()); value.json_stringify(self.global_this, self.indent, &mut str)?; @@ -4455,16 +4231,7 @@ pub mod formatter { // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let len = value.get_length(self.global_this)?; @@ -4518,11 +4285,7 @@ pub mod formatter { } self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4550,9 +4313,9 @@ pub mod formatter { "... N more items".len(), format_args!( "{}... {} more items{}", - pf!(""), + pfmt!("", C), len - u64::from(i), - pf!("") + pfmt!("", C) ), ); break; @@ -4577,7 +4340,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4585,9 +4348,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4608,11 +4371,7 @@ pub mod formatter { let tag = Tag::get_advanced(element, self.global_this, tag_opts)?; self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4639,7 +4398,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4647,9 +4406,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4679,11 +4438,7 @@ pub mod formatter { if self.failed { return Ok(()); } - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } } @@ -5008,12 +4763,6 @@ pub mod formatter { value: JSValue, remove_before_recurse: &mut bool, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } - let event_type_value: JSValue = 'brk: { let Some(value_) = value.get(self.global_this, "type")? else { break 'brk JSValue::UNDEFINED; @@ -5052,9 +4801,9 @@ pub mod formatter { let _ = writeln!( writer_, "{}{}{} {{", - pf!(""), + pfmt!("", C), event_tag_name, - pf!("") + pfmt!("", C) ); { self.indent += 1; @@ -5070,23 +4819,23 @@ pub mod formatter { let _ = write!( writer_, "{}type: {}\"{}\"{}{},{} ", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } else { let _ = writeln!( writer_, "{}type: {}\"{}\"{}{},{}", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } @@ -5100,9 +4849,9 @@ pub mod formatter { let _ = write!( writer_, "{}message{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(message_value, self.global_this, self.tag_opts())?; @@ -5125,9 +4874,9 @@ pub mod formatter { let _ = write!( writer_, "{}data{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let data: JSValue = value .fast_get(self.global_this, jsc::BuiltinName::Data)? @@ -5152,9 +4901,9 @@ pub mod formatter { let _ = write!( writer_, "{}error{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(error_value, self.global_this, self.tag_opts())?; @@ -5188,22 +4937,13 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } // Cache once: `disable_inspect_custom` does not change inside this // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); writer.write_all(b"<"); // Both arms of the `type` if/else below assign these, so deferred @@ -5241,13 +4981,13 @@ pub mod formatter { } if !is_tag_kind_primitive { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } if let Some(key_value) = value.get(self.global_this, "key")? { @@ -5271,11 +5011,7 @@ pub mod formatter { key_value, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); needs_space = true; } @@ -5331,10 +5067,10 @@ pub mod formatter { writer.print(format_args!( "{}{}{}={}", - pf!(""), + pfmt!("", C), prop.trunc(128), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); if tag.cell.is_string_like() && C { @@ -5345,11 +5081,7 @@ pub mod formatter { self.failed = true; } self.format::(tag, writer_, property_value, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -5380,12 +5112,11 @@ pub mod formatter { if let Some(children) = children_prop { let tag = Tag::get(children, self.global_this)?; - let print_children = - matches!(tag.tag.tag(), Tag::String | Tag::JSX | Tag::Array); + let print_children = matches!(tag.tag, Tag::String | Tag::JSX | Tag::Array); if print_children && !self.single_line { 'print_children: { - match tag.tag.tag() { + match tag.tag { Tag::String => { let children_string = children.get_zig_string(self.global_this)?; @@ -5426,12 +5157,10 @@ pub mod formatter { children, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); } writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5474,12 +5203,10 @@ pub mod formatter { child, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); if (j as u64) + 1 < length { writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5497,13 +5224,13 @@ pub mod formatter { writer.write_all(b"").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(b">"); } @@ -5611,11 +5338,6 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } if self.single_line { let _ = writer_.write_all(b" "); } else if self.always_newline_scope || self.good_time_for_a_new_line() { @@ -5631,9 +5353,9 @@ pub mod formatter { let _ = write!( writer_, "{}[{} ...]{}", - pf!(""), + pfmt!("", C), display_name, - pf!("") + pfmt!("", C) ); Ok(()) } @@ -5683,11 +5405,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let array_buffer = value.as_array_buffer(self.global_this).unwrap(); let slice = array_buffer.byte_slice(); @@ -5826,10 +5544,10 @@ pub mod formatter { let _restore = defer_restore!(self.global_this, prev_global_this); self.global_this = global_this; - if let TagPayload::CustomFormattedObject(obj) = result.tag { + if let Some(obj) = result.custom { self.custom_formatted_object = obj; } - self.print_as::(result.tag.tag(), writer, value, result.cell) + self.print_as::(result.tag, writer, value, result.cell) } } diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index bc2fe0fa2cb..67fa8981ed4 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -3,150 +3,33 @@ use core::ptr::NonNull; use bun_core::String; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -bun_opaque::opaque_ffi! { - /// Opaque handle to a WebKit `WTF::URL` allocated on the C++ side. - pub struct URL; -} +// The JSC-agnostic surface (constructors, getters, `destroy`, the +// whole-string conversions) lives in `bun_url::whatwg`; only the entry +// points that need `JSValue`/`JSGlobalObject` stay in this crate, as the +// `UrlJsc` extension trait. +pub use bun_url::whatwg::URL; -// Getters take `&URL` (non-null `*const URL` at the C ABI; BunString.cpp never -// mutates the WTF::URL on read). `&mut String` for the in/out params is -// ABI-identical to non-null `*mut String`. `URL__deinit` consumes the C++ -// allocation, so it keeps a raw pointer and stays `unsafe fn`. unsafe extern "C" { safe fn URL__fromJS(value: JSValue, global: &JSGlobalObject) -> *mut URL; - safe fn URL__fromString(input: &mut String) -> *mut URL; - safe fn URL__protocol(url: &URL) -> String; - safe fn URL__href(url: &URL) -> String; - safe fn URL__username(url: &URL) -> String; - safe fn URL__password(url: &URL) -> String; - safe fn URL__search(url: &URL) -> String; - safe fn URL__host(url: &URL) -> String; - safe fn URL__hostname(url: &URL) -> String; - safe fn URL__port(url: &URL) -> u32; - fn URL__deinit(url: *mut URL); - safe fn URL__pathname(url: &URL) -> String; safe fn URL__getHrefFromJS(value: JSValue, global: &JSGlobalObject) -> String; - safe fn URL__getHref(input: &mut String) -> String; - safe fn URL__getFileURLString(input: &mut String) -> String; - safe fn URL__getHrefJoin(base: &mut String, relative: &mut String) -> String; - safe fn URL__pathFromFileURL(input: &mut String) -> String; - safe fn URL__hash(url: &URL) -> String; - safe fn URL__fragmentIdentifier(url: &URL) -> String; } -impl URL { - /// Includes the leading '#'. - pub fn hash(&self) -> String { - URL__hash(self) - } - - /// Exactly the same as hash, excluding the leading '#'. - pub fn fragment_identifier(&self) -> String { - URL__fragmentIdentifier(self) - } - - pub fn href_from_string(str: String) -> String { - let mut input = str; - URL__getHref(&mut input) - } - - pub fn join(base: String, relative: String) -> String { - let mut base_str = base; - let mut relative_str = relative; - URL__getHrefJoin(&mut base_str, &mut relative_str) - } - - pub fn file_url_from_string(str: String) -> String { - let mut input = str; - URL__getFileURLString(&mut input) - } - - pub fn path_from_file_url(str: String) -> String { - let mut input = str; - URL__pathFromFileURL(&mut input) - } +pub trait UrlJsc: Sized { + /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result. + /// If it fails, the tag is marked Dead. + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult; + /// Returns an owned C++ heap pointer that the caller must `destroy()`. + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>>; +} - /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result - /// If it fails, the tag is marked Dead +impl UrlJsc for URL { #[track_caller] - pub fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { crate::call_check_slow(global, || URL__getHrefFromJS(value, global)) } #[track_caller] - pub fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { crate::call_check_slow(global, || URL__fromJS(value, global)).map(NonNull::new) } - - pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(String::borrow_utf8(input)) - } - - pub fn from_string(str: String) -> Option> { - let mut input = str; - NonNull::new(URL__fromString(&mut input)) - } - // from_js/from_string/from_utf8 return an owned C++ heap pointer that the - // caller must destroy(). - - pub fn protocol(&self) -> String { - URL__protocol(self) - } - - pub fn href(&self) -> String { - URL__href(self) - } - - pub fn username(&self) -> String { - URL__username(self) - } - - pub fn password(&self) -> String { - URL__password(self) - } - - pub fn search(&self) -> String { - URL__search(self) - } - - /// Returns the host WITHOUT the port. - /// - /// Note that this does NOT match JS behavior, which returns the host with the port. See - /// `hostname` for the JS equivalent of `host`. - /// - /// ```text - /// URL("http://example.com:8080").host() => "example.com" - /// ``` - pub fn host(&self) -> String { - URL__host(self) - } - - /// Returns the host WITH the port. - /// - /// Note that this does NOT match JS behavior which returns the host without the port. See - /// `host` for the JS equivalent of `hostname`. - /// - /// ```text - /// URL("http://example.com:8080").hostname() => "example.com:8080" - /// ``` - pub fn hostname(&self) -> String { - URL__hostname(self) - } - - /// Returns `u32::MAX` if the port is not set. Otherwise, `port` - /// is guaranteed to be within the `u16` range. - pub fn port(&self) -> u32 { - URL__port(self) - } - - // Kept as explicit destroy (not Drop) — URL is an opaque #[repr(C)] FFI - // handle constructed/destroyed across the C++ boundary. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` is a valid *URL from C++; freed exactly once - unsafe { URL__deinit(this) } - } - - pub fn pathname(&self) -> String { - URL__pathname(self) - } } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e847b6a6627..4c91a9ea7d7 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -2379,17 +2379,14 @@ impl VirtualMachine { } } - /// `loadEntryPoint(entry_path)` — `reload_entry_point` + spin until the - /// returned promise settles. - pub fn load_entry_point( - &mut self, - entry_path: &[u8], - ) -> Result<*mut JSInternalPromise, bun_core::Error> { - let promise = self.reload_entry_point(entry_path)?; - + /// Shared wait body of [`load_entry_point`](Self::load_entry_point) / + /// [`load_entry_point_for_test_runner`](Self::load_entry_point_for_test_runner): + /// spin the event loop until the entry-point promise settles. Returns + /// `true` when `promise` was already rejected before waiting — callers + /// return it as-is, skipping their trailing tick/unwrap. + fn wait_for_entry_point_promise(&mut self, promise: *mut JSInternalPromise) -> bool { // pending_internal_promise can change if hot module reloading is enabled if self.is_watcher_enabled() { - // accessed here (no overlapping `&mut EventLoop`). self.event_loop_mut().perform_gc(); loop { let Some(p) = self.pending_internal_promise else { @@ -2411,12 +2408,24 @@ impl VirtualMachine { } else { // SAFETY: `promise` is a live JSC heap cell. if crate::JSPromise::status_ptr(promise) == crate::js_promise::Status::Rejected { - return Ok(promise); + return true; } self.event_loop_mut().perform_gc(); self.wait_for_promise(jsc::AnyPromise::Internal(promise)); } + false + } + /// `loadEntryPoint(entry_path)` — `reload_entry_point` + spin until the + /// returned promise settles. + pub fn load_entry_point( + &mut self, + entry_path: &[u8], + ) -> Result<*mut JSInternalPromise, bun_core::Error> { + let promise = self.reload_entry_point(entry_path)?; + if self.wait_for_entry_point_promise(promise) { + return Ok(promise); + } Ok(self.pending_internal_promise.unwrap_or(promise)) } @@ -4562,36 +4571,9 @@ impl VirtualMachine { entry_path: &[u8], ) -> Result<*mut JSInternalPromise, bun_core::Error> { let promise = self.reload_entry_point_for_test_runner(entry_path)?; - - // pending_internal_promise can change if hot module reloading is enabled - if self.is_watcher_enabled() { - self.event_loop_mut().perform_gc(); - loop { - let Some(p) = self.pending_internal_promise else { - break; - }; - // SAFETY: `p` is a live JSC heap cell tracked by the VM. - if crate::JSPromise::status_ptr(p) != crate::js_promise::Status::Pending { - break; - } - self.event_loop_mut().tick(); - let Some(p) = self.pending_internal_promise else { - break; - }; - // SAFETY: see above. - if crate::JSPromise::status_ptr(p) == crate::js_promise::Status::Pending { - self.auto_tick(); - } - } - } else { - // SAFETY: `promise` is a live JSC heap cell. - if crate::JSPromise::status_ptr(promise) == crate::js_promise::Status::Rejected { - return Ok(promise); - } - self.event_loop_mut().perform_gc(); - self.wait_for_promise(jsc::AnyPromise::Internal(promise)); + if self.wait_for_entry_point_promise(promise) { + return Ok(promise); } - self.auto_tick(); Ok(self.pending_internal_promise.unwrap()) } @@ -5593,7 +5575,7 @@ impl VirtualMachine { ) -> Result<(), bun_core::Error> { use crate::JSType; use crate::console_object::formatter::TagOptions; - use crate::console_object::{self, Tag, TagPayload}; + use crate::console_object::{self, Tag}; let prev_had_errors = self.had_errors; self.had_errors = true; @@ -6065,7 +6047,7 @@ impl VirtualMachine { global_ref, TagOptions::DISABLE_INSPECT_CUSTOM | TagOptions::HIDE_GLOBAL, )?; - if !matches!(tag.tag, TagPayload::NativeCode) { + if !matches!(tag.tag, Tag::NativeCode) { let _ = if allow_ansi_color { formatter.format::(tag, writer, error_instance, global_ref) } else { diff --git a/src/jsc/ipc.rs b/src/jsc/ipc.rs index 54a91536dbd..97f6e7a4a1f 100644 --- a/src/jsc/ipc.rs +++ b/src/jsc/ipc.rs @@ -1879,6 +1879,89 @@ fn handle_ipc_message( } } +/// Handles every decode failure other than `NotEnoughBytes` (which each call +/// site recovers from differently): report OOM, then close the socket. +#[inline] +fn close_socket_on_decode_failure(send_queue: &mut SendQueue, err: &IPCDecodeError) { + debug_assert!(!matches!(err, IPCDecodeError::NotEnoughBytes)); + if matches!(err, IPCDecodeError::OutOfMemory) { + Output::print_errorln("IPC message is too long."); + } + send_queue.close_socket(CloseReason::Failure, CloseFrom::User); +} + +/// Drains complete JSON-mode messages from `send_queue.incoming` (which must +/// be `IncomingBuffer::Json`). Shared by the POSIX `on_data` and Windows +/// libuv `on_read` callbacks. +fn drain_json_messages(send_queue: &mut SendQueue, global_this: &JSGlobalObject) { + loop { + let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { + unreachable!() + }; + let Some(msg) = json_buf.next() else { break }; + let result = + match decode_ipc_message(Mode::Json, msg.data, global_this, Some(msg.newline_pos)) { + Ok(r) => r, + Err(IPCDecodeError::NotEnoughBytes) => { + log!("hit NotEnoughBytes"); + return; + } + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); + return; + } + }; + + let bytes_consumed = result.bytes_consumed; + handle_ipc_message(send_queue, result.message, global_this); + let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { + unreachable!() + }; + json_buf.consume(bytes_consumed); + } +} + +/// Drains complete Advanced-mode messages from the buffered bytes in +/// `send_queue.incoming` (which must be `IncomingBuffer::Advanced`). Shared by +/// the POSIX `on_data` and Windows libuv `on_read` callbacks. The buffer never +/// grows during the loop (no re-entrant reads inside `handle_ipc_message`), so +/// re-slicing from `slice_start` each iteration sees a stable tail. +fn drain_advanced_messages(send_queue: &mut SendQueue, global_this: &JSGlobalObject) { + let mut slice_start: usize = 0; + loop { + let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { + unreachable!() + }; + let slice = &adv_buf.slice()[slice_start..]; + let result = match decode_ipc_message(Mode::Advanced, slice, global_this, None) { + Ok(r) => r, + Err(IPCDecodeError::NotEnoughBytes) => { + // copy the remaining bytes to the start of the buffer + adv_buf.drain_front(slice_start); + log!("hit NotEnoughBytes2"); + return; + } + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); + return; + } + }; + + let slice_len = slice.len(); + handle_ipc_message(send_queue, result.message, global_this); + + if (result.bytes_consumed as usize) < slice_len { + slice_start += result.bytes_consumed as usize; + } else { + let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { + unreachable!() + }; + adv_buf.clear(); + return; + } + } +} + fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { let mut data = all_data; @@ -1898,45 +1981,7 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { unreachable!() }; json_buf.append(data); - - loop { - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let Some(msg) = json_buf.next() else { break }; - let result = match decode_ipc_message( - Mode::Json, - msg.data, - &global_this, - Some(msg.newline_pos), - ) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - log!("hit NotEnoughBytes"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let bytes_consumed = result.bytes_consumed; - handle_ipc_message(send_queue, result.message, &global_this); - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - json_buf.consume(bytes_consumed); - } + drain_json_messages(send_queue, &global_this); } IncomingBuffer::Advanced(_) => { // Advanced mode: uses length-prefix, no newline scanning needed. @@ -1957,17 +2002,8 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { log!("hit NotEnoughBytes"); return; } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); return; } }; @@ -1987,48 +2023,7 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { unreachable!() }; handle_oom(adv_buf.write(data)); - let mut slice_start: usize = 0; - loop { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let slice = &adv_buf.slice()[slice_start..]; - let result = match decode_ipc_message(Mode::Advanced, slice, &global_this, None) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - // copy the remaining bytes to the start of the buffer - adv_buf.drain_front(slice_start); - log!("hit NotEnoughBytes2"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let slice_len = slice.len(); - handle_ipc_message(send_queue, result.message, &global_this); - - if (result.bytes_consumed as usize) < slice_len { - slice_start += result.bytes_consumed as usize; - } else { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - adv_buf.clear(); - return; - } - } + drain_advanced_messages(send_queue, &global_this); } } } @@ -2167,46 +2162,7 @@ pub mod IPCHandlers { // and handing it to a `&mut self` method would alias // `json_buf.data`, undoing the Stacked-Borrows fix above. json_buf.notify_written(nread); - - // Process complete messages using next() - avoids O(n²) re-scanning - loop { - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let Some(msg) = json_buf.next() else { break }; - let result = match decode_ipc_message( - Mode::Json, - msg.data, - &global_this, - Some(msg.newline_pos), - ) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - log!("hit NotEnoughBytes3"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let bytes_consumed = result.bytes_consumed; - handle_ipc_message(send_queue, result.message, &global_this); - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - json_buf.consume(bytes_consumed); - } + drain_json_messages(send_queue, &global_this); } IncomingBuffer::Advanced(_) => { let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { @@ -2214,54 +2170,7 @@ pub mod IPCHandlers { }; // SAFETY: `on_read_alloc` reserved ≥ nread bytes; libuv initialised them. unsafe { adv_buf.uv_commit(nread) }; - let total_len = adv_buf.len(); - let mut slice_start: usize = 0; - - loop { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let slice = &adv_buf.slice()[slice_start..total_len]; - let result = - match decode_ipc_message(Mode::Advanced, slice, &global_this, None) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - // copy the remaining bytes to the start of the buffer - // `total_len == adv_buf.len()` (captured post-uv_commit, never - // grown in this loop) ⇒ exact `len - slice_start` truncate. - adv_buf.drain_front(slice_start); - log!("hit NotEnoughBytes3"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let slice_len = slice.len(); - handle_ipc_message(send_queue, result.message, &global_this); - - if (result.bytes_consumed as usize) < slice_len { - slice_start += result.bytes_consumed as usize; - } else { - // clear the buffer - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - adv_buf.clear(); - return; - } - } + drain_advanced_messages(send_queue, &global_this); } } } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index f1e9717a439..eab9c29c272 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -393,8 +393,9 @@ impl<'a> ConsoleFormatter for self::console_object::Formatter<'a> { // the const-generic `print_as::<{ Tag::… }, …>` arms. let mut sink = bun_io::FmtAdapter::new(writer); let result = self::console_object::formatter::TagResult { - tag: tag.into(), + tag, cell, + custom: None, }; let global = self.global_this; self.format::(result, &mut sink, value, global) @@ -987,7 +988,7 @@ mod __macro_smoke { pub use self::cached_bytecode::CachedBytecode; pub use self::deferred_error::DeferredError; pub use self::dom_form_data::DOMFormData; -pub use self::url::URL; +pub use self::url::{URL, UrlJsc}; pub use self::zig_stack_frame::ZigStackFrame; pub use self::zig_stack_trace::ZigStackTrace; pub use abort_signal::{AbortSignal, AbortSignalRef}; @@ -1480,8 +1481,8 @@ impl FromJsEnum for bun_http_types::FetchCacheMode::FetchCacheMode { } } -// `URL::path_from_file_url` / `URL::href_from_js` live in `URL.rs` (the -// dedicated port file); the lib.rs copies were duplicate definitions. +// `URL` is a re-export of `bun_url::whatwg::URL`; the JS-value entry points +// (`UrlJsc::from_js` / `UrlJsc::href_from_js`) live in `URL.rs`. // JSString (real module in JSString.rs). #[path = "JSString.rs"] diff --git a/src/runtime/api/HashObject.rs b/src/runtime/api/HashObject.rs index e70d3c615c0..1caaac5b3e0 100644 --- a/src/runtime/api/HashObject.rs +++ b/src/runtime/api/HashObject.rs @@ -276,20 +276,7 @@ fn hash_wrap(global: &JSGlobalObject, frame: &CallFrame) -> Js input = blob.shared_view(); } else { match arg.js_type_loose() { - jsc::JSType::ArrayBuffer - | jsc::JSType::Int8Array - | jsc::JSType::Uint8Array - | jsc::JSType::Uint8ClampedArray - | jsc::JSType::Int16Array - | jsc::JSType::Uint16Array - | jsc::JSType::Int32Array - | jsc::JSType::Uint32Array - | jsc::JSType::Float16Array - | jsc::JSType::Float32Array - | jsc::JSType::Float64Array - | jsc::JSType::BigInt64Array - | jsc::JSType::BigUint64Array - | jsc::JSType::DataView => { + t if t.is_array_buffer_like() => { array_buffer = match arg.as_array_buffer(global) { Some(ab) => ab, None => { diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index 0ca08293d49..95ccbc895ba 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -1,10 +1,9 @@ -use bun_ast::{E, Expr, expr::Data as ExprData}; +use bun_ast::ToJSError; use bun_collections::HashMap; -use bun_collections::VecExt; use bun_core::StackCheck; -use bun_core::{String as BunString, ZigString}; +use bun_core::String as BunString; use bun_js_parser::lexer; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, StringJsc, wtf}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::json5; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { @@ -76,7 +75,7 @@ pub fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { } }; - expr_to_js(root, global) + expr_to_js(&root, global) }, ) } @@ -432,55 +431,15 @@ impl Stringifier { } } -fn estring_to_js(str: &E::EString, global: &JSGlobalObject) -> JsResult { - // NOTE: the JSON5 parser never builds ropes, so the simple slice → JS - // path is sufficient. - if str.is_utf16 { - let zig = ZigString::init_utf16(str.slice16()); - let bun_s = BunString::init(zig); - bun_s.to_js(global) - } else { - jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) - } -} - -fn expr_to_js(expr: Expr, global: &JSGlobalObject) -> JsResult { - expr_to_js_with_check(expr, global, StackCheck::init()) -} - -fn expr_to_js_with_check( - expr: Expr, - global: &JSGlobalObject, - stack_check: StackCheck, -) -> JsResult { - if !stack_check.is_safe_to_recurse() { - return Err(global.throw_stack_overflow()); - } - match expr.data { - ExprData::ENull(_) => Ok(JSValue::NULL), - ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), - ExprData::ENumber(number) => Ok(JSValue::js_number(number.value)), - ExprData::EString(str) => estring_to_js(str.get(), global), - ExprData::EArray(arr) => { - JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { - expr_to_js_with_check(*item, global, stack_check) - }) - } - ExprData::EObject(obj) => { - let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); - for prop in obj.properties.slice() { - let key_expr = prop.key.expect("infallible: prop has key"); - let value = expr_to_js_with_check( - prop.value.expect("infallible: prop has value"), - global, - stack_check, - )?; - let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; - let key_str = bun_core::OwnedString::new(key_js.to_bun_string(global)?); - js_obj.put_may_be_index(global, &key_str, value)?; - } - Ok(js_obj) - } - _ => Ok(JSValue::UNDEFINED), - } +fn expr_to_js(expr: &bun_ast::Expr, global: &JSGlobalObject) -> JsResult { + bun_js_parser_jsc::expr_to_js(expr, global).map_err(|err| match err { + ToJSError::OutOfMemory => JsError::OutOfMemory, + ToJSError::JSTerminated => JsError::Terminated, + // The exception (e.g. stack overflow) is already pending on the global. + ToJSError::JSError => JsError::Thrown, + // Unreachable: the JSON5 parser only emits literal nodes. + ToJSError::CannotConvertArgumentTypeToJS + | ToJSError::CannotConvertIdentifierToJS + | ToJSError::MacroError => global.throw(format_args!("Cannot convert JSON5 value to JS")), + }) } diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index a5e423cbdd5..f58c9b3d04b 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -57,6 +57,44 @@ impl Drop for PinnedView { } } +/// Validate the input argument and pin its backing buffer (if any). The +/// caller derives the byte slice via [`input_slice`] so the borrow of +/// `buffer`/`pinned` stays local to the caller's frame. +fn prepare_input( + global_this: &JSGlobalObject, + input_value: JSValue, +) -> JsResult<(StringOrBuffer, Option)> { + if input_value.is_empty_or_undefined_or_null() { + return Err(global_this + .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); + } + + let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { + return Err(global_this + .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); + }; + + let pinned = PinnedView::pin(global_this, &buffer)?; + Ok((buffer, pinned)) +} + +#[inline] +fn input_slice<'a>(buffer: &'a StringOrBuffer, pinned: &'a Option) -> &'a [u8] { + match pinned { + Some(p) => p.slice(), + None => buffer.slice(), + } +} + +fn map_parser_error(global_this: &JSGlobalObject, err: ParserError) -> bun_jsc::JsError { + match err { + ParserError::JSError => bun_jsc::JsError::Thrown, + ParserError::JSTerminated => bun_jsc::JsError::Terminated, + ParserError::OutOfMemory => global_this.throw_out_of_memory(), + ParserError::StackOverflow => global_this.throw_stack_overflow(), + } +} + pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { bun_jsc::create_host_function_object( global_this, @@ -77,21 +115,8 @@ pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { pub fn render_to_ansi(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, theme_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let mut theme = md::AnsiTheme { colors: true, @@ -150,21 +175,8 @@ pub(crate) fn render_to_html( ) -> JsResult { let [input_value, opts_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let options = parse_options(global_this, opts_value)?; @@ -253,21 +265,8 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, callbacks_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -286,14 +285,8 @@ pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsR })?; // Run parser with the JS callback renderer - if let Err(err) = md::render_with_renderer(input, options, js_renderer.renderer()) { - return match err { - ParserError::JSError => Err(bun_jsc::JsError::Thrown), - ParserError::JSTerminated => Err(bun_jsc::JsError::Terminated), - ParserError::OutOfMemory => Err(global_this.throw_out_of_memory()), - ParserError::StackOverflow => Err(global_this.throw_stack_overflow()), - }; - } + md::render_with_renderer(input, options, js_renderer.renderer()) + .map_err(|err| map_parser_error(global_this, err))?; // Return accumulated result let result = js_renderer.get_result(); @@ -354,21 +347,8 @@ fn render_ast( ) -> JsResult { let [input_value, components_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -391,14 +371,8 @@ fn render_ast( JSValue::UNDEFINED })?; - if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { - return match err { - ParserError::JSError => Err(bun_jsc::JsError::Thrown), - ParserError::JSTerminated => Err(bun_jsc::JsError::Terminated), - ParserError::OutOfMemory => Err(global_this.throw_out_of_memory()), - ParserError::StackOverflow => Err(global_this.throw_stack_overflow()), - }; - } + md::render_with_renderer(input, options, renderer.renderer()) + .map_err(|err| map_parser_error(global_this, err))?; Ok(renderer.get_result()) } diff --git a/src/runtime/api/bun/subprocess/Writable.rs b/src/runtime/api/bun/subprocess/Writable.rs index 0ec7ac6e134..4fd9297bb57 100644 --- a/src/runtime/api/bun/subprocess/Writable.rs +++ b/src/runtime/api/bun/subprocess/Writable.rs @@ -17,6 +17,37 @@ use bun_io::pipe_writer::BaseWindowsPipeWriter as _; use super::{Flags, StaticPipeWriter, StdioResult, Subprocess, js}; +/// Build the `Writable::Buffer` writer for a `Stdio::Blob` / +/// `Stdio::ArrayBuffer` stdin, leaving `Stdio::Ignore` behind. Shared by the +/// `Bun.spawn` and shell `Writable::init` (both platform arms of each). +pub(crate) fn buffered_stdin_writer( + stdio: &mut Stdio, + event_loop: bun_event_loop::EventLoopHandle, + process: *mut P, + result: StdioResult, +) -> RefPtr> { + let source = match stdio { + Stdio::Blob(_) => { + // `Stdio` has a Drop impl (it would `blob.detach()`), so the + // payload cannot be destructure-moved out (E0509); take ownership + // via ManuallyDrop + ptr::read so the blob is moved exactly once. + let owned = core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); + let blob = match &*owned { + // SAFETY: `owned` is ManuallyDrop and discarded after this + // read; the Blob payload is moved out exactly once. + Stdio::Blob(b) => unsafe { core::ptr::read(b) }, + _ => unreachable!(), + }; + super::source_from_blob(blob) + } + Stdio::ArrayBuffer(array_buffer) => { + super::source_from_array_buffer(core::mem::take(array_buffer)) + } + _ => unreachable!("caller matched Blob/ArrayBuffer"), + }; + super::NewStaticPipeWriter::create(event_loop, process, result, source) +} + pub enum Writable<'a> { // `FileSink` is intrusive-refcounted (manual ref/deref): keep a raw // NonNull and call `FileSink::deref` explicitly. @@ -247,29 +278,12 @@ impl<'a> Writable<'a> { return Ok(Writable::Inherit); } - Stdio::Blob(_) => { - // See the unix arm below: Stdio has Drop, so move the - // payload out via ManuallyDrop + ptr::read. - let owned = - core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); - let blob = match &*owned { - // SAFETY: owned is ManuallyDrop; payload moved exactly once. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - return Ok(Writable::Buffer(StaticPipeWriter::create( + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => { + return Ok(Writable::Buffer(buffered_stdin_writer( + stdio, evtloop, subprocess as *mut Subprocess<'a>, result, - super::source_from_blob(blob), - ))); - } - Stdio::ArrayBuffer(array_buffer) => { - return Ok(Writable::Buffer(StaticPipeWriter::create( - evtloop, - subprocess as *mut Subprocess<'a>, - result, - super::source_from_array_buffer(core::mem::take(array_buffer)), ))); } Stdio::Fd(fd) => { @@ -349,29 +363,11 @@ impl<'a> Writable<'a> { Ok(Writable::Pipe(pipe_nn)) } - Stdio::Blob(_) => { - // `Stdio` has a Drop impl (would `blob.detach()`), so we can't - // move the payload out by match — take ownership via - // ManuallyDrop + ptr::read to transfer without detaching. - let owned = core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); - let blob = match &*owned { - // SAFETY: `owned` is ManuallyDrop and discarded after this - // read; the Blob payload is moved out exactly once. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - Ok(Writable::Buffer(StaticPipeWriter::create( - evtloop, - std::ptr::from_mut::>(subprocess), - result, - super::source_from_blob(blob), - ))) - } - Stdio::ArrayBuffer(array_buffer) => Ok(Writable::Buffer(StaticPipeWriter::create( + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => Ok(Writable::Buffer(buffered_stdin_writer( + stdio, evtloop, std::ptr::from_mut::>(subprocess), result, - super::source_from_array_buffer(core::mem::take(array_buffer)), ))), Stdio::Memfd(_) => { // Transfer ownership: `Stdio`'s Drop would close the memfd, so diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index b2f47266199..c9dc485ff9a 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -64,87 +64,29 @@ use crate::jsc_hooks::timer_all_mut as timer_all; // CronJobBase — shared base for CronRegisterJob and CronRemoveJob // ============================================================================ -/// Shared base for [`CronRegisterJob`] and [`CronRemoveJob`]. -// Note: every method on the path to `finish()` (which `heap::take`- -// drops `this`) takes a raw `*mut Self` receiver. -// A `&mut self` *parameter* would carry a Stacked Borrows FnEntry protector, -// making the in-flight dealloc UB; a *local* `let s = &mut *this` reborrow -// has no protector and ends at last use under NLL, so field access via `s` -// followed by `Self::finish(this)` is sound. -trait CronJobBase: Sized { - fn remaining_fds_mut(&mut self) -> &mut i8; - fn err_msg_mut(&mut self) -> &mut Option>; - fn has_called_process_exit_mut(&mut self) -> &mut bool; - fn exit_status_mut(&mut self) -> &mut Option; - /// May free `this`. Caller must not touch `this` afterward. - unsafe fn maybe_finished(this: *mut Self); - - fn loop_(&self) -> *mut AsyncLoop { - // `VirtualMachine::uv_loop` already returns the native loop on both - // targets (jsc/VirtualMachine.rs:2975); the prior POSIX arm's - // `bun_uws::Loop::get()` named the same per-thread singleton. - vm_mut().uv_loop() - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_reader_done(this: *mut Self) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - if s.err_msg_mut().is_none() { - let mut msg = Vec::new(); - let _ = write!( - &mut msg, - "Failed to read process output: {}", - <&'static str>::from(err.get_errno()) - ); - *s.err_msg_mut() = Some(msg); - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_process_exit(this: *mut Self, _proc: &Process, status: Status, _rusage: &Rusage) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = true; - *s.exit_status_mut() = Some(status); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum CronJobState { + ReadingCrontab, + InstallingCrontab, + #[cfg(target_os = "macos")] + WritingPlist, + BootingOut, + #[cfg(target_os = "macos")] + Bootstrapping, + Done, + Failed, } -// ============================================================================ -// CronRegisterJob -// ============================================================================ - -pub struct CronRegisterJob { +/// Fields shared by [`CronRegisterJob`] and [`CronRemoveJob`]. +struct CronJobCommon { promise: jsc::JSPromiseStrong, // LIFETIMES.tsv: JSC_BORROW → GlobalRef global: GlobalRef, poll: KeepAlive, - - bun_exe: &'static ZStr, - abs_path: ZString, - /// normalized numeric form for crontab/launchd - schedule: ZString, title: ZString, - #[cfg(windows)] - parsed_cron: CronExpression, - state: RegisterState, + state: CronJobState, // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). process: Option<*mut Process>, stdout_reader: OutputReader, @@ -160,95 +102,158 @@ pub struct CronRegisterJob { event_loop_handle: EventLoopHandle, } -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -enum RegisterState { - ReadingCrontab, - #[cfg(not(target_os = "macos"))] - InstallingCrontab, - #[cfg(target_os = "macos")] - WritingPlist, - BootingOut, - #[cfg(target_os = "macos")] - Bootstrapping, - Done, - Failed, +impl CronJobCommon { + /// `T` is the concrete job type owning this base (reader-parent vtable). + fn init(global: &JSGlobalObject, title: &[u8]) -> Self { + Self { + promise: jsc::JSPromiseStrong::init(global), + global: GlobalRef::from(global), + poll: KeepAlive::default(), + title: ZString::from_bytes(title), + state: CronJobState::ReadingCrontab, + process: None, + stdout_reader: OutputReader::init::(), + #[cfg(windows)] + stderr_reader: OutputReader::init::(), + remaining_fds: 0, + has_called_process_exit: false, + exit_status: None, + err_msg: None, + tmp_path: None, + // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. + event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), + } + } + + fn set_err(&mut self, args: core::fmt::Arguments<'_>) { + if self.err_msg.is_none() { + let mut msg = Vec::new(); + let _ = msg.write_fmt(args); + self.err_msg = Some(msg); + } + } + + fn detach_process(&mut self) { + if let Some(proc) = self.process.take() { + // SAFETY: `proc` is the intrusive-RC pointer returned by `to_process`. + unsafe { + (*proc).detach(); + Process::deref(proc); + } + } + } } -// Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. -bun_io::impl_buffered_reader_parent! { - CronRegister for CronRegisterJob; - has_on_read_chunk = false; - on_reader_done = |this| ::on_reader_done(this); - on_reader_error = |this, err| ::on_reader_error(this, err); - loop_ = |this| ::loop_(&*this).cast(); - event_loop = |this| (*this).event_loop_handle.as_event_loop_ctx(); +impl Drop for CronJobCommon { + fn drop(&mut self) { + // stdout_reader / stderr_reader drop via their own Drop. + self.detach_process(); + if let Some(p) = self.tmp_path.take() { + let _ = sys::unlink(&p); + } + // err_msg, title freed via field Drop. + } } -impl CronJobBase for CronRegisterJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds +/// Shared base for [`CronRegisterJob`] and [`CronRemoveJob`]. +// Note: every method on the path to `finish()` (which `heap::take`- +// drops `this`) takes a raw `*mut Self` receiver. +// A `&mut self` *parameter* would carry a Stacked Borrows FnEntry protector, +// making the in-flight dealloc UB; a *local* `let s = &mut *this` reborrow +// has no protector and ends at last use under NLL, so field access via `s` +// followed by `Self::finish(this)` is sound. +trait CronJobBase: Sized + BufferedReaderParent { + const EXIT_KIND: bun_spawn::ProcessExitKind; + fn base(&self) -> &CronJobCommon; + fn base_mut(&mut self) -> &mut CronJobCommon; + /// Dispatch on the state machine after a clean process exit. + /// May free `this`. Caller must not touch `this` afterward. + unsafe fn advance_state(this: *mut Self); + + /// Whether a nonzero exit code is benign in the current state: an empty + /// crontab makes `crontab -l` exit 1, and `launchctl bootout` fails when + /// the job was not loaded. + fn accepts_nonzero_exit(&self, code: u8) -> bool { + let state = self.base().state; + (state == CronJobState::ReadingCrontab && code == 1) || state == CronJobState::BootingOut } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg + + /// Hook for a job-specific error message derived from stderr; returns + /// true if it consumed the failure (an error was set). + #[cfg(windows)] + fn exit_err_override(&mut self, stderr: &[u8]) -> bool { + let _ = stderr; + false } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit + + fn loop_(&self) -> *mut AsyncLoop { + // `VirtualMachine::uv_loop` already returns the native loop on both + // targets (jsc/VirtualMachine.rs:2975); the prior POSIX arm's + // `bun_uws::Loop::get()` named the same per-thread singleton. + vm_mut().uv_loop() } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + + /// May free `this` via `maybe_finished`. + unsafe fn on_reader_done(this: *mut Self) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + debug_assert!(b.remaining_fds > 0); + b.remaining_fds -= 1; + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRegisterJob::maybe_finished(this) } + + /// May free `this` via `maybe_finished`. + unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + debug_assert!(b.remaining_fds > 0); + b.remaining_fds -= 1; + b.set_err(format_args!( + "Failed to read process output: {}", + <&'static str>::from(err.get_errno()) + )); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } -} -impl CronRegisterJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { - let mut msg = Vec::new(); - let _ = msg.write_fmt(args); - self.err_msg = Some(msg); - } + /// May free `this` via `maybe_finished`. + unsafe fn on_process_exit(this: *mut Self, _proc: &Process, status: Status, _rusage: &Rusage) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + b.has_called_process_exit = true; + b.exit_status = Some(status); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// May free `this`. Raw-ptr receiver: see trait-level note. unsafe fn maybe_finished(this: *mut Self) { // SAFETY: local reborrow (no FnEntry protector); not used after any // call below that may free `this`. let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { + if !s.base().has_called_process_exit || s.base().remaining_fds != 0 { return; } - if let Some(proc) = s.process.take() { - // SAFETY: `proc` is the intrusive-RC pointer returned by `to_process`. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if s.err_msg.is_some() { + s.base_mut().detach_process(); + if s.base().err_msg.is_some() { // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } - let Some(status) = s.exit_status.take() else { + let Some(status) = s.base_mut().exit_status.take() else { return; }; match status { Status::Exited(exited) => { - if exited.code != 0 - && !(s.state == RegisterState::ReadingCrontab && exited.code == 1) - && s.state != RegisterState::BootingOut - { + if exited.code != 0 && !s.accepts_nonzero_exit(exited.code) { // Materialize the trimmed stderr into an owned buffer: - // `final_buffer()` borrows `s` mutably, and `set_err` - // below needs another `&mut s` — copy out so the two + // `final_buffer()` borrows the base mutably, and `set_err` + // below needs another `&mut` — copy out so the two // borrows do not overlap (Windows only; POSIX ignores // stderr here). #[cfg(windows)] let stderr_owned: Vec = bun_core::immutable::trim( - s.stderr_reader.final_buffer().as_slice(), + s.base_mut().stderr_reader.final_buffer().as_slice(), &ASCII_WHITESPACE, ) .to_vec(); @@ -256,44 +261,32 @@ impl CronRegisterJob { let stderr_output: &[u8] = stderr_owned.as_slice(); #[cfg(not(windows))] let stderr_output: &[u8] = b""; - // On Windows, detect the SID resolution error and provide - // a clear message instead of the raw schtasks output. #[cfg(windows)] - { - if s.state == RegisterState::InstallingCrontab - && bun_core::index_of( - stderr_output, - b"No mapping between account names", - ) - .is_some() - { - s.set_err(format_args!( - "Failed to register cron job: your Windows account's Security Identifier (SID) could not be resolved. \ - This typically happens on headless servers or CI where the process runs under a service account. \ - To fix this, either run Bun as a regular user account, or create the scheduled task manually with: \ - schtasks /create /xml /tn /ru SYSTEM /f" - )); - return unsafe { Self::finish(this) }; - } + if s.exit_err_override(stderr_output) { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; } if !stderr_output.is_empty() { - s.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); + s.base_mut() + .set_err(format_args!("{}", bstr::BStr::new(stderr_output))); } else { - s.set_err(format_args!("Process exited with code {}", exited.code)); + s.base_mut() + .set_err(format_args!("Process exited with code {}", exited.code)); } // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } Status::Signaled(sig) => { - if s.state != RegisterState::BootingOut { - s.set_err(format_args!("Process killed by signal {}", sig as i32)); + if s.base().state != CronJobState::BootingOut { + s.base_mut() + .set_err(format_args!("Process killed by signal {}", sig as i32)); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } Status::Err(err) => { - s.set_err(format_args!( + s.base_mut().set_err(format_args!( "Process error: {}", <&'static str>::from(err.get_errno()) )); @@ -306,70 +299,31 @@ impl CronRegisterJob { unsafe { Self::advance_state(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - unsafe fn advance_state(this: *mut Self) { - // SAFETY: local reborrow; last use precedes any self-freeing call. - let s = unsafe { &mut *this }; - #[cfg(target_os = "macos")] - { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::WritingPlist => unsafe { Self::spawn_bootout(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::Bootstrapping => unsafe { Self::finish(this) }, - _ => { - s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; - } - } - } - #[cfg(not(target_os = "macos"))] - { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::InstallingCrontab => unsafe { Self::finish(this) }, - _ => { - s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; - } - } - } - } - /// Consumes and frees `this` (`heap::take`). unsafe fn finish(this: *mut Self) { // SAFETY: caller holds the unique Box; consumed below. Local // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { - RegisterState::Failed + let b = unsafe { &mut *this }.base_mut(); + b.state = if b.err_msg.is_some() { + CronJobState::Failed } else { - RegisterState::Done + CronJobState::Done }; - this_ref.poll.unref(bun_io::js_vm_ctx()); + b.poll.unref(bun_io::js_vm_ctx()); let ev = VirtualMachine::get().event_loop_mut(); ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref - .global + if let Some(msg) = &b.err_msg { + let _ = b.promise.reject_with_async_stack( + &b.global, + Ok(b.global .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), ); } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); + let _ = b.promise.resolve(&b.global, JSValue::UNDEFINED); } // Drop runs INSIDE the enter/exit scope so Process detach/deref and // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_register. + // SAFETY: `this` was created via heap::alloc in cron_register/cron_remove. unsafe { drop(bun_core::heap::take(this)) }; ev.exit(); } @@ -385,101 +339,252 @@ impl CronRegisterJob { unsafe { spawn_cmd_generic(this, argv, stdin_opt, stdout_opt) }; } - // -- Linux -- - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// Spawn `crontab -l` and buffer its output. May free `this`. #[cfg(all(not(target_os = "macos"), not(windows)))] unsafe fn start_linux(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); + let b = unsafe { &mut *this }.base_mut(); + b.state = CronJobState::ReadingCrontab; + b.stdout_reader = OutputReader::init::(); + b.stdout_reader.set_parent(this.cast()); let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("crontab not found in PATH")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: local reborrow has ended; `this` is the live heap job. unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// Read the captured `crontab -l` output and drop any existing entry for + /// `title`. On allocation failure, fails the job (freeing `this`) and + /// returns `None`. #[cfg(not(target_os = "macos"))] - unsafe fn process_crontab_and_install(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); + unsafe fn take_filtered_crontab(this: *mut Self) -> Option> { + // SAFETY: local reborrow; not used after `finish`. + let b = unsafe { &mut *this }.base_mut(); + let existing_content = b.stdout_reader.final_buffer().as_slice(); let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { - s.set_err(format_args!("Out of memory building crontab")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - - // Build new entry with single-quoted paths to prevent shell injection - let mut new_entry = Vec::new(); - if write!( - &mut new_entry, - "# bun-cron: {title}\n{sched} '{exe}' run --cron-title={title} --cron-period='{sched}' '{path}'\n", - title = bstr::BStr::new(s.title.as_bytes()), - sched = bstr::BStr::new(s.schedule.as_bytes()), - exe = bstr::BStr::new(s.bun_exe.as_bytes()), - path = bstr::BStr::new(s.abs_path.as_bytes()), - ) - .is_err() - { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + if filter_crontab(existing_content, b.title.as_bytes(), &mut result).is_err() { + b.set_err(format_args!("Out of memory building crontab")); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + return None; } - result.extend_from_slice(&new_entry); + Some(result) + } - let tmp_path = match make_temp_path("bun-cron-") { + /// Write `content` to a fresh temp file and spawn `crontab ` to + /// install it. May free `this`. + #[cfg(not(target_os = "macos"))] + unsafe fn install_crontab(this: *mut Self, content: &[u8], tmp_prefix: &'static str) { + // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. + let b = unsafe { &mut *this }.base_mut(); + let tmp_path = match make_temp_path(tmp_prefix) { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); + b.tmp_path = Some(tmp_path); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + b.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Failed to create temp file")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; - if file.write_all(&result).is_err() { + if file.write_all(content).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Failed to write temp file")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } let _ = file.close(); // close error is non-actionable - s.state = RegisterState::InstallingCrontab; + b.state = CronJobState::InstallingCrontab; // Note: explicit deinit of old reader before reassign — Drop handles it. - s.stdout_reader = OutputReader::init::(); + b.stdout_reader = OutputReader::init::(); let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("crontab not found in PATH")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + } + + /// Spawn `launchctl bootout` for this job's launchd label. May free `this`. + #[cfg(target_os = "macos")] + unsafe fn spawn_bootout(this: *mut Self) { + // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. + let b = unsafe { &mut *this }.base_mut(); + b.state = CronJobState::BootingOut; + let uid_str = match alloc_print_z(format_args!( + "gui/{}/bun.cron.{}", + get_uid(), + bstr::BStr::new(b.title.as_bytes()) + )) { + Ok(v) => v, + Err(_) => { + b.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; + } + }; + let mut argv: [*const c_char; 4] = [ + c"/bin/launchctl".as_ptr().cast(), + c"bootout".as_ptr().cast(), + uid_str.as_ptr().cast(), + core::ptr::null(), + ]; + // SAFETY: local reborrow has ended; `this` is the live heap job. unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + drop(uid_str); + } +} + +/// Ref the keep-alive poll and grab the promise value before `start_*` runs +/// (which may synchronously free `job`). +/// +/// SAFETY: `job` must be the live, uniquely-owned heap job (freshly leaked Box). +unsafe fn arm_job(job: *mut T) -> JSValue { + // SAFETY: caller contract; short-lived borrow ends on return. + let b = unsafe { &mut *job }.base_mut(); + b.poll.ref_(bun_io::js_vm_ctx()); + b.promise.value() +} + +// ============================================================================ +// CronRegisterJob +// ============================================================================ + +pub struct CronRegisterJob { + base: CronJobCommon, + + bun_exe: &'static ZStr, + abs_path: ZString, + /// normalized numeric form for crontab/launchd + schedule: ZString, + #[cfg(windows)] + parsed_cron: CronExpression, +} + +// Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. +bun_io::impl_buffered_reader_parent! { + CronRegister for CronRegisterJob; + has_on_read_chunk = false; + on_reader_done = |this| ::on_reader_done(this); + on_reader_error = |this, err| ::on_reader_error(this, err); + loop_ = |this| ::loop_(&*this).cast(); + event_loop = |this| (*this).base.event_loop_handle.as_event_loop_ctx(); +} + +impl CronJobBase for CronRegisterJob { + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; + fn base(&self) -> &CronJobCommon { + &self.base + } + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base + } + + /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + unsafe fn advance_state(this: *mut Self) { + // SAFETY: local reborrow; last use precedes any self-freeing call. + let s = unsafe { &mut *this }; + #[cfg(target_os = "macos")] + { + match s.base.state { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::WritingPlist => unsafe { Self::spawn_bootout(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::Bootstrapping => unsafe { Self::finish(this) }, + _ => { + s.base.set_err(format_args!("Unexpected state")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + } + } + } + #[cfg(not(target_os = "macos"))] + { + match s.base.state { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, + _ => { + s.base.set_err(format_args!("Unexpected state")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + } + } + } + } + + /// On Windows, detect the SID resolution error and provide a clear + /// message instead of the raw schtasks output. + #[cfg(windows)] + fn exit_err_override(&mut self, stderr: &[u8]) -> bool { + if self.base.state == CronJobState::InstallingCrontab + && bun_core::index_of(stderr, b"No mapping between account names").is_some() + { + self.base.set_err(format_args!( + "Failed to register cron job: your Windows account's Security Identifier (SID) could not be resolved. \ + This typically happens on headless servers or CI where the process runs under a service account. \ + To fix this, either run Bun as a regular user account, or create the scheduled task manually with: \ + schtasks /create /xml /tn /ru SYSTEM /f" + )); + return true; + } + false + } +} + +impl CronRegisterJob { + /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + #[cfg(not(target_os = "macos"))] + unsafe fn process_crontab_and_install(this: *mut Self) { + // SAFETY: `this` is the live heap job; freed inside on failure. + let Some(mut result) = (unsafe { Self::take_filtered_crontab(this) }) else { + return; + }; + // SAFETY: local reborrow; not used after `install_crontab`/`finish`. + let s = unsafe { &mut *this }; + + // Build new entry with single-quoted paths to prevent shell injection + if write!( + &mut result, + "# bun-cron: {title}\n{sched} '{exe}' run --cron-title={title} --cron-period='{sched}' '{path}'\n", + title = bstr::BStr::new(s.base.title.as_bytes()), + sched = bstr::BStr::new(s.schedule.as_bytes()), + exe = bstr::BStr::new(s.bun_exe.as_bytes()), + path = bstr::BStr::new(s.abs_path.as_bytes()), + ) + .is_err() + { + s.base.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; + } + + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::install_crontab(this, &result, "bun-cron-") }; } // -- macOS -- @@ -489,19 +594,20 @@ impl CronRegisterJob { unsafe fn start_mac(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_bootout`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::WritingPlist; + s.base.state = CronJobState::WritingPlist; let calendar_xml = match cron_to_calendar_interval(s.schedule.as_bytes()) { Ok(x) => x, Err(_) => { - s.set_err(format_args!("Invalid cron expression")); + s.base.set_err(format_args!("Invalid cron expression")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let Some(home) = env_var::HOME.get() else { - s.set_err(format_args!("HOME environment variable not set")); + s.base + .set_err(format_args!("HOME environment variable not set")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; @@ -513,7 +619,7 @@ impl CronRegisterJob { bstr::BStr::new(home) ); if Fd::cwd().make_path(&launch_agents_dir).is_err() { - s.set_err(format_args!( + s.base.set_err(format_args!( "Failed to create ~/Library/LaunchAgents directory" )); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -523,16 +629,16 @@ impl CronRegisterJob { let plist_path = match alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; - s.tmp_path = Some(plist_path); + s.base.tmp_path = Some(plist_path); // XML-escape all dynamic values macro_rules! try_escape { @@ -540,14 +646,14 @@ impl CronRegisterJob { match xml_escape($e) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } }; } - let xml_title = try_escape!(s.title.as_bytes()); + let xml_title = try_escape!(s.base.title.as_bytes()); let xml_bun = try_escape!(s.bun_exe.as_bytes()); let xml_path = try_escape!(s.abs_path.as_bytes()); let xml_sched = try_escape!(s.schedule.as_bytes()); @@ -585,27 +691,27 @@ impl CronRegisterJob { ) .is_err() { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, 0o644, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create plist file")); + s.base.set_err(format_args!("Failed to create plist file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; if file.write_all(&plist).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write plist")); + s.base.set_err(format_args!("Failed to write plist")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -615,50 +721,21 @@ impl CronRegisterJob { unsafe { Self::spawn_bootout(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn spawn_bootout(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::BootingOut; - let uid_str = match alloc_print_z(format_args!( - "gui/{}/bun.cron.{}", - get_uid(), - bstr::BStr::new(s.title.as_bytes()) - )) { - Ok(v) => v, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - let mut argv: [*const c_char; 4] = [ - c"/bin/launchctl".as_ptr().cast(), - c"bootout".as_ptr().cast(), - uid_str.as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(uid_str); - } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn spawn_bootstrap(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::Bootstrapping; - let Some(plist_path) = s.tmp_path.take() else { - s.set_err(format_args!("No plist path")); + s.base.state = CronJobState::Bootstrapping; + let Some(plist_path) = s.base.tmp_path.take() else { + s.base.set_err(format_args!("No plist path")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let uid_str = match alloc_print_z(format_args!("gui/{}", get_uid())) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -778,35 +855,15 @@ pub fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(global, title_slice.slice()), bun_exe, abs_path, schedule: ZString::from_bytes(normalized_schedule), - title: ZString::from_bytes(title_slice.slice()), #[cfg(windows)] parsed_cron: parsed, - state: RegisterState::ReadingCrontab, - process: None, - stdout_reader: OutputReader::init::(), - #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, - // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. - event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + // SAFETY: `job` is the freshly-leaked Box; unique until `start_*` runs. + let promise_value = unsafe { arm_job(job) }; // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. @@ -835,15 +892,15 @@ impl CronRegisterJob { unsafe fn start_windows(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::InstallingCrontab; + s.base.state = CronJobState::InstallingCrontab; let task_name = match alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -852,18 +909,18 @@ impl CronRegisterJob { let xml = match cron_to_task_xml( &s.parsed_cron, s.bun_exe.as_bytes(), - s.title.as_bytes(), + s.base.title.as_bytes(), s.schedule.as_bytes(), s.abs_path.as_bytes(), ) { Ok(x) => x, Err(e) => { if e == TaskXmlError::TooManyTriggers { - s.set_err(format_args!( + s.base.set_err(format_args!( "This cron expression requires too many triggers for Windows Task Scheduler (max 48). Simplify the expression or use fewer restricted fields." )); } else { - s.set_err(format_args!("Failed to build task XML")); + s.base.set_err(format_args!("Failed to build task XML")); } // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; @@ -873,30 +930,32 @@ impl CronRegisterJob { let xml_path = match make_temp_path("bun-cron-xml-") { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let xml_path_ptr = xml_path.as_ptr(); - s.tmp_path = Some(xml_path); + s.base.tmp_path = Some(xml_path); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create temp XML file")); + s.base + .set_err(format_args!("Failed to create temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; if file.write_all(&xml).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp XML file")); + s.base + .set_err(format_args!("Failed to write temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -910,29 +969,12 @@ impl CronRegisterJob { b"/tn\0".as_ptr().cast(), task_name.as_ptr().cast(), b"/np\0".as_ptr().cast(), - b"/f\0".as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(task_name); - } -} - -impl Drop for CronRegisterJob { - fn drop(&mut self) { - // stdout_reader / stderr_reader drop via their own Drop. - if let Some(proc) = self.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if let Some(p) = self.tmp_path.take() { - let _ = sys::unlink(&p); - } - // err_msg, abs_path, schedule, title freed via field Drop. + b"/f\0".as_ptr().cast(), + core::ptr::null(), + ]; + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + drop(task_name); } } @@ -944,36 +986,7 @@ const ASCII_WHITESPACE: [u8; 6] = *b" \t\n\r\x0b\x0c"; // ============================================================================ pub struct CronRemoveJob { - promise: jsc::JSPromiseStrong, - // LIFETIMES.tsv: JSC_BORROW → GlobalRef - global: GlobalRef, - poll: KeepAlive, - title: ZString, - - state: RemoveState, - // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). - process: Option<*mut Process>, - stdout_reader: OutputReader, - #[cfg(windows)] - stderr_reader: OutputReader, - remaining_fds: i8, - has_called_process_exit: bool, - exit_status: Option, - err_msg: Option>, - tmp_path: Option, - /// Typed enum for the io-layer FilePoll vtable (`bun_io::EventLoopHandle` - /// wraps `*const EventLoopHandle`). - event_loop_handle: EventLoopHandle, -} - -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -enum RemoveState { - ReadingCrontab, - InstallingCrontab, - BootingOut, - Done, - Failed, + base: CronJobCommon, } // Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. @@ -983,108 +996,25 @@ bun_io::impl_buffered_reader_parent! { on_reader_done = |this| ::on_reader_done(this); on_reader_error = |this, err| ::on_reader_error(this, err); loop_ = |this| ::loop_(&*this).cast(); - event_loop = |this| (*this).event_loop_handle.as_event_loop_ctx(); + event_loop = |this| (*this).base.event_loop_handle.as_event_loop_ctx(); } impl CronJobBase for CronRemoveJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds - } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg - } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit - } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status - } - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRemoveJob::maybe_finished(this) } + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; + fn base(&self) -> &CronJobCommon { + &self.base } -} - -impl CronRemoveJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { - let mut msg = Vec::new(); - let _ = msg.write_fmt(args); - self.err_msg = Some(msg); - } + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: local reborrow (no FnEntry protector); not used after any - // call below that may free `this`. - let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { - return; - } - if let Some(proc) = s.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if s.err_msg.is_some() { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - let Some(status) = s.exit_status.take() else { - return; - }; - match status { - Status::Exited(exited) => { - let is_acceptable_nonzero = (s.state == RemoveState::ReadingCrontab - && exited.code == 1) - || s.state == RemoveState::BootingOut - // On Windows, schtasks /delete exits non-zero when the task doesn't exist; - // removal of a non-existent job should resolve without error. - || (cfg!(windows) && s.state == RemoveState::InstallingCrontab); - if exited.code != 0 && !is_acceptable_nonzero { - // Owned copy: `final_buffer()` is `&mut self` and would - // alias `s.set_err` below. Copy the trimmed bytes out. - #[cfg(windows)] - let stderr_owned: Vec = bun_core::immutable::trim( - s.stderr_reader.final_buffer().as_slice(), - &ASCII_WHITESPACE, - ) - .to_vec(); - #[cfg(windows)] - let stderr_output: &[u8] = stderr_owned.as_slice(); - #[cfg(not(windows))] - let stderr_output: &[u8] = b""; - if !stderr_output.is_empty() { - s.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); - } else { - s.set_err(format_args!("Process exited with code {}", exited.code)); - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - } - Status::Signaled(sig) => { - if s.state != RemoveState::BootingOut { - s.set_err(format_args!("Process killed by signal {}", sig as i32)); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - } - Status::Err(err) => { - s.set_err(format_args!( - "Process error: {}", - <&'static str>::from(err.get_errno()) - )); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - Status::Running => return, - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::advance_state(this) }; + fn accepts_nonzero_exit(&self, code: u8) -> bool { + let state = self.base.state; + (state == CronJobState::ReadingCrontab && code == 1) + || state == CronJobState::BootingOut + // On Windows, schtasks /delete exits non-zero when the task doesn't exist; + // removal of a non-existent job should resolve without error. + || (cfg!(windows) && state == CronJobState::InstallingCrontab) } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. @@ -1093,21 +1023,21 @@ impl CronRemoveJob { let s = unsafe { &mut *this }; #[cfg(target_os = "macos")] { - match s.state { - RemoveState::BootingOut => { + match s.base.state { + CronJobState::BootingOut => { let Some(home) = env_var::HOME.get() else { - s.set_err(format_args!("HOME not set")); + s.base.set_err(format_args!("HOME not set")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; if let Ok(plist_path) = alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { let _ = sys::unlink(&plist_path); } else { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -1115,7 +1045,7 @@ impl CronRemoveJob { unsafe { Self::finish(this) }; } _ => { - s.set_err(format_args!("Unexpected state")); + s.base.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. unsafe { Self::finish(this) }; } @@ -1123,166 +1053,31 @@ impl CronRemoveJob { } #[cfg(not(target_os = "macos"))] { - match s.state { + match s.base.state { // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RemoveState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, + CronJobState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RemoveState::InstallingCrontab => unsafe { Self::finish(this) }, + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, _ => { - s.set_err(format_args!("Unexpected state")); + s.base.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. unsafe { Self::finish(this) }; } } } } +} - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller holds the unique Box; consumed below. Local - // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { - RemoveState::Failed - } else { - RemoveState::Done - }; - this_ref.poll.unref(bun_io::js_vm_ctx()); - let ev = VirtualMachine::get().event_loop_mut(); - ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref - .global - .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), - ); - } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); - } - // Drop runs INSIDE the enter/exit scope so Process detach/deref and - // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_remove. - unsafe { drop(bun_core::heap::take(this)) }; - ev.exit(); - } - - /// May free `this` (via spawn → synchronous exit → finish, or error path). - unsafe fn spawn_cmd( - this: *mut Self, - argv: &mut [*const c_char], - stdin_opt: spawn::Stdio, - stdout_opt: spawn::Stdio, - ) { - // SAFETY: `this` is the live heap job (caller contract); may be freed inside. - unsafe { spawn_cmd_generic(this, argv, stdin_opt, stdout_opt) }; - } - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(all(not(target_os = "macos"), not(windows)))] - unsafe fn start_linux(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); - let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - }; - let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; - } - +impl CronRemoveJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn remove_crontab_entry(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); - let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - - let tmp_path = match make_temp_path("bun-cron-rm-") { - Ok(p) => p, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); - - let file = match File::openat( - Fd::cwd(), - s.tmp_path.as_ref().unwrap(), - sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, - 0o600, - ) { - Ok(f) => f, - Err(_) => { - s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - if file.write_all(&result).is_err() { - let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - let _ = file.close(); // close error is non-actionable - - s.state = RemoveState::InstallingCrontab; - s.stdout_reader = OutputReader::init::(); - let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - }; - let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - } - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn start_mac(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::BootingOut; - let uid_str = match alloc_print_z(format_args!( - "gui/{}/bun.cron.{}", - get_uid(), - bstr::BStr::new(s.title.as_bytes()) - )) { - Ok(v) => v, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } + // SAFETY: `this` is the live heap job; freed inside on failure. + let Some(result) = (unsafe { Self::take_filtered_crontab(this) }) else { + return; }; - let mut argv: [*const c_char; 4] = [ - c"/bin/launchctl".as_ptr().cast(), - c"bootout".as_ptr().cast(), - uid_str.as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(uid_str); + // SAFETY: `this` is the live heap job; `install_crontab` may free it. + unsafe { Self::install_crontab(this, &result, "bun-cron-rm-") }; } } @@ -1305,35 +1100,15 @@ pub fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(), - #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, - // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. - event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), + base: CronJobCommon::init::(global, title_slice.slice()), })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + // SAFETY: `job` is the freshly-leaked Box; unique until `start_*` runs. + let promise_value = unsafe { arm_job(job) }; // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. #[cfg(target_os = "macos")] unsafe { - CronRemoveJob::start_mac(job) + CronRemoveJob::spawn_bootout(job) }; #[cfg(windows)] unsafe { @@ -1353,14 +1128,14 @@ impl CronRemoveJob { unsafe fn start_windows(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RemoveState::InstallingCrontab; + s.base.state = CronJobState::InstallingCrontab; let task_name = match alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -1379,21 +1154,6 @@ impl CronRemoveJob { } } -impl Drop for CronRemoveJob { - fn drop(&mut self) { - if let Some(proc) = self.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if let Some(p) = self.tmp_path.take() { - let _ = sys::unlink(&p); - } - } -} - // ============================================================================ // CronJob — in-process callback-style cron (Bun.cron(expr, cb)) // ============================================================================ @@ -2061,20 +1821,6 @@ pub fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult); - /// Consumes and frees `this`. - unsafe fn finish(this: *mut Self); - fn process_slot(&mut self) -> &mut Option<*mut Process>; - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader; - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader; - fn remaining_fds(&mut self) -> &mut i8; -} - bun_spawn::link_impl_ProcessExit! { CronRegister for CronRegisterJob => |this| { // Forward `this` raw — `on_process_exit` → `maybe_finished` may free it. @@ -2089,73 +1835,24 @@ bun_spawn::link_impl_ProcessExit! { } } -impl SpawnCmdTarget for CronRegisterJob { - const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - CronRegisterJob::set_err(self, args) - } - unsafe fn finish(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRegisterJob::finish(this) } - } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process - } - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader - } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds - } -} -impl SpawnCmdTarget for CronRemoveJob { - const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - CronRemoveJob::set_err(self, args) - } - unsafe fn finish(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRemoveJob::finish(this) } - } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process - } - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader - } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds - } -} - /// Generic spawn used by both CronRegisterJob and CronRemoveJob. /// /// May free `this` (synchronously, via either an early `T::finish` on setup /// error or `watch_or_reap` → exit handler → `maybe_finished` → `finish`). /// Raw-ptr receiver: see [`CronJobBase`] note. Callers must not touch /// `this` after this returns. -unsafe fn spawn_cmd_generic( +unsafe fn spawn_cmd_generic( this: *mut T, argv: &mut [*const c_char], stdin_opt: spawn::Stdio, stdout_opt: spawn::Stdio, ) { - // SAFETY: local reborrow (no FnEntry protector). Re-derived after each - // section so no `&mut T` outlives a potentially-freeing call. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = false; - *s.exit_status_mut() = None; - *s.remaining_fds() = 0; + // SAFETY: local reborrow (no FnEntry protector); last use precedes any + // potentially-freeing call. + let b = unsafe { &mut *this }.base_mut(); + b.has_called_process_exit = false; + b.exit_status = None; + b.remaining_fds = 0; #[cfg(not(windows))] let resolved_argv0: Option<*const c_char> = None; @@ -2176,7 +1873,7 @@ unsafe fn spawn_cmd_generic( match bun_which::which(&mut path_buf, path_env, b"", argv0) { Some(p) => resolved_argv0 = Some(p.as_ptr().cast()), None => { - s.set_err(format_args!( + b.set_err(format_args!( "Could not find '{}' in PATH", bstr::BStr::new(argv0) )); @@ -2203,7 +1900,7 @@ unsafe fn spawn_cmd_generic( envp_owned.as_ptr().cast() } Err(_) => { - s.set_err(format_args!("Failed to create environment block")); + b.set_err(format_args!("Failed to create environment block")); return unsafe { T::finish(this) }; } } @@ -2263,7 +1960,7 @@ unsafe fn spawn_cmd_generic( // `Drop`. Reclaim it (uv_close + free if init'd) here. #[cfg(windows)] spawn_options.stderr.deinit(); - s.set_err(format_args!( + b.set_err(format_args!( "Failed to spawn process: {}", bstr::BStr::new(err.name()) )); @@ -2273,7 +1970,7 @@ unsafe fn spawn_cmd_generic( Err(e) => { #[cfg(windows)] spawn_options.stderr.deinit(); - s.set_err(format_args!("Failed to spawn process: {}", e.name())); + b.set_err(format_args!("Failed to spawn process: {}", e.name())); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { T::finish(this) }; } @@ -2286,12 +1983,12 @@ unsafe fn spawn_cmd_generic( if let Some(stdout) = spawned.stdout { let this_ptr = this.cast::(); if !spawned.memfds[1] { - s.stdout_reader().set_parent(this_ptr); + b.stdout_reader.set_parent(this_ptr); let _ = sys::set_nonblocking(stdout); - *s.remaining_fds() += 1; + b.remaining_fds += 1; { use bun_io::pipe_reader::PosixFlags; - let flags = &mut s.stdout_reader().flags; + let flags = &mut b.stdout_reader.flags; flags.insert(PosixFlags::NONBLOCKING | PosixFlags::SOCKET); flags.remove( PosixFlags::MEMFD @@ -2299,17 +1996,17 @@ unsafe fn spawn_cmd_generic( | PosixFlags::CLOSED_WITHOUT_REPORTING, ); } - if s.stdout_reader().start(stdout, true).is_err() { - s.set_err(format_args!("Failed to start reading stdout")); + if b.stdout_reader.start(stdout, true).is_err() { + b.set_err(format_args!("Failed to start reading stdout")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { T::finish(this) }; } - if let Some(p) = s.stdout_reader().handle.get_poll() { + if let Some(p) = b.stdout_reader.handle.get_poll() { p.set_flag(bun_io::FilePollFlag::Socket); } } else { - s.stdout_reader().set_parent(this_ptr); - s.stdout_reader().start_memfd(stdout); + b.stdout_reader.set_parent(this_ptr); + b.stdout_reader.start_memfd(stdout); } } } @@ -2326,12 +2023,11 @@ unsafe fn spawn_cmd_generic( // callback + double-free on reader close). if let spawn::WindowsStdioResult::Buffer(pipe) = spawned.stderr.take() { debug_assert!(core::ptr::eq(Box::as_ref(&pipe), stderr_pipe_ptr)); - s.stderr_reader().source = Some(bun_io::Source::Pipe(pipe)); - s.stderr_reader() - .set_parent(this.cast::()); - *s.remaining_fds() += 1; - if s.stderr_reader().start_with_current_pipe().is_err() { - s.set_err(format_args!("Failed to start reading stderr")); + b.stderr_reader.source = Some(bun_io::Source::Pipe(pipe)); + b.stderr_reader.set_parent(this.cast::()); + b.remaining_fds += 1; + if b.stderr_reader.start_with_current_pipe().is_err() { + b.set_err(format_args!("Failed to start reading stderr")); return unsafe { T::finish(this) }; } } @@ -2340,18 +2036,18 @@ unsafe fn spawn_cmd_generic( // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. let ev_handle = EventLoopHandle::init(vm_mut().event_loop().cast::<()>()); let process = spawned.to_process(ev_handle, false); - *s.process_slot() = Some(process); + b.process = Some(process); // SAFETY: `process` was just allocated by `to_process`; we hold the only // ref. `this` is the owning `Box` (only freed in `T::finish`, gated on // `has_called_process_exit`), so it outlives `process`. unsafe { (*process).set_exit_handler(bun_spawn::ProcessExit::new(T::EXIT_KIND, this)) }; - // `s` not used past this point — `watch_or_reap` may synchronously invoke + // `b` not used past this point — `watch_or_reap` may synchronously invoke // the exit handler, which can free `this`. // SAFETY: `process` is live; `watch_or_reap` may synchronously invoke the // exit handler (which re-enters `this` via the vtable thunk). match unsafe { (*process).watch_or_reap() } { Err(err) => { - // SAFETY: we hold a ref on `process` via `process_slot()`; it is live. + // SAFETY: we hold a ref on `process` via the base's `process` slot; it is live. if !unsafe { (*process).has_exited() } { // SAFETY: all-zero is a valid Rusage. let rusage = bun_core::ffi::zeroed::(); diff --git a/src/runtime/api/csrf_jsc.rs b/src/runtime/api/csrf_jsc.rs index 7629a69fc59..7a70f9b087e 100644 --- a/src/runtime/api/csrf_jsc.rs +++ b/src/runtime/api/csrf_jsc.rs @@ -71,6 +71,67 @@ fn get_optional_int_u64( Ok(Some(num as u64)) } +/// Reads an optional string option that must be non-empty when present. +/// `label` is the user-facing name used in the error message. +fn parse_non_empty_opt( + options: JSValue, + global: &JSGlobalObject, + property: &'static [u8], + label: &str, +) -> JsResult> { + match get_optional_slice(options, global, property)? { + Some(slice) if slice.slice().is_empty() => { + Err(global.throw_invalid_arguments(format_args!("{label} must be a non-empty string"))) + } + other => Ok(other), + } +} + +/// Reads the optional `encoding` option. Returns `None` when absent. +fn parse_encoding_opt( + options: JSValue, + global: &JSGlobalObject, +) -> JsResult> { + let Some(encoding_js) = options.get(global, "encoding")? else { + return Ok(None); + }; + let encoding_enum = + NodeEncoding::from_js_with_default_on_empty(encoding_js, global, NodeEncoding::Base64url)?; + match encoding_enum { + Some(NodeEncoding::Base64) => Ok(Some(csrf::TokenFormat::Base64)), + Some(NodeEncoding::Base64url) => Ok(Some(csrf::TokenFormat::Base64Url)), + Some(NodeEncoding::Hex) => Ok(Some(csrf::TokenFormat::Hex)), + _ => Err(global.throw_invalid_arguments(format_args!( + "Invalid format: must be 'base64', 'base64url', or 'hex'" + ))), + } +} + +/// Reads the optional `algorithm` option, restricted to the algorithms CSRF +/// supports. Returns `None` when absent. +fn parse_algorithm_opt( + options: JSValue, + global: &JSGlobalObject, +) -> JsResult> { + let Some(algorithm_js) = options.get(global, "algorithm")? else { + return Ok(None); + }; + if !algorithm_js.is_string() { + return Err(global.throw_invalid_argument_type_value("algorithm", "string", algorithm_js)); + } + match algorithm_from_js_case_insensitive(global, algorithm_js)? { + Some( + algo @ (EvpAlgorithm::Blake2b256 + | EvpAlgorithm::Blake2b512 + | EvpAlgorithm::Sha256 + | EvpAlgorithm::Sha384 + | EvpAlgorithm::Sha512 + | EvpAlgorithm::Sha512_256), + ) => Ok(Some(algo)), + _ => Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))), + } +} + /// JS binding function for generating CSRF tokens /// First argument is secret (required), second is options (optional) #[bun_jsc::host_fn] @@ -109,64 +170,16 @@ pub(crate) fn csrf__generate(global: &JSGlobalObject, frame: &CallFrame) -> JsRe } // Extract sessionId (optional) - if let Some(session_id_slice) = get_optional_slice(options_value, global, b"sessionId")? { - if session_id_slice.slice().is_empty() { - return Err(global.throw_invalid_arguments(format_args!( - "sessionId must be a non-empty string" - ))); - } - session_id = Some(session_id_slice); - } + session_id = parse_non_empty_opt(options_value, global, b"sessionId", "sessionId")?; // Extract encoding (optional) - if let Some(encoding_js) = options_value.get(global, "encoding")? { - let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty( - encoding_js, - global, - NodeEncoding::Base64url, - )? - else { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - }; - encoding = match encoding_enum { - NodeEncoding::Base64 => csrf::TokenFormat::Base64, - NodeEncoding::Base64url => csrf::TokenFormat::Base64Url, - NodeEncoding::Hex => csrf::TokenFormat::Hex, - _ => { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - } - }; + if let Some(encoding_opt) = parse_encoding_opt(options_value, global)? { + encoding = encoding_opt; } - if let Some(algorithm_js) = options_value.get(global, "algorithm")? { - if !algorithm_js.is_string() { - return Err(global.throw_invalid_argument_type_value( - "algorithm", - "string", - algorithm_js, - )); - } - let Some(algo) = algorithm_from_js_case_insensitive(global, algorithm_js)? else { - return Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))); - }; + // Extract algorithm (optional) + if let Some(algo) = parse_algorithm_opt(options_value, global)? { algorithm = algo; - match algorithm { - EvpAlgorithm::Blake2b256 - | EvpAlgorithm::Blake2b512 - | EvpAlgorithm::Sha256 - | EvpAlgorithm::Sha384 - | EvpAlgorithm::Sha512 - | EvpAlgorithm::Sha512_256 => {} - _ => { - return Err( - global.throw_invalid_arguments(format_args!("Algorithm not supported")) - ); - } - } } } @@ -249,24 +262,11 @@ pub(crate) fn csrf__verify(global: &JSGlobalObject, frame: &CallFrame) -> JsResu if args.len() > 1 && args[1].is_object() { let options_value = args[1]; - // Extract the secret (required) - if let Some(secret_slice) = get_optional_slice(options_value, global, b"secret")? { - if secret_slice.slice().is_empty() { - return Err(global - .throw_invalid_arguments(format_args!("Secret must be a non-empty string"))); - } - secret = Some(secret_slice); - } + // Extract the secret (optional; falls back to the per-VM default) + secret = parse_non_empty_opt(options_value, global, b"secret", "Secret")?; // Extract sessionId (optional) - if let Some(session_id_slice) = get_optional_slice(options_value, global, b"sessionId")? { - if session_id_slice.slice().is_empty() { - return Err(global.throw_invalid_arguments(format_args!( - "sessionId must be a non-empty string" - ))); - } - session_id = Some(session_id_slice); - } + session_id = parse_non_empty_opt(options_value, global, b"sessionId", "sessionId")?; // Extract maxAge (optional) if let Some(max_age_js) = get_optional_int_u64(options_value, global, "maxAge")? { @@ -274,53 +274,13 @@ pub(crate) fn csrf__verify(global: &JSGlobalObject, frame: &CallFrame) -> JsResu } // Extract encoding (optional) - if let Some(encoding_js) = options_value.get(global, "encoding")? { - let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty( - encoding_js, - global, - NodeEncoding::Base64url, - )? - else { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - }; - encoding = match encoding_enum { - NodeEncoding::Base64 => csrf::TokenFormat::Base64, - NodeEncoding::Base64url => csrf::TokenFormat::Base64Url, - NodeEncoding::Hex => csrf::TokenFormat::Hex, - _ => { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - } - }; + if let Some(encoding_opt) = parse_encoding_opt(options_value, global)? { + encoding = encoding_opt; } - if let Some(algorithm_js) = options_value.get(global, "algorithm")? { - if !algorithm_js.is_string() { - return Err(global.throw_invalid_argument_type_value( - "algorithm", - "string", - algorithm_js, - )); - } - let Some(algo) = algorithm_from_js_case_insensitive(global, algorithm_js)? else { - return Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))); - }; + + // Extract algorithm (optional) + if let Some(algo) = parse_algorithm_opt(options_value, global)? { algorithm = algo; - match algorithm { - EvpAlgorithm::Blake2b256 - | EvpAlgorithm::Blake2b512 - | EvpAlgorithm::Sha256 - | EvpAlgorithm::Sha384 - | EvpAlgorithm::Sha512 - | EvpAlgorithm::Sha512_256 => {} - _ => { - return Err( - global.throw_invalid_arguments(format_args!("Algorithm not supported")) - ); - } - } } } // Verify the token diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 930ca31e707..023bf36d1a4 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -449,6 +449,67 @@ pub struct DevServer { bun_event_loop::impl_timer_owner!(DevServer; from_timer_ptr => memory_visualizer_timer); +/// Exhaustiveness check: destructures a `&DevServer` without `..`, so adding, +/// removing, or renaming a field fails to compile at every invocation +/// (`Drop`, `memory_cost_detailed`), forcing the per-field logic there to be +/// reviewed. All bindings are `_` so nothing is moved or borrowed past the +/// statement. +macro_rules! destructure_dev_server_fields { + ($e:expr) => { + let crate::bake::dev_server::DevServer { + magic: _, + root: _, + inspector_server_id: _, + configuration_hash_key: _, + vm: _, + server: _, + router: _, + route_bundles: _, + graph_safety_lock: _, + client_graph: _, + server_graph: _, + barrel_files_with_deferrals: _, + barrel_needed_exports: _, + incremental_result: _, + route_lookup: _, + html_router: _, + assets: _, + source_maps: _, + bundling_failures: _, + frontend_only: _, + has_tailwind_plugin_hack: _, + server_fetch_function_callback: _, + server_register_update_callback: _, + bun_watcher: _, + directory_watchers: _, + watcher_atomics: _, + testing_batch_events: _, + generation: _, + bundles_since_last_error: _, + framework: _, + bundler_framework_views: _, + bundler_options: _, + server_transpiler: _, + client_transpiler: _, + ssr_transpiler: _, + log: _, + plugin_state: _, + current_bundle: _, + next_bundle: _, + deferred_request_pool: _, + active_websocket_connections: _, + dump_dir: _, + emit_incremental_visualizer_events: _, + emit_memory_visualizer_events: _, + memory_visualizer_timer: _, + has_pre_crash_handler: _, + assume_perfect_incremental_bundling: _, + broadcast_console_log_from_browser_to_server: _, + } = $e; + }; +} +pub(crate) use destructure_dev_server_fields; + pub(super) const INTERNAL_PREFIX: &str = "/_bun"; /// Assets which are routed to the `Assets` storage. pub(super) const ASSET_PREFIX: &str = const_format::concatcp!(INTERNAL_PREFIX, "/asset"); @@ -1070,62 +1131,11 @@ impl Drop for DevServer { // practice, so a plain fetch_add is fine. DEV_SERVER_DEINIT_COUNT_FOR_TESTING.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - // Exhaustiveness check: destructuring without `..` fails to compile when a field is added, - // removed, or renamed, forcing this Drop to be reviewed. All bindings - // are `_` so nothing is moved; cleanup not done explicitly below - // happens via the implicit field drops after this body returns. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = &*self; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a field is added, removed, or renamed, forcing this + // Drop to be reviewed. Cleanup not done explicitly below happens via + // the implicit field drops after this body returns. + destructure_dev_server_fields!(&*self); // WebSockets should be deinitialized before other parts. // `websocket.close()` synchronously dispatches `HmrSocket.onClose`, diff --git a/src/runtime/bake/DevServer/memory_cost.rs b/src/runtime/bake/DevServer/memory_cost.rs index 30c0265a71e..82cee7802b6 100644 --- a/src/runtime/bake/DevServer/memory_cost.rs +++ b/src/runtime/bake/DevServer/memory_cost.rs @@ -33,62 +33,10 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { let mut source_maps: usize = 0; let mut assets: usize = 0; - // Exhaustiveness check: - // destructuring without `..` fails to compile when a DevServer field is - // added, removed, or renamed, forcing the accounting below to be updated. - // All bindings are `_` so nothing is moved or borrowed past this block. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = dev; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a DevServer field is added, removed, or renamed, forcing + // the accounting below to be updated. + crate::bake::dev_server_body::destructure_dev_server_fields!(dev); // does not contain pointers // .assume_perfect_incremental_bundling diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index 8d28a1bc91d..7400e9d1952 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -24,11 +24,11 @@ use bun_paths::{self as paths, PathBuffer}; pub(crate) use crate::api::js_bundler::Plugin; use crate::api::js_bundler::js_bundler::PluginJscExt as _; -// Note: parent `mod.rs` already declares `dev_server` / `framework_router` -// as sibling modules of this file; pull them in instead of re-declaring (which -// would duplicate the module tree and fail on `framework_router` having no +// Note: parent `mod.rs` already declares `framework_router` as a sibling +// module of this file; pull it in instead of re-declaring (which would +// duplicate the module tree and fail on `framework_router` having no // matching filename). -use super::{dev_server, framework_router}; +use super::framework_router; // Note: `pub use dev_server as DevServer` / `framework_router as // FrameworkRouter` are already provided by the parent `mod.rs` (lines 349/369); @@ -1140,38 +1140,6 @@ impl Framework { ) } - pub fn init_transpiler<'a>( - &mut self, - arena: &'a Arena, - log: &mut bun_ast::Log, - mode: Mode, - renderer: Graph, - out: &mut core::mem::MaybeUninit>, - bundler_options: &BuildConfigSubset, - ) -> Result<(), bun_core::Error> { - let source_map: bun_bundler::options::SourceMapOption = match mode { - // Source maps must always be external, as DevServer special cases - // the linking and part of the generation of these. It also relies - // on source maps always being enabled. - Mode::Development => bun_bundler::options::SourceMapOption::External, - // TODO: follow user configuration - _ => bun_bundler::options::SourceMapOption::None, - }; - - self.init_transpiler_with_options( - arena, - log, - mode, - renderer, - out, - bundler_options, - source_map, - None, - None, - None, - ) - } - pub fn init_transpiler_with_options<'a>( &mut self, arena: &'a Arena, @@ -1185,149 +1153,27 @@ impl Framework { minify_syntax: Option, minify_identifiers: Option, ) -> Result<(), bun_core::Error> { - // `ASTMemoryAllocator::enter` returns an RAII `Scope` whose `Drop` - // runs `exit()` at end-of-fn. - let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); - let _ast_scope = ast_memory_allocator.enter(); - - // The caller (`DevServer::init`) hands us an uninitialized slot, so - // use `MaybeUninit::write` (no drop of prior bytes) then reborrow as - // `&mut Transpiler` for the field assignments below. - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( + // The arena slot for the `bake_types::Framework` projection is + // deliberately not tracked here (DevServer's keystone wrapper is the + // path that `drop_in_place`s it; see `init_transpiler_impl` docs). + super::init_transpiler_impl( arena, log, - // `TransformOptions::default()`: every `Option` is `None`, every - // slice empty, every scalar zero/false. - bun_schema::api::TransformOptions::default(), - None, - )?); - - out.options.target = match renderer { - Graph::Client => bun_ast::Target::Browser, - Graph::Server | Graph::Ssr => bun_ast::Target::Bun, - }; - out.options.public_path = match renderer { - Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), - Graph::Server | Graph::Ssr => Box::default(), - }; - out.options.entry_points = Box::default(); - out.options.log = log; - out.options.output_format = match mode { - Mode::Development => bun_bundler::options::Format::InternalBakeDev, - Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, - }; - out.options.out_extensions = bun_collections::StringHashMap::new(); - out.options.hot_module_reloading = mode == Mode::Development; - out.options.code_splitting = mode != Mode::Development; - - // force disable filesystem output, even though bundle_v2 - // is special cased to return before that code is reached. - out.options.output_dir = Box::default(); - - // framework configuration - out.options.react_fast_refresh = mode == Mode::Development - && renderer == Graph::Client - && self.react_fast_refresh.is_some(); - out.options.server_components = self.server_components.is_some(); - - out.options.conditions = bun_bundler::options::ESMConditions::init( - out.options.target.default_conditions(), - out.options.target.is_server_side(), - bundler_options.conditions.keys(), - )?; - if renderer == Graph::Server && self.server_components.is_some() { - out.options.conditions.append_slice(&[b"react-server"])?; - } - if mode == Mode::Development { - // Support `esm-env` package using this condition. - out.options.conditions.append_slice(&[b"development"])?; - } - // Ensure "node" condition is included for server-side rendering - // This helps with package.json imports field resolution - if renderer == Graph::Server || renderer == Graph::Ssr { - out.options.conditions.append_slice(&[b"node"])?; - } - - out.options.production = mode != Mode::Development; - out.options.tree_shaking = mode != Mode::Development; - out.options.minify_syntax = minify_syntax.unwrap_or(mode != Mode::Development); - out.options.minify_identifiers = minify_identifiers.unwrap_or(mode != Mode::Development); - out.options.minify_whitespace = minify_whitespace.unwrap_or(mode != Mode::Development); - out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY projection - // (`bake_types::Framework`); construct it here and give it arena - // lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - // NOTE: interior `Box<[u8]>` in the projection are not dropped by - // bumpalo — bounded per-session, revisit when `bake_types::BuiltInModule` - // is reshaped to `&'a [u8]`. - out.options.framework = Some(&*arena.alloc(self.as_bundler_view())); - out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - - out.options.source_map = source_map; - if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { - out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); - } - // The resolver crate carries a FORWARD_DECL subset of - // `BundleOptions`, so re-project via the dedicated helper rather than - // `Clone`. - out.sync_resolver_opts(); - - out.configure_linker(); - out.configure_defines()?; - - out.options.jsx.development = mode == Mode::Development; - - add_import_meta_defines( - &mut out.options.define, mode, - match renderer { - Graph::Client => Side::Client, - Graph::Server | Graph::Ssr => Side::Server, + renderer, + out, + bundler_options, + super::InitTranspilerOptions { + source_map, + minify_whitespace, + minify_syntax, + minify_identifiers, + has_react_fast_refresh: self.react_fast_refresh.is_some(), + has_server_components: self.server_components.is_some(), + framework_view: self.as_bundler_view(), }, - )?; - - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { - debug_assert_eq!( - bundler_options.define.keys.len(), - bundler_options.define.values.len() - ); - use bun_bundler::DefineDataExt; - for (k, v) in bundler_options - .define - .keys - .iter() - .zip(bundler_options.define.values.iter()) - { - let parsed = - bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; - out.options.define.insert(k, parsed)?; - } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } - } - - if mode != Mode::Development { - // Hide information about the source repository, at the cost of debugging quality. - out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); - } - - // Re-sync after define/naming mutations so the resolver sees the - // final option set. - out.sync_resolver_opts(); - Ok(()) + ) + .map(|_framework_view| ()) } } diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 39d4d9f35b9..35ca15c689e 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -200,12 +200,11 @@ impl Framework { ) } - /// Sets up a per-graph - /// `Transpiler` in place. The full body lives in - /// `bake_body::Framework::init_transpiler_with_options`; this keystone - /// version operates on the keystone `BuildConfigSubset` (which omits - /// `conditions`/`env`/`define`/`drop` until the schema types are - /// const-constructible — those paths default). + /// Sets up a per-graph `Transpiler` in place via `init_transpiler_impl`, + /// with the DevServer defaults: source maps follow `mode` and the three + /// minify overrides always default to `mode != Development` regardless of + /// `BuildConfigSubset`. User-supplied source-map/minify flags are only + /// honored by `init_transpiler_with_options` (bake_body). /// Returns the arena slot for the `bake_types::Framework` projection; caller must `drop_in_place` it. pub fn init_transpiler<'a>( &mut self, @@ -216,79 +215,7 @@ impl Framework { out: &mut core::mem::MaybeUninit>, bundler_options: &BuildConfigSubset, ) -> Result<*mut bun_bundler::bake_types::Framework, bun_core::Error> { - use bun_options_types::schema as bun_schema; - - let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); - let _ast_scope = ast_memory_allocator.enter(); - - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( - arena, - log, - bun_schema::api::TransformOptions::default(), - None, - )?); - - out.options.target = match renderer { - Graph::Client => bun_ast::Target::Browser, - Graph::Server | Graph::Ssr => bun_ast::Target::Bun, - }; - out.options.public_path = match renderer { - Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), - Graph::Server | Graph::Ssr => Box::default(), - }; - out.options.entry_points = Box::default(); - out.options.log = log; - out.options.output_format = match mode { - Mode::Development => bun_bundler::options::Format::InternalBakeDev, - Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, - }; - out.options.out_extensions = bun_collections::StringHashMap::new(); - out.options.hot_module_reloading = mode == Mode::Development; - out.options.code_splitting = mode != Mode::Development; - out.options.output_dir = Box::default(); - - out.options.react_fast_refresh = mode == Mode::Development - && renderer == Graph::Client - && self.react_fast_refresh.is_some(); - out.options.server_components = self.server_components.is_some(); - - out.options.conditions = bun_bundler::options::ESMConditions::init( - out.options.target.default_conditions(), - out.options.target.is_server_side(), - bundler_options.conditions.keys(), - )?; - if renderer == Graph::Server && self.server_components.is_some() { - out.options.conditions.append_slice(&[b"react-server"])?; - } - if mode == Mode::Development { - out.options.conditions.append_slice(&[b"development"])?; - } - if matches!(renderer, Graph::Server | Graph::Ssr) { - out.options.conditions.append_slice(&[b"node"])?; - } - - out.options.production = mode != Mode::Development; - out.options.tree_shaking = mode != Mode::Development; - // The three minify overrides always default to `mode != Development` - // here regardless of `BuildConfigSubset`. User-supplied minify flags - // are only honored by `init_transpiler_with_options` (bake_body). - out.options.minify_syntax = mode != Mode::Development; - out.options.minify_identifiers = mode != Mode::Development; - out.options.minify_whitespace = mode != Mode::Development; - out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY - // projection (`bake_types::Framework`); construct it here and give it - // arena lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - let framework_view: *mut bun_bundler::bake_types::Framework = - arena.alloc(self.as_bundler_view()); - // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, - // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. - out.options.framework = Some(unsafe { &*framework_view }); - out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - out.options.source_map = match mode { + let source_map = match mode { // Source maps must always be external, as DevServer special cases // the linking and part of the generation of these. It also relies // on source maps always being enabled. @@ -298,65 +225,23 @@ impl Framework { bun_bundler::options::SourceMapOption::None } }; - if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { - out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); - } - // The resolver crate carries a FORWARD_DECL subset of `BundleOptions`, so - // re-project via the dedicated helper rather than `Clone`. - out.sync_resolver_opts(); - - out.configure_linker(); - out.configure_defines()?; - out.options.jsx.development = mode == Mode::Development; - - bake_body::add_import_meta_defines( - &mut out.options.define, + init_transpiler_impl( + arena, + log, mode, - match renderer { - Graph::Client => Side::Client, - Graph::Server | Graph::Ssr => Side::Server, + renderer, + out, + bundler_options, + InitTranspilerOptions { + source_map, + minify_whitespace: None, + minify_syntax: None, + minify_identifiers: None, + has_react_fast_refresh: self.react_fast_refresh.is_some(), + has_server_components: self.server_components.is_some(), + framework_view: self.as_bundler_view(), }, - )?; - - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { - debug_assert_eq!( - bundler_options.define.keys.len(), - bundler_options.define.values.len() - ); - use bun_bundler::DefineDataExt; - for (k, v) in bundler_options - .define - .keys - .iter() - .zip(bundler_options.define.values.iter()) - { - let parsed = - bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; - out.options.define.insert(k, parsed)?; - } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } - } - - if mode != Mode::Development { - // Hide information about the source repository, at the cost of debugging quality. - out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); - } - - // Re-sync after define/naming mutations so the - // resolver sees the final option set. - out.sync_resolver_opts(); - Ok(framework_view) + ) } /// Resolves built-in module @@ -475,6 +360,181 @@ impl Framework { } } +/// Caller-specific inputs to `init_transpiler_impl`: the two `Framework` +/// representations contribute their feature flags and `bake_types::Framework` +/// projection here. A `None` minify override defaults to +/// `mode != Development`. +pub(crate) struct InitTranspilerOptions { + pub source_map: bun_bundler::options::SourceMapOption, + pub minify_whitespace: Option, + pub minify_syntax: Option, + pub minify_identifiers: Option, + pub has_react_fast_refresh: bool, + pub has_server_components: bool, + pub framework_view: bun_bundler::bake_types::Framework, +} + +/// Shared body of `Framework::init_transpiler` (keystone, DevServer) and +/// `bake_body::Framework::init_transpiler_with_options` (production): wires +/// the per-graph transpiler options (target/conditions/minify/source +/// map/define/drop) that are identical between the two `Framework` +/// representations, which only contribute the `InitTranspilerOptions` here. +/// +/// Returns the arena slot for the projection; the caller must `drop_in_place` +/// it — interior `Box<[u8]>` are not dropped by bumpalo. (The production path +/// deliberately leaks it: bounded per-session, revisit when +/// `bake_types::BuiltInModule` is reshaped to `&'a [u8]`.) +pub(crate) fn init_transpiler_impl<'a>( + arena: &'a bun_alloc::Arena, + log: &mut bun_ast::Log, + mode: Mode, + renderer: Graph, + out: &mut core::mem::MaybeUninit>, + bundler_options: &BuildConfigSubset, + opts: InitTranspilerOptions, +) -> Result<*mut bun_bundler::bake_types::Framework, bun_core::Error> { + use bun_options_types::schema as bun_schema; + + // `ASTMemoryAllocator::enter` returns an RAII `Scope` whose `Drop` runs + // `exit()` at end-of-fn. + let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); + let _ast_scope = ast_memory_allocator.enter(); + + // The caller hands us an uninitialized slot, so use `MaybeUninit::write` + // (no drop of prior bytes) then reborrow as `&mut Transpiler` for the + // field assignments below. + let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( + arena, + log, + bun_schema::api::TransformOptions::default(), + None, + )?); + + out.options.target = match renderer { + Graph::Client => bun_ast::Target::Browser, + Graph::Server | Graph::Ssr => bun_ast::Target::Bun, + }; + out.options.public_path = match renderer { + Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), + Graph::Server | Graph::Ssr => Box::default(), + }; + out.options.entry_points = Box::default(); + out.options.log = log; + out.options.output_format = match mode { + Mode::Development => bun_bundler::options::Format::InternalBakeDev, + Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, + }; + out.options.out_extensions = bun_collections::StringHashMap::new(); + out.options.hot_module_reloading = mode == Mode::Development; + out.options.code_splitting = mode != Mode::Development; + + // force disable filesystem output, even though bundle_v2 + // is special cased to return before that code is reached. + out.options.output_dir = Box::default(); + + // framework configuration + out.options.react_fast_refresh = + mode == Mode::Development && renderer == Graph::Client && opts.has_react_fast_refresh; + out.options.server_components = opts.has_server_components; + + out.options.conditions = bun_bundler::options::ESMConditions::init( + out.options.target.default_conditions(), + out.options.target.is_server_side(), + bundler_options.conditions.keys(), + )?; + if renderer == Graph::Server && opts.has_server_components { + out.options.conditions.append_slice(&[b"react-server"])?; + } + if mode == Mode::Development { + // Support `esm-env` package using this condition. + out.options.conditions.append_slice(&[b"development"])?; + } + // Ensure "node" condition is included for server-side rendering + // This helps with package.json imports field resolution + if matches!(renderer, Graph::Server | Graph::Ssr) { + out.options.conditions.append_slice(&[b"node"])?; + } + + out.options.production = mode != Mode::Development; + out.options.tree_shaking = mode != Mode::Development; + out.options.minify_syntax = opts.minify_syntax.unwrap_or(mode != Mode::Development); + out.options.minify_identifiers = opts.minify_identifiers.unwrap_or(mode != Mode::Development); + out.options.minify_whitespace = opts.minify_whitespace.unwrap_or(mode != Mode::Development); + out.options.css_chunking = true; + // The bundler crate (lower tier) carries a TYPE_ONLY projection + // (`bake_types::Framework`); arena-allocate it here so `BundleOptions<'a>` + // can borrow it for the bundle pass. + let framework_view: *mut bun_bundler::bake_types::Framework = arena.alloc(opts.framework_view); + // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, + // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. + out.options.framework = Some(unsafe { &*framework_view }); + out.options.inline_entrypoint_import_meta_main = true; + if let Some(ignore) = bundler_options.ignore_dce_annotations { + out.options.ignore_dce_annotations = ignore; + } + + out.options.source_map = opts.source_map; + if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { + out.options.env.behavior = bundler_options.env; + out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); + } + // The resolver crate carries a FORWARD_DECL subset of `BundleOptions`, so + // re-project via the dedicated helper rather than `Clone`. + out.sync_resolver_opts(); + + out.configure_linker(); + out.configure_defines()?; + + out.options.jsx.development = mode == Mode::Development; + + bake_body::add_import_meta_defines( + &mut out.options.define, + mode, + match renderer { + Graph::Client => Side::Client, + Graph::Server | Graph::Ssr => Side::Server, + }, + )?; + + if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { + debug_assert_eq!( + bundler_options.define.keys.len(), + bundler_options.define.values.len() + ); + use bun_bundler::DefineDataExt; + for (k, v) in bundler_options + .define + .keys + .iter() + .zip(bundler_options.define.values.iter()) + { + let parsed = bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; + out.options.define.insert(k, parsed)?; + } + + for drop_item in bundler_options.drop.keys() { + if !drop_item.is_empty() { + let parsed = bun_bundler::defines::DefineData::parse( + drop_item, b"", true, true, log, arena, + )?; + out.options.define.insert(drop_item, parsed)?; + } + } + } + + if mode != Mode::Development { + // Hide information about the source repository, at the cost of debugging quality. + out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); + out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); + out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); + } + + // Re-sync after define/naming mutations so the resolver sees the + // final option set. + out.sync_resolver_opts(); + Ok(framework_view) +} + /// `bake.SplitBundlerOptions` — per-graph bundler config + shared plugin. #[derive(Default)] pub struct SplitBundlerOptions { @@ -555,56 +615,24 @@ impl From for Framework { } } } -impl From for BuildConfigSubset { - fn from(src: bake_body::BuildConfigSubset) -> Self { - // `BuildConfigSubset` mirrors the field-set - // `Framework::init_transpiler` reads (everything except `loader` / - // `source_map`, which only `init_transpiler_with_options` honours). - Self { - ignore_dce_annotations: src.ignore_dce_annotations, - conditions: src.conditions, - drop: src.drop, - env: src.env, - env_prefix: src.env_prefix, - define: src.define, - minify_syntax: src.minify_syntax, - minify_identifiers: src.minify_identifiers, - minify_whitespace: src.minify_whitespace, - } - } -} impl From for SplitBundlerOptions { fn from(src: bake_body::SplitBundlerOptions) -> Self { Self { // `bake_body::Plugin` and keystone `jsc::Plugin` both alias // `crate::api::js_bundler::Plugin` — same nominal type, no cast. plugin: src.plugin, - client: src.client.into(), - server: src.server.into(), - ssr: src.ssr.into(), + client: src.client, + server: src.server, + ssr: src.ssr, } } } /// `bake.SplitBundlerOptions.BuildConfigSubset`. Full body (with `from_js`) -/// lives in `bake_body.rs`; this keystone mirror carries every field that -/// `Framework::init_transpiler` reads so DevServer's -/// per-graph transpilers see bunfig `[serve.static]` define/env/conditions. -#[derive(Default)] -pub struct BuildConfigSubset { - pub ignore_dce_annotations: Option, - pub conditions: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub drop: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub env: bun_options_types::schema::api::DotEnvBehavior, - pub env_prefix: Option<&'static [u8]>, - pub define: bun_options_types::schema::api::StringMap, - pub minify_syntax: Option, - pub minify_identifiers: Option, - pub minify_whitespace: Option, - // `loader`/`source_map` intentionally omitted — only - // `init_transpiler_with_options` (bake_body) honours those, and DevServer - // never calls that path. -} +/// lives in `bake_body.rs`; DevServer's `init_transpiler` reads everything +/// except `loader`/`source_map`, which only `init_transpiler_with_options` +/// honours. +pub use bake_body::BuildConfigSubset; /// `bake.HmrRuntime` — embedded HMR runtime code + precomputed line count. /// Canonical definition; `bake_body::HmrRuntime` re-exports this diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index f61de35ad56..b14b9bde63e 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -35,7 +35,6 @@ use bun_resolver as resolver; use crate::cli::command::{Context, HotReload}; use bun_options_types::context::MacroOptions; -use bun_options_types::offline_mode::OfflineMode; use bun_bundler::options::OutputKind; @@ -135,35 +134,7 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { vm.argv.clone_from(&ctx.passthrough); vm.arena = NonNull::new(&raw mut arena); // vm.allocator = arena.arena() — dropped per §Allocators - // `BundleOptions.install` is `Option>`, so no - // lifetime-extension cast is needed. - let install_ptr = ctx.install.as_deref().map(NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - b.resolver.opts.prefer_offline_install = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Offline; - // Note: `bun_resolver::options::BundleOptions` has no - // `prefer_latest_install` field; compute the value once - // and assign only to `b.options` (which does carry it). The resolver - // never reads it. - let prefer_latest = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Latest; - b.options.global_cache = b.resolver.opts.global_cache; - b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; - b.options.prefer_latest_install = prefer_latest; - // SAFETY: `b.env` is the Transpiler-owned `*mut Loader`; store it - // as `NonNull` (not `&Loader`) because `configure_defines()` below - // reborrows the same allocation as `&mut Loader` via `run_env_loader()`, - // which would alias a live `&Loader` here. The Loader outlives the - // resolver (process-lifetime singleton or VM-owned). - b.resolver.env_loader = NonNull::new(b.env); + crate::cli::run_command::wire_install_options(b, ctx); b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; b.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; diff --git a/src/runtime/crypto/CryptoHasher.rs b/src/runtime/crypto/CryptoHasher.rs index abaaa7a42b3..e591693964a 100644 --- a/src/runtime/crypto/CryptoHasher.rs +++ b/src/runtime/crypto/CryptoHasher.rs @@ -49,6 +49,97 @@ fn is_bun_file_blob(input: &BlobOrStringOrBuffer) -> bool { } } +/// Parsed form of the optional `digest()`/`hash()` output argument: either a +/// caller-provided byte sink (`None` → allocate a fresh buffer), or an +/// encoding name to stringify the digest with. +enum DigestOutput { + Bytes(Option), + Encoding(Encoding), +} + +fn parse_digest_output( + global: &JSGlobalObject, + output: Option, +) -> JsResult { + let Some(string_or_buffer) = output else { + return Ok(DigestOutput::Bytes(None)); + }; + if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { + return Ok(DigestOutput::Bytes(Some(buffer.buffer))); + } + // `inline else => |*str|` — every non-buffer arm yields a string-like + // `defer str.deinit()` — handled by Drop. + let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { + return Err(global + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!( + "Unknown encoding: {}", + bstr::BStr::new(string_or_buffer.slice()) + ), + ) + .throw()); + }; + Ok(DigestOutput::Encoding(encoding)) +} + +/// Hand-expanded `wrapInstanceMethod` decode for the trailing +/// `?Node.StringOrBuffer` parameter (instance-method arm: +/// empty/undefined/null → None). +fn digest_output_argument( + global: &JSGlobalObject, + callframe: &CallFrame, +) -> JsResult> { + let arguments = callframe.arguments_old::<1>(); + if arguments.len > 0 { + let arg = arguments.ptr[0]; + if !arg.is_empty_or_undefined_or_null() { + return match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + }; + } + } + Ok(None) +} + +/// Hand-expanded static-method decode for the `Node.BlobOrStringOrBuffer` +/// input parameter. +fn hash_input_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult { + if let Some(arg) = arg { + if let Some(b) = BlobOrStringOrBuffer::from_js(global, arg)? { + return Ok(b); + } + } + Err(global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))) +} + +/// Hand-expanded static-method decode for the trailing `?Node.StringOrBuffer` +/// output parameter (static-method arm: only `undefined` → None). +fn hash_output_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult> { + match arg { + Some(arg) => match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + if arg.is_undefined() { + Ok(None) + } else { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + } + }, + None => Ok(None), + } +} + /// `union(enum)` → Rust enum with payload variants. /// `.classes.ts`-backed type: the C++ JSCell wrapper stays generated; this is the `m_ctx` payload. /// @@ -218,24 +309,7 @@ impl CryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -243,58 +317,20 @@ impl CryptoHasher { /// `(algorithm string, input, optional output buffer/encoding)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<3>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; let algorithm = { - let Some(string_value) = next_eat() else { + if arguments.len == 0 { return Err(global.throw_invalid_arguments(format_args!("Missing argument"))); - }; + } + let string_value = arguments.ptr[0]; if string_value.is_undefined_or_null() { return Err(global.throw_invalid_arguments(format_args!("Expected string"))); } string_value.get_zig_string(global)? }; - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; + let input = hash_input_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; + let output = hash_output_argument(global, (arguments.len > 2).then(|| arguments.ptr[2]))?; Self::hash_(global, algorithm, &input, output) } @@ -437,28 +473,11 @@ impl CryptoHasher { }; // `defer evp.deinit()` — handled by Drop on `evp`. - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_to_bytes(global, &mut evp, input, Some(ab)); + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_to_bytes(global, &mut evp, input, ab), + DigestOutput::Encoding(encoding) => { + Self::hash_to_encoding(global, &mut evp, input, encoding) } - // `inline else => |*str|` — every non-buffer arm yields a string-like - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - Self::hash_to_encoding(global, &mut evp, input, encoding) - } else { - Self::hash_to_bytes(global, &mut evp, input, None) } } @@ -670,27 +689,9 @@ impl CryptoHasher { global: &JSGlobalObject, output: Option, ) -> JsResult { - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return this.digest_to_bytes(global, Some(ab)); - } - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - this.digest_to_encoding(global, encoding) - } else { - this.digest_to_bytes(global, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => this.digest_to_bytes(global, ab), + DigestOutput::Encoding(encoding) => this.digest_to_encoding(global, encoding), } } @@ -909,30 +910,15 @@ impl CryptoHasherZig { input: &BlobOrStringOrBuffer, output: Option, ) -> JsResult { - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_by_name_inner_to_bytes::(global, input, Some(ab)); + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_by_name_inner_to_bytes::(global, input, ab), + DigestOutput::Encoding(Encoding::Buffer) => { + Self::hash_by_name_inner_to_bytes::(global, input, None) } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - if encoding == Encoding::Buffer { - return Self::hash_by_name_inner_to_bytes::(global, input, None); + DigestOutput::Encoding(encoding) => { + Self::hash_by_name_inner_to_string::(global, input, encoding) } - - return Self::hash_by_name_inner_to_string::(global, input, encoding); } - Self::hash_by_name_inner_to_bytes::(global, input, None) } fn hash_by_name_inner_to_string( @@ -1207,24 +1193,7 @@ impl StaticCryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -1234,49 +1203,8 @@ impl StaticCryptoHasher { /// `(*JSGlobalObject, Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<2>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; - - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; - + let input = hash_input_argument(global, (arguments.len > 0).then(|| arguments.ptr[0]))?; + let output = hash_output_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; Self::hash_(global, &input, output) } @@ -1314,29 +1242,37 @@ impl StaticCryptoHasher { encoding.encode_with_max_size(global, EVP_MAX_MD_SIZE_USIZE, output_digest_buf.as_ref()) } + /// Validate the optional caller-provided output buffer and return the + /// destination digest array (falling back to `fallback`). + fn output_digest<'a>( + global: &JSGlobalObject, + output: Option<&ArrayBuffer>, + fallback: &'a mut H::Digest, + ) -> JsResult<&'a mut H::Digest> { + let Some(output_buf) = output else { + return Ok(fallback); + }; + if output_buf.byte_slice().len() < H::DIGEST { + return Err(global.throw_invalid_arguments(format_args!( + "TypedArray must be at least {} bytes", + H::DIGEST + ))); + } + // SAFETY: `byte_slice().len() >= H::DIGEST` checked above; + // `H::Digest = [u8; H::DIGEST]`; `output_buf.ptr` is the JSC-owned + // writable backing store. Build the `&mut` directly from the raw + // `*mut u8` field — never via `&[u8].as_ptr()` (Stacked-Borrows UB). + Ok(unsafe { &mut *output_buf.ptr.cast::() }) + } + fn hash_to_bytes( global: &JSGlobalObject, input: &BlobOrStringOrBuffer, output: Option, ) -> JsResult { let mut output_digest_buf: H::Digest = H::new_digest(); - let output_digest_slice: &mut H::Digest; - if let Some(output_buf) = &output { - let bytes_len = output_buf.byte_slice().len(); - if bytes_len < H::DIGEST { - return Err(global.throw_invalid_arguments(format_args!( - "TypedArray must be at least {} bytes", - H::DIGEST - ))); - } - // SAFETY: `bytes_len >= H::DIGEST` checked above; `H::Digest = [u8; H::DIGEST]`; - // `output_buf.ptr` is the JSC-owned writable backing store. Build the - // `&mut` directly from the raw `*mut u8` field — never via - // `&[u8].as_ptr()` (Stacked-Borrows UB). - output_digest_slice = unsafe { &mut *output_buf.ptr.cast::() }; - } else { - output_digest_slice = &mut output_digest_buf; - } + let output_digest_slice = + Self::output_digest(global, output.as_ref(), &mut output_digest_buf)?; // SAFETY: `boring_engine` returns the VM-owned engine (live for the // process) or null; the else arm passes null. @@ -1368,26 +1304,9 @@ impl StaticCryptoHasher { ))); } - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_to_bytes(global, input, Some(ab)); - } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - Self::hash_to_encoding(global, input, encoding) - } else { - Self::hash_to_bytes(global, input, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_to_bytes(global, input, ab), + DigestOutput::Encoding(encoding) => Self::hash_to_encoding(global, input, encoding), } } @@ -1458,26 +1377,9 @@ impl StaticCryptoHasher { ) .throw()); } - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return this.digest_to_bytes(global, Some(ab)); - } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - this.digest_to_encoding(global, encoding) - } else { - this.digest_to_bytes(global, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => this.digest_to_bytes(global, ab), + DigestOutput::Encoding(encoding) => this.digest_to_encoding(global, encoding), } } @@ -1487,23 +1389,8 @@ impl StaticCryptoHasher { output: Option, ) -> JsResult { let mut output_digest_buf: H::Digest = H::new_digest(); - let output_digest_slice: &mut H::Digest; - if let Some(output_buf) = &output { - let bytes_len = output_buf.byte_slice().len(); - if bytes_len < H::DIGEST { - return Err(global.throw_invalid_arguments(format_args!( - "TypedArray must be at least {} bytes", - H::DIGEST - ))); - } - // SAFETY: `bytes_len >= H::DIGEST`; `H::Digest = [u8; H::DIGEST]`; - // `output_buf.ptr` is the JSC-owned writable backing store. Build the - // `&mut` directly from the raw `*mut u8` field — never via - // `&[u8].as_ptr()` (Stacked-Borrows UB). - output_digest_slice = unsafe { &mut *output_buf.ptr.cast::() }; - } else { - output_digest_slice = &mut output_digest_buf; - } + let output_digest_slice = + Self::output_digest(global, output.as_ref(), &mut output_digest_buf)?; self.hashing.with_mut(|h| h.final_(output_digest_slice)); self.digested.set(true); @@ -1511,7 +1398,7 @@ impl StaticCryptoHasher { if let Some(output_buf) = output { Ok(output_buf.value) } else { - ArrayBuffer::create_uint8_array(global, output_digest_buf.as_ref()) + ArrayBuffer::create_uint8_array(global, output_digest_slice.as_ref()) } } diff --git a/src/runtime/crypto/PasswordObject.rs b/src/runtime/crypto/PasswordObject.rs index 950cc59d114..307ab1d8fb0 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -819,6 +819,40 @@ pub(crate) fn js_password_object_hash_sync( // ─── verify host functions ──────────────────────────────────────────────── +/// Parse the optional third `verify(password, hash, algorithm)` argument. +fn parse_verify_algorithm( + global_object: &JSGlobalObject, + arguments: &[JSValue], +) -> JsResult> { + let Some(&arg) = arguments.get(2) else { + return Ok(None); + }; + + if arg.is_empty_or_undefined_or_null() { + return Ok(None); + } + + if !arg.is_string() { + return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); + } + + let algorithm_string = arg.get_zig_string(global_object)?; + + match algorithm_from_zig_string(&algorithm_string) { + Some(a) => Ok(Some(a)), + None => { + if !global_object.has_exception() { + return Err(global_object.throw_invalid_argument_type( + "verify", + "algorithm", + UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, + )); + } + Err(JsError::Thrown) + } + } +} + // Once we have bindings generator, this should be replaced with a generated function #[bun_jsc::host_fn] pub(crate) fn js_password_object_verify( @@ -832,29 +866,7 @@ pub(crate) fn js_password_object_verify( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Err(JsError::Thrown); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; // TODO: this most likely should error like `verifySync` instead of stringifying. // @@ -913,29 +925,7 @@ pub(crate) fn js_password_object_verify_sync( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Ok(JSValue::ZERO); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; let Some(password) = StringOrBuffer::from_js(global_object, arguments[0])? else { return Err(global_object.throw_invalid_argument_type( diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index 145e2a5aaa7..ec8cf815d86 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -194,7 +194,7 @@ pub(super) mod lib_info { return lib_c::lookup(this, query, global_this); }; - let key = get_addr_info_request::PendingCacheKey::init(query); + let key = PendingCacheKey::init_query(query); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -253,7 +253,7 @@ pub(super) mod lib_info { // SAFETY: request is exclusively owned; freed below via heap::take. unsafe { if (*request).cache.pending_cache() { - // Release the pending-cache slot. `getOrPutIntoPendingCache` already + // Release the pending-cache slot. `get_or_put_into_pending_cache` already // set the `used` bit, so failing to unset it here permanently orphans // the slot and leaves `buffer[pos].lookup` pointing at the request we // are about to free (UAF on the next `.inflight` hit). @@ -322,7 +322,7 @@ pub(super) mod lib_c { query_init: &GetAddrInfo, global_this: &JSGlobalObject, ) -> JSValue { - let key = get_addr_info_request::PendingCacheKey::init(query_init); + let key = PendingCacheKey::init_query(query_init); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -418,7 +418,7 @@ pub(super) mod lib_uv_backend { query: GetAddrInfo, global_this: &JSGlobalObject, ) -> JsResult { - let key = get_addr_info_request::PendingCacheKey::init(&query); + let key = PendingCacheKey::init_query(&query); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -600,34 +600,97 @@ pub struct ResolveInfoRequest { pub tail: *mut CAresLookup, // INTRUSIVE — points at `head` or last appended node } -pub mod resolve_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut ResolveInfoRequest, - } +/// Request types holding an intrusive `head`/`tail` list of lookup nodes, so the +/// shared `PendingCacheKey` can append a waiter while the request is in flight. +pub trait HasTail { + type Node; + /// Append `node` after the current tail and advance `tail`. + /// + /// # Safety + /// `this` and its current `tail` must point at live nodes. + unsafe fn append_node(this: *mut Self, node: *mut Self::Node); +} - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresLookup) { - // SAFETY: lookup/tail are valid while request is in the pending cache +macro_rules! impl_has_tail { + (<$T:ident: $bound:path> $req:ty => $node:ty) => { + impl<$T: $bound> HasTail for $req { impl_has_tail!(@body $node); } + }; + ($req:ty => $node:ty) => { + impl HasTail for $req { impl_has_tail!(@body $node); } + }; + (@body $node:ty) => { + type Node = $node; + unsafe fn append_node(this: *mut Self, node: *mut Self::Node) { + // SAFETY: fn contract — `this` and its current `tail` are live. unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; + let tail = (*this).tail; + (*tail).next = NonNull::new(node); + (*this).tail = node; } } + }; +} - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } +impl_has_tail!( ResolveInfoRequest => CAresLookup); + +/// Pending-cache slot key: dedupes in-flight DNS requests by `{hash, len, name}` +/// and points at the request whose intrusive list collects waiting lookups. +pub struct PendingCacheKey { + pub hash: u64, + pub len: u16, + pub name: Box<[u8]>, + pub lookup: *mut Req, +} + +/// Request types whose pending-cache key hashes only the lookup name. +/// `GetAddrInfoRequest` is deliberately excluded: its keys must be built with +/// [`PendingCacheKey::init_query`], which hashes `port` + `options` + `name`. +pub trait NameKeyed: HasTail {} + +impl NameKeyed for ResolveInfoRequest {} +impl NameKeyed for GetHostByAddrInfoRequest {} +impl NameKeyed for GetNameInfoRequest {} + +impl PendingCacheKey { + pub(crate) fn append(&mut self, node: *mut Req::Node) { + // SAFETY: lookup/tail are valid while request is in the pending cache + unsafe { Req::append_node(self.lookup, node) } + } + + /// `{ hash, len, name, lookup: null }` copy for `HiveArray::get_init`. + /// `lookup` is filled in later by `*Request::init` once the request has + /// been heap-allocated; until then it is a defined null rather than uninit + /// garbage, so the `iter_set` loop in `get_or_put_into_pending_cache` can + /// safely materialise `&mut PendingCacheKey` over the slot. + pub(crate) fn unlinked(&self) -> Self { + Self { + hash: self.hash, + len: self.len, + name: self.name.clone(), + lookup: ptr::null_mut(), + } + } +} + +impl PendingCacheKey { + pub(crate) fn init(name: &[u8]) -> Self { + Self { + hash: wyhash(name), + len: name.len() as u16, + name: Box::<[u8]>::from(name), + lookup: ptr::null_mut(), + } + } +} + +impl PendingCacheKey { + /// addr-info keys hash `port` + `options` + `name`, not just the name bytes. + pub(crate) fn init_query(query: &GetAddrInfo) -> Self { + Self { + hash: query.hash(), + len: query.name.len() as u16, + name: query.name.clone(), + lookup: ptr::null_mut(), } } } @@ -740,37 +803,7 @@ pub struct GetHostByAddrInfoRequest { pub tail: *mut CAresReverse, // INTRUSIVE } -pub mod get_host_by_addr_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetHostByAddrInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresReverse) { - // SAFETY: lookup/tail are valid while request is in the pending cache - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; - } - } - - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } - } - } -} +impl_has_tail!(GetHostByAddrInfoRequest => CAresReverse); impl GetHostByAddrInfoRequest { /// Reverse lookups always cache through `pending_addr_cache_cares`, so no @@ -991,37 +1024,7 @@ pub struct GetNameInfoRequest { pub tail: *mut CAresNameInfo, // INTRUSIVE } -pub mod get_name_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetNameInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresNameInfo) { - // SAFETY: lookup/tail are valid while request is in the pending cache - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; - } - } - - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } - } - } -} +impl_has_tail!(GetNameInfoRequest => CAresNameInfo); impl GetNameInfoRequest { pub(crate) fn init( @@ -1134,6 +1137,8 @@ pub struct GetAddrInfoRequest { pub task: thread_pool::Task, } +impl_has_tail!(GetAddrInfoRequest => DNSLookup); + pub mod get_addr_info_request { use super::*; @@ -1141,33 +1146,6 @@ pub mod get_addr_info_request { /// on the work pool, then re-enters the JS thread via `then`. pub type Task = jsc::work_task::WorkTask; - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetAddrInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, dns_lookup: *mut DNSLookup) { - // SAFETY: `lookup`/`tail` are valid while the request sits in the pending cache. - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(dns_lookup); - (*self.lookup).tail = dns_lookup; - } - } - - pub(crate) fn init(query: &GetAddrInfo) -> Self { - Self { - hash: query.hash(), - len: query.name.len() as u16, - name: query.name.clone(), - lookup: ptr::null_mut(), - } - } - } - #[derive(Default)] pub struct BackendLibInfo { /// OWNED hive slot from `FilePoll::init` (returned via `FilePoll::deinit`, @@ -3610,28 +3588,22 @@ hostent_ttls_newtype!( parse_aaaa ); -pub type PendingCache = HiveArray; -type SrvPendingCache = - HiveArray, 32>; -type SoaPendingCache = - HiveArray, 32>; -type TxtPendingCache = - HiveArray, 32>; -type NaptrPendingCache = - HiveArray, 32>; -type MxPendingCache = - HiveArray, 32>; -type CaaPendingCache = - HiveArray, 32>; -type NSPendingCache = HiveArray, 32>; -type PtrPendingCache = HiveArray, 32>; -type CnamePendingCache = HiveArray, 32>; -type APendingCache = HiveArray, 32>; -type AAAAPendingCache = HiveArray, 32>; -type AnyPendingCache = - HiveArray, 32>; -type AddrPendingCache = HiveArray; -type NameInfoPendingCache = HiveArray; +pub type PendingCache = HiveArray, 32>; +type ResolvePendingCache = HiveArray>, 32>; +type SrvPendingCache = ResolvePendingCache; +type SoaPendingCache = ResolvePendingCache; +type TxtPendingCache = ResolvePendingCache; +type NaptrPendingCache = ResolvePendingCache; +type MxPendingCache = ResolvePendingCache; +type CaaPendingCache = ResolvePendingCache; +type NSPendingCache = ResolvePendingCache; +type PtrPendingCache = ResolvePendingCache; +type CnamePendingCache = ResolvePendingCache; +type APendingCache = ResolvePendingCache; +type AAAAPendingCache = ResolvePendingCache; +type AnyPendingCache = ResolvePendingCache; +type AddrPendingCache = HiveArray, 32>; +type NameInfoPendingCache = HiveArray, 32>; #[cfg(windows)] type PollType = UvDnsPoll; @@ -3732,18 +3704,11 @@ impl UvDnsPoll { } } -#[derive(Clone, Copy)] -pub enum CacheHit { - Inflight(*mut get_addr_info_request::PendingCacheKey), // BORROW_FIELD into resolver buffer - New(*mut get_addr_info_request::PendingCacheKey), // BORROW_FIELD into resolver buffer - Disabled, -} +pub type CacheHit = LookupCacheHit; pub enum LookupCacheHit { - // The request type is threaded via `R`; `PendingCacheKey` resolves - // through `HasPendingCacheKey`. - Inflight(*mut R::PendingCacheKey), // BORROW_FIELD - New(*mut R::PendingCacheKey), // BORROW_FIELD + Inflight(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer + New(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer Disabled, } @@ -3754,11 +3719,9 @@ impl Clone for LookupCacheHit { } impl Copy for LookupCacheHit {} -/// Associates a request type with its `PendingCacheKey` and the matching `HiveArray` +/// Associates a request type with the matching pending-cache `HiveArray` /// field on `Resolver`. -pub trait HasPendingCacheKey { - type PendingCacheKey; - +pub trait HasPendingCacheKey: HasTail + Sized { /// Return the per-request-type pending HiveArray field on `Resolver`. /// `field` is the runtime tag selecting which field (some request types are reachable /// via more than one field, e.g. `pending_host_cache_{cares,native}`). @@ -3770,122 +3733,50 @@ pub trait HasPendingCacheKey { fn pending_cache( resolver: &Resolver, field: PendingCacheField, - ) -> &mut HiveArray; - - /// `key.hash` — all `PendingCacheKey` shapes carry `{ hash: u64, len: u16, lookup: *mut _ }`. - fn key_hash(key: &Self::PendingCacheKey) -> u64; - /// `key.len` - fn key_len(key: &Self::PendingCacheKey) -> u16; - fn key_name(key: &Self::PendingCacheKey) -> &[u8]; - /// Construct a fully-initialized `PendingCacheKey { hash, len, lookup: null }` - /// for `HiveArray::get_init`. `lookup` is filled in later by `*Request::init` - /// once the request has been heap-allocated; until then it is a defined null - /// rather than uninit garbage, so the `iter_set` loop in - /// `get_or_put_into_resolve_pending_cache` can safely materialise - /// `&mut PendingCacheKey` over the slot. - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey; + ) -> &mut HiveArray, 32>; } impl HasPendingCacheKey for ResolveInfoRequest { - type PendingCacheKey = resolve_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { resolver.pending_cache_for::(field) } - #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - resolve_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } - } } impl HasPendingCacheKey for GetHostByAddrInfoRequest { - type PendingCacheKey = get_host_by_addr_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, _field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { // SAFETY: see `HasPendingCacheKey::pending_cache` doc — short, // non-reentrant borrow on the single JS thread. unsafe { resolver.pending_addr_cache_cares.get_mut() } } - #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - get_host_by_addr_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } - } } impl HasPendingCacheKey for GetNameInfoRequest { - type PendingCacheKey = get_name_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, _field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { // SAFETY: see `HasPendingCacheKey::pending_cache` doc — short, // non-reentrant borrow on the single JS thread. unsafe { resolver.pending_nameinfo_cache_cares.get_mut() } } +} + +impl HasPendingCacheKey for GetAddrInfoRequest { #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - get_name_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } + fn pending_cache( + resolver: &Resolver, + field: PendingCacheField, + ) -> &mut HiveArray, 32> { + resolver.pending_host_cache(field) } } @@ -3946,6 +3837,110 @@ impl RecordType { pub const DEFAULT: Self = RecordType::A; } +/// Intrusive pending-chain node shared by the `drain_pending_*` family. +trait PendingChainNode: Sized { + fn chain_next(&self) -> Option>; + fn chain_global(&self) -> &JSGlobalObject; +} + +macro_rules! impl_pending_chain_node { + ($($node:ty),* $(,)?) => {$( + impl PendingChainNode for $node { + #[inline] + fn chain_next(&self) -> Option> { + self.next + } + #[inline] + fn chain_global(&self) -> &JSGlobalObject { + self.global_this() + } + } + )*}; +} +impl_pending_chain_node!(DNSLookup, CAresReverse, CAresNameInfo); + +impl PendingChainNode for CAresLookup { + #[inline] + fn chain_next(&self) -> Option> { + self.next + } + #[inline] + fn chain_global(&self) -> &JSGlobalObject { + self.global_this() + } +} + +/// Error-arm skeleton shared by the `drain_pending_*` family: hand the +/// in-place chain head to `process`, free the boxed request via +/// `consume_head`, then walk the remaining (individually boxed) tail nodes. +/// +/// SAFETY: `head` must point at the intrusive head embedded in the live, +/// heap-allocated request held by the pending-cache slot. `consume_head` must +/// consume exactly that request (via `heap::take`) without touching the tail +/// nodes, and `process` must consume each node it is handed (the per-type +/// `process_*` contract). +unsafe fn drain_chain_err( + head: *mut Node, + mut process: impl FnMut(*mut Node), + consume_head: impl FnOnce(), +) { + // SAFETY: see fn contract — each node's `next` is read before the node is + // consumed. + unsafe { + let mut pending = (*head).chain_next(); + process(head); + consume_head(); + + while let Some(value) = pending { + pending = (*value.as_ptr()).chain_next(); + process(value.as_ptr()); + } + } +} + +/// Success-arm skeleton shared by the `drain_pending_*` family. `array` is +/// the JS response already materialized for `prev_global` (the head's +/// global); `to_js` re-materializes it whenever a tail node belongs to a +/// different global. `ensure_still_alive` brackets every `on_complete` so the +/// conservative stack scan keeps `array` rooted across the completion +/// callbacks. +/// +/// SAFETY: same contract as [`drain_chain_err`], with `on_complete` consuming +/// each node it is handed. Additionally, `to_js` must not append to or +/// consume chain nodes: each node's `next` is snapshotted only as the walk +/// reaches it, after earlier `to_js`/`on_complete` calls have run. +unsafe fn drain_chain_ok<'a, Node: PendingChainNode + 'a>( + head: *mut Node, + mut array: JSValue, + mut prev_global: &'a JSGlobalObject, + mut to_js: impl FnMut(&JSGlobalObject) -> JSValue, + mut on_complete: impl FnMut(*mut Node, JSValue), + consume_head: impl FnOnce(), +) { + // SAFETY: see fn contract — each node's `next` is read before the node is + // consumed. + unsafe { + let mut pending = (*head).chain_next(); + array.ensure_still_alive(); + on_complete(head, array); + consume_head(); + array.ensure_still_alive(); + + while let Some(value) = pending { + let new_global = (*value.as_ptr()).chain_global(); + if !core::ptr::eq(prev_global, new_global) { + array = to_js(new_global); + prev_global = new_global; + } + pending = (*value.as_ptr()).chain_next(); + + array.ensure_still_alive(); + on_complete(value.as_ptr(), array); + array.ensure_still_alive(); + } + } +} + impl Resolver { /// Dereference the back-pointer to the VirtualMachine. /// SAFETY: VirtualMachine outlives the Resolver (BACKREF, see field decl). @@ -4232,7 +4227,7 @@ impl Resolver { /// Dispatch to a typed ResolveInfoRequest cache by record type. // Each per-record cache is a distinct monomorphization of - // `HiveArray, 32>`; `PendingCacheKey` is + // `ResolvePendingCache<_>`; `PendingCacheKey>` is // layout-identical for all `T` (only the `*mut ResolveInfoRequest` payload's pointee // type differs), so reinterpreting the field reference at the caller's `T` is sound when // `T::CACHE_FIELD` selects the matching field. @@ -4240,21 +4235,16 @@ impl Resolver { fn pending_cache_for( &self, _field: PendingCacheField, - ) -> &mut HiveArray, 32> { + ) -> &mut ResolvePendingCache { macro_rules! field { ($f:ident) => { // SAFETY: the matched arm guarantees `self.$f` *is* - // `JsCell, 32>>` for this `T::CACHE_FIELD`; + // `JsCell>` for this `T::CACHE_FIELD`; // the cast is an identity transmute (same layout, same lifetime). // R-2: `JsCell::as_ptr` projects `&mut` from `&self`; caller // holds the borrow only for a short, non-reentrant window // (see `pending_host_cache` doc). - unsafe { - &mut *self - .$f - .as_ptr() - .cast::, 32>>() - } + unsafe { &mut *self.$f.as_ptr().cast::>() } }; } match T::CACHE_FIELD { @@ -4285,24 +4275,24 @@ impl Resolver { &self, index: u8, field: PendingCacheField, - ) -> get_addr_info_request::PendingCacheKey { + ) -> PendingCacheKey { let cache = self.pending_host_cache(field); - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() } - fn get_key_addr(&self, index: u8) -> get_host_by_addr_info_request::PendingCacheKey { + fn get_key_addr(&self, index: u8) -> PendingCacheKey { self.pending_addr_cache_cares.with_mut(|cache| { - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() }) } - fn get_key_nameinfo(&self, index: u8) -> get_name_info_request::PendingCacheKey { + fn get_key_nameinfo(&self, index: u8) -> PendingCacheKey { self.pending_nameinfo_cache_cares.with_mut(|cache| { - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() @@ -4322,63 +4312,46 @@ impl Resolver { let key = { let cache = self.pending_cache_for::(T::CACHE_FIELD); - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresLookup::::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-allocated reply freed by `_free_addr` below. + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| CAresLookup::::process_resolve(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresLookup::::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-allocated reply freed by `_free_addr` below. - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - let mut array = (*addr) - .to_js_response(prev_global, T::TYPE_NAME) + let head_global = (*head).global_this(); + let array = (*addr) + .to_js_response(head_global, T::TYPE_NAME) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards // SAFETY: addr is the c-ares-allocated reply; freed once after all consumers run. let _free_addr = scopeguard::guard(addr, |a| T::destroy(a)); - array.ensure_still_alive(); - CAresLookup::::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = (*addr) - .to_js_response(new_global, T::TYPE_NAME) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresLookup::::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + (*addr) + .to_js_response(global, T::TYPE_NAME) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresLookup::::on_complete(node, value), + consume_head, + ); } } @@ -4394,56 +4367,39 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - DNSLookup::process_get_addr_info( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-allocated AddrInfo freed by `_free_addr` below. + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| DNSLookup::process_get_addr_info(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - DNSLookup::process_get_addr_info(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-allocated AddrInfo freed by `_free_addr` below. - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - let mut array = super::cares_jsc::addr_info_to_js_array(&mut *addr, prev_global) + let head_global = (*head).global_this(); + let array = super::cares_jsc::addr_info_to_js_array(&mut *addr, head_global) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards // SAFETY: addr is the c-ares-allocated AddrInfo; freed once after all consumers run. // Move the raw pointer into the guard so the loop body can keep borrowing `*addr`. let _free_addr = scopeguard::guard(addr, |a| c_ares::AddrInfo::destroy(a)); - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::addr_info_to_js_array(&mut *addr, new_global) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::addr_info_to_js_array(&mut *addr, global) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + consume_head, + ); } } @@ -4460,7 +4416,7 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let mut array: JSValue = match super::options_jsc::result_any_to_js(result, global_object) + let array: JSValue = match super::options_jsc::result_any_to_js(result, global_object) .unwrap_or(None) { // TODO: properly propagate exception upwards @@ -4489,32 +4445,21 @@ impl Resolver { } }; // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. + // pending-cache slot; consumed via `heap::take` in `consume_head`. unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - - { - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - array.ensure_still_alive(); - } - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - pending = (*value.as_ptr()).next; - if !core::ptr::eq(prev_global, new_global) { - array = super::options_jsc::result_any_to_js(result, new_global) + let head = ptr::addr_of_mut!((*key.lookup).head); + drain_chain_ok( + head, + array, + (*head).global_this(), + |global| { + super::options_jsc::result_any_to_js(result, global) .unwrap_or(None) - .unwrap(); // TODO: properly propagate exception upwards - prev_global = new_global; - } - - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - array.ensure_still_alive(); - } + .unwrap() // TODO: properly propagate exception upwards + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + || drop(bun_core::heap::take(key.lookup)), + ); } } @@ -4530,56 +4475,39 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresReverse::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-owned hostent (freed by c-ares after the callback). + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| CAresReverse::process_resolve(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresReverse::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-owned hostent (freed by c-ares after the callback). - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); // The callback need not and should not attempt to free the memory // pointed to by hostent; the ares library will free it when the // callback returns. - let mut array = super::cares_jsc::hostent_to_js_response(&mut *addr, prev_global, b"") + let head_global = (*head).global_this(); + let array = super::cares_jsc::hostent_to_js_response(&mut *addr, head_global, b"") .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - array.ensure_still_alive(); - CAresReverse::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::hostent_to_js_response(&mut *addr, new_global, b"") - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresReverse::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::hostent_to_js_response(&mut *addr, global, b"") + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresReverse::on_complete(node, value), + consume_head, + ); } } @@ -4595,60 +4523,41 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(mut name_info) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresNameInfo::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, - ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresNameInfo::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. + // pending-cache slot; consumed via `heap::take` in `consume_head`. unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(mut name_info) = result else { + drain_chain_err( + head, + |node| CAresNameInfo::process_resolve(node, err, timeout, None), + consume_head, + ); + return; + }; - let mut array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, prev_global) + let head_global = (*head).global_this(); + let array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, head_global) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - array.ensure_still_alive(); - CAresNameInfo::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, new_global) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresNameInfo::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::nameinfo_to_js_response(&mut name_info, global) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresNameInfo::on_complete(node, value), + consume_head, + ); } } - pub fn get_or_put_into_resolve_pending_cache( + pub fn get_or_put_into_pending_cache( &self, - key: &R::PendingCacheKey, + key: &PendingCacheKey, field: PendingCacheField, ) -> LookupCacheHit { // Dispatch via `HasPendingCacheKey::pending_cache`; the body is @@ -4659,49 +4568,18 @@ impl Resolver { while let Some(index) = inflight_iter.next() { // SAFETY: `used` bit is set ⇒ slot was initialized. let entry = unsafe { &mut *cache.ptr_at(index) }; - if R::key_hash(entry) == R::key_hash(key) - && R::key_len(entry) == R::key_len(key) - && R::key_name(entry) == R::key_name(key) - { + if entry.hash == key.hash && entry.len == key.len && entry.name == key.name { return LookupCacheHit::Inflight(std::ptr::from_mut(entry)); } } - if let Some(new) = cache.get_init(R::key_new(key)) { + if let Some(new) = cache.get_init(key.unlinked()) { return LookupCacheHit::New(new.as_ptr()); } LookupCacheHit::Disabled } - pub fn get_or_put_into_pending_cache( - &self, - key: &get_addr_info_request::PendingCacheKey, - field: PendingCacheField, - ) -> CacheHit { - let cache = self.pending_host_cache(field); - let mut inflight_iter = cache.used.iter_set(); - - while let Some(index) = inflight_iter.next() { - // SAFETY: `used` bit is set ⇒ slot was initialized. - let entry = unsafe { &mut *cache.ptr_at(index) }; - if entry.hash == key.hash && entry.len == key.len && entry.name == key.name { - return CacheHit::Inflight(std::ptr::from_mut(entry)); - } - } - - if let Some(new) = cache.get_init(get_addr_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - }) { - return CacheHit::New(new.as_ptr()); - } - - CacheHit::Disabled - } - pub fn get_channel(&self) -> ChannelResult<'_> { if self.channel.get().is_none() { let opts = self.options.get(); @@ -5097,8 +4975,8 @@ impl Resolver { } }; - let key = get_host_by_addr_info_request::PendingCacheKey::init(ip); - let cache = self.get_or_put_into_resolve_pending_cache::( + let key = PendingCacheKey::::init(ip); + let cache = self.get_or_put_into_pending_cache::( &key, PendingCacheField::PendingAddrCacheCares, ); @@ -5390,10 +5268,9 @@ impl Resolver { let cache_field = T::CACHE_FIELD; // "pending_{TYPE_NAME}_cache_cares" - let key = resolve_info_request::PendingCacheKey::::init(name); + let key = PendingCacheKey::>::init(name); - let cache = - self.get_or_put_into_resolve_pending_cache::>(&key, cache_field); + let cache = self.get_or_put_into_pending_cache::>(&key, cache_field); if let LookupCacheHit::Inflight(inflight) = cache { // CAresLookup will have the name ownership let cares_lookup = CAresLookup::::init(Some(self.as_ctx_ptr()), global_this, name); @@ -5449,7 +5326,7 @@ impl Resolver { } }; - let key = get_addr_info_request::PendingCacheKey::init(query); + let key = PendingCacheKey::init_query(query); let cache = self.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheCares); @@ -5970,8 +5847,8 @@ impl Resolver { } let cache_name: Box<[u8]> = cache_name.into_boxed_slice(); - let key = get_name_info_request::PendingCacheKey::init(&cache_name); - let cache = resolver.get_or_put_into_resolve_pending_cache::( + let key = PendingCacheKey::::init(&cache_name); + let cache = resolver.get_or_put_into_pending_cache::( &key, PendingCacheField::PendingNameinfoCacheCares, ); diff --git a/src/runtime/dns_jsc/mod.rs b/src/runtime/dns_jsc/mod.rs index a843cb49fbb..4c0ea475f2c 100644 --- a/src/runtime/dns_jsc/mod.rs +++ b/src/runtime/dns_jsc/mod.rs @@ -25,11 +25,8 @@ pub mod options_jsc; // GetAddrInfo.Options ↔ JSValue // `Resolver`, and `dispatch.rs`'s `from_field_ptr!`/`owner_as!` casts now resolve // to the same allocation `dns_body::Resolver::init` produces. +pub use dns_body::get_addr_info_request; pub use dns_body::{ CacheConfig, CacheHit, GetAddrInfoAsyncCallback, GetAddrInfoRequest, GlobalData, InternalDNSRequest, Order, PendingCache, PendingCacheField, RecordType, Resolver, internal, }; -pub use dns_body::{ - get_addr_info_request, get_host_by_addr_info_request, get_name_info_request, - resolve_info_request, -}; diff --git a/src/runtime/ffi/host_fns.rs b/src/runtime/ffi/host_fns.rs deleted file mode 100644 index 01e5142af37..00000000000 --- a/src/runtime/ffi/host_fns.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! Bodies for `FFI::{open, close}` and `Function::{compile, -//! print_source_code, print_callback_source_code}` plus the -//! `generate_symbols` / `generate_symbol_for_function` helpers. -//! -//! The JSC-dependent paths are wired against the type identities declared in -//! `super` (`FFI`, `Function`, `ABIType`, `Step`, `Compiled`). -//! -//! TinyCC compile/relocate (`bun_tcc_sys::State` method-ful API) remains -//! gated; `Function::compile` therefore short-circuits with a `Step::Failed` -//! when the `tinycc` feature is off (which it always is until -//! `bun_tcc_sys::tcc` un-gates). The full TCC body is preserved in -//! `ffi_body.rs` (``) for reference. - -use std::ffi::c_void; -use std::io::Write as _; - -use bstr::BStr; - -use bun_collections::StringArrayHashMap; -use bun_core::{self, ZigString}; -use bun_jsc::{self as jsc, JSGlobalObject, JSPropertyIterator, JSValue, JsResult}; - -use super::{ABIType, Function}; - -unsafe extern "C" { - /// `JSValue::getOwn` — own-property lookup (no prototype-chain walk). - /// Declared locally while `bun_jsc::JSValue::get_own` (JSValue.rs) is gated. - fn JSC__JSValue__getOwn( - value: JSValue, - global: *const JSGlobalObject, - name: *const bun_core::String, - ) -> JSValue; -} - -/// Own-property lookup. Local thin -/// wrapper while `bun_jsc::JSValue::get_own` stays gated. -#[inline] -fn get_own(value: JSValue, global: &JSGlobalObject, key: &[u8]) -> JsResult> { - let key_str = bun_core::String::init(ZigString::init(key)); - // Open a top exception scope before the FFI call (the C++ side has a - // ThrowScope whose dtor sets `m_needExceptionCheck`); a post-hoc `has_exception()` - // would assert under `BUN_JSC_validateExceptionChecks=1`. - bun_jsc::top_scope!(scope, global); - // SAFETY: `global` is live; `key_str` borrows `key` for the call duration. - let v = unsafe { JSC__JSValue__getOwn(value, global, &raw const key_str) }; - scope.return_if_exception()?; - if v.is_empty() { Ok(None) } else { Ok(Some(v)) } -} - -// ══════════════════════════════════════════════════════════════════════════ -// Symbol-spec parsing — generate_symbols / generate_symbol_for_function -// ══════════════════════════════════════════════════════════════════════════ - -/// Parse one -/// `{ args, returns, threadsafe, ptr }` spec into a `Function`. -pub fn generate_symbol_for_function( - global: &JSGlobalObject, - value: JSValue, - function: &mut Function, -) -> JsResult> { - jsc::mark_binding!(); - - let mut abi_types: Vec = Vec::new(); - - if let Some(args) = get_own(value, global, b"args")? { - if args.is_empty_or_undefined_or_null() || !args.js_type().is_array() { - return Ok(Some(global.create_error_instance(format_args!( - "Expected an object with \"args\" as an array" - )))); - } - - let mut array = args.array_iterator(global)?; - abi_types.reserve_exact(array.len as usize); - while let Some(val) = array.next()? { - if val.is_empty_or_undefined_or_null() { - return Ok(Some(global.create_error_instance(format_args!( - "param must be a string (type name) or number" - )))); - } - - if val.is_any_int() { - let int = val.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. - if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { - abi_types.push(t); - continue; - } else { - return Ok(Some( - global.create_error_instance(format_args!("invalid ABI type")), - )); - } - } - - if !val.js_type().is_string_like() { - return Ok(Some(global.create_error_instance(format_args!( - "param must be a string (type name) or number" - )))); - } - - let type_name = val.to_slice(global)?; - let Some(abi) = ABIType::LABEL.get(type_name.slice()).copied() else { - return Ok(Some(global.create_type_error_instance(format_args!( - "Unknown type {}", - BStr::new(type_name.slice()) - )))); - }; - abi_types.push(abi); - } - } - - let mut return_type = ABIType::Void; - let mut threadsafe = false; - - if let Some(threadsafe_value) = value.get_truthy(global, b"threadsafe")? { - threadsafe = threadsafe_value.to_boolean(); - } - - 'brk: { - if let Some(ret_value) = value.get_truthy(global, b"returns")? { - if ret_value.is_any_int() { - let int = ret_value.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. - if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { - return_type = t; - break 'brk; - } else { - return Ok(Some( - global.create_error_instance(format_args!("invalid ABI type")), - )); - } - } - - let ret_slice = ret_value.to_slice(global)?; - return_type = match ABIType::LABEL.get(ret_slice.slice()).copied() { - Some(t) => t, - None => { - return Ok(Some(global.create_type_error_instance(format_args!( - "Unknown return type {}", - BStr::new(ret_slice.slice()) - )))); - } - }; - } - } - - if return_type == ABIType::NapiEnv { - return Ok(Some(global.create_error_instance(format_args!( - "Cannot return napi_env to JavaScript" - )))); - } - - if return_type == ABIType::Buffer { - return Ok(Some(global.create_error_instance(format_args!( - "Cannot return a buffer to JavaScript (since byteLength and byteOffset are unknown)" - )))); - } - - if function.threadsafe && return_type != ABIType::Void { - return Ok(Some(global.create_error_instance(format_args!( - "Threadsafe functions must return void" - )))); - } - - // `Function` has a `Drop` impl, so functional-record-update - // (`..Default::default()`) is rejected (E0509). Reset to default and assign - // the parsed fields individually instead. - *function = Function::default(); - function.arg_types = abi_types; - function.return_type = return_type; - function.threadsafe = threadsafe; - - if let Some(ptr) = value.get(global, b"ptr")? { - if ptr.is_number() { - let num = ptr.as_ptr_address(); - if num > 0 { - function.symbol_from_dynamic_library = Some(num as *mut c_void); - } - } else if ptr.is_heap_big_int() { - let num = ptr.to_uint64_no_truncate() as usize; - if num > 0 { - function.symbol_from_dynamic_library = Some(num as *mut c_void); - } - } - } - - Ok(None) -} - -/// Iterate own-properties of `object`, -/// parsing each value as a `Function` spec. -pub fn generate_symbols( - global: &JSGlobalObject, - symbols: &mut StringArrayHashMap, - object: impl jsc::IntoIterObject, -) -> JsResult> { - jsc::mark_binding!(); - - // skip_empty_name = true, include_value = true, own_only = true - let mut symbols_iter = JSPropertyIterator::init( - global, - object, - jsc::JSPropertyIteratorOptions { - skip_empty_name: true, - include_value: true, - own_properties_only: true, - ..Default::default() - }, - )?; - - symbols.reserve(symbols_iter.len); - - while let Some(prop) = symbols_iter.next()? { - let value = symbols_iter.value; - - if value.is_empty_or_undefined_or_null() || !value.is_object() { - return Ok(Some(global.create_type_error_instance(format_args!( - "Expected an object for key \"{}\"", - prop - )))); - } - - let mut function = Function::default(); - if let Some(val) = generate_symbol_for_function(global, value, &mut function)? { - return Ok(Some(val)); - } - let base_name = prop.to_owned_slice_z(); - let key = base_name.as_bytes().to_vec().into_boxed_slice(); - function.base_name = Some(base_name); - - symbols.insert(&key, function); - } - - Ok(None) -} - -// ══════════════════════════════════════════════════════════════════════════ -// Function — compile + C-source emission -// ══════════════════════════════════════════════════════════════════════════ - -impl Function { - /// Emit the C trampoline that - /// adapts a JSC host-call frame to the native symbol's ABI. - pub fn print_source_code( - &self, - writer: &mut impl std::io::Write, - ) -> Result<(), bun_core::Error> { - if !self.arg_types.is_empty() { - writer.write_all(b"#define HAS_ARGUMENTS\n")?; - } - - 'brk: { - if self.return_type.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break 'brk; - } - for arg in self.arg_types.iter() { - // conditionally include math.h - if arg.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break; - } - } - } - - writer.write_all(Self::ffi_header())?; - - // -- Generate the FFI function symbol - writer.write_all(b"/* --- The Function To Call */\n")?; - self.return_type.typename(writer)?; - writer.write_all(b" ")?; - writer.write_all(self.base_name.as_ref().map(|b| b.as_bytes()).unwrap_or(b""))?; - writer.write_all(b"(")?; - let mut first = true; - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - arg.param_typename(writer)?; - write!(writer, " arg{}", i)?; - } - writer.write_all( - b");\n\ - \n\ - /* ---- Your Wrapper Function ---- */\n\ - ZIG_REPR_TYPE JSFunctionCall(void* JS_GLOBAL_OBJECT, void* callFrame) {\n", - )?; - - if self.needs_handle_scope() { - writer.write_all( - b" void* handleScope = NapiHandleScope__open(&Bun__thisFFIModuleNapiEnv, false);\n", - )?; - } - - if !self.arg_types.is_empty() { - writer.write_all(b" LOAD_ARGUMENTS_FROM_CALL_FRAME;\n")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if *arg == ABIType::NapiEnv { - write!( - writer, - " napi_env arg{} = (napi_env)&Bun__thisFFIModuleNapiEnv;\n argsPtr++;\n", - i - )?; - } else if *arg == ABIType::NapiValue { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else if arg.needs_a_cast_in_c() { - if i < self.arg_types.len() - 1 { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else { - write!( - writer, - " EncodedJSValue arg{};\n arg{}.asInt64 = *argsPtr;\n", - i, i - )?; - } - } else if i < self.arg_types.len() - 1 { - writeln!(writer, " int64_t arg{} = *argsPtr++;", i)?; - } else { - writeln!(writer, " int64_t arg{} = *argsPtr;", i)?; - } - } - } - - let mut arg_buf = [0u8; 32]; - - writer.write_all(b" ")?; - if self.return_type != ABIType::Void { - self.return_type.typename(writer)?; - writer.write_all(b" return_value = ")?; - } - write!( - writer, - "{}(", - BStr::new(self.base_name.as_ref().map(|b| b.as_bytes()).unwrap_or(b"")) - )?; - first = true; - arg_buf[0..3].copy_from_slice(b"arg"); - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - writer.write_all(b" ")?; - - let length_buf = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + length_buf]; - if arg.needs_a_cast_in_c() { - write!(writer, "{}", arg.to_c(arg_name))?; - } else { - writer.write_all(arg_name)?; - } - } - writer.write_all(b");\n")?; - - if !first { - writer.write_all(b"\n")?; - } - - writer.write_all(b" ")?; - - if self.needs_handle_scope() { - writer.write_all( - b" NapiHandleScope__close(&Bun__thisFFIModuleNapiEnv, handleScope);\n", - )?; - } - - writer.write_all(b"return ")?; - - if self.return_type != ABIType::Void { - write!( - writer, - "{}.asZigRepr", - self.return_type.to_js(b"return_value") - )?; - } else { - writer.write_all(b"ValueUndefined.asZigRepr")?; - } - - writer.write_all(b";\n}\n\n")?; - Ok(()) - } - - /// Emit the C - /// trampoline that adapts a native call into a JSC `FFI_Callback_call`. - pub fn print_callback_source_code( - &self, - global_object: Option<&JSGlobalObject>, - context_ptr: Option<*mut c_void>, - writer: &mut impl std::io::Write, - ) -> Result<(), bun_core::Error> { - { - let ptr = global_object - .map(|g| std::ptr::from_ref(g) as usize) - .unwrap_or(0); - writeln!(writer, "#define JS_GLOBAL_OBJECT (void*)0x{:X}ULL", ptr)?; - } - - writer.write_all(b"#define IS_CALLBACK 1\n")?; - - 'brk: { - if self.return_type.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break 'brk; - } - for arg in self.arg_types.iter() { - if arg.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break; - } - } - } - - writer.write_all(Self::ffi_header())?; - - // -- Generate the FFI function symbol - writer.write_all(b"\n \n/* --- The Callback Function */\n")?; - let mut first = true; - self.return_type.typename(writer)?; - - writer.write_all(b" my_callback_function")?; - writer.write_all(b"(")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - arg.typename(writer)?; - write!(writer, " arg{}", i)?; - } - writer.write_all(b") {\n")?; - - if cfg!(debug_assertions) { - writer.write_all(b"#ifdef INJECT_BEFORE\n")?; - writer.write_all(b"INJECT_BEFORE;\n")?; - writer.write_all(b"#endif\n")?; - } - - first = true; - let _ = first; - - if !self.arg_types.is_empty() { - let mut arg_buf = [0u8; 32]; - writeln!( - writer, - " ZIG_REPR_TYPE arguments[{}];", - self.arg_types.len() - )?; - - arg_buf[0..3].copy_from_slice(b"arg"); - for (i, arg) in self.arg_types.iter().enumerate() { - let printed = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + printed]; - writeln!( - writer, - "arguments[{}] = {}.asZigRepr;", - i, - arg.to_js(arg_name) - )?; - } - } - - writer.write_all(b" ")?; - let mut inner_buf_ = [0u8; 372]; - - let written = { - let ptr = context_ptr.map(|p| p as usize).unwrap_or(0); - let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]); - if !self.arg_types.is_empty() { - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{:X}ULL, {}, arguments)", - ptr, - self.arg_types.len() - )?; - } else { - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{:X}ULL, 0, (ZIG_REPR_TYPE*)0)", - ptr - )?; - } - cursor.position() as usize - }; - - if self.return_type == ABIType::Void { - writer.write_all(&inner_buf_[1..1 + written])?; - } else { - inner_buf_[0] = b'_'; - let inner_buf = &inner_buf_[0..1 + written]; - write!(writer, "return {}", self.return_type.to_c_exact(inner_buf))?; - } - - writer.write_all(b";\n}\n\n")?; - Ok(()) - } -} diff --git a/src/runtime/ffi/mod.rs b/src/runtime/ffi/mod.rs index 1e5800bf823..debf00cc75e 100644 --- a/src/runtime/ffi/mod.rs +++ b/src/runtime/ffi/mod.rs @@ -1,21 +1,11 @@ //! `Bun.FFI` / `bun:ffi`. //! -//! `ABIType` (CType) enum, `FFI`/`Function`/`Step`/`Compiled` structs, -//! formatters, dlopen data path, and the JSC host-fn entry points -//! (`open`/`close`/`compile`/`generate_symbols`) are real. The full TinyCC -//! compile bodies (`CompileC`, `Function::compile`, `cc`/`linkSymbols`/ -//! `callback`) live in `ffi_body` on top of `bun_tcc_sys::State`. - -use core::ffi::{c_char, c_void}; -use core::ptr::NonNull; - -use bun_core::ZBox; - -use crate::jsc::JSGlobalObject; - -// ─── un-gated host-fn bodies (open/close/compile/generate_symbols) ─────────── -mod host_fns; -pub use host_fns::{generate_symbol_for_function, generate_symbols}; +//! `ABIType` (CType) enum and formatters live in `abi_type`; everything else — +//! `FFI`/`Function`/`Step`/`Compiled` structs, dlopen data path, the JSC +//! host-fn entry points (`open`/`close`/`compile`/`generate_symbols`), and the +//! full TinyCC compile bodies (`CompileC`, `Function::compile`, +//! `cc`/`linkSymbols`/`callback`) — lives in `ffi_body` on top of +//! `bun_tcc_sys::State`. // ─── implementation modules ────────────────────────────────────────────────── @@ -104,11 +94,6 @@ mod dom_call_slowpath { } } -// `bun_tcc_sys` provides the method-ful `State` (compile_string/relocate/ -// add_symbol/…) with per-target link stubs where TinyCC isn't built — see -// `tcc_externs!` in `src/tcc_sys/tcc.rs`. -use bun_tcc_sys as TCC; - /// Get the last dynamic-library loading error message in a cross-platform way. /// On POSIX systems, this calls `dlerror()`. /// On Windows, this uses `GetLastError()` and formats the error code. @@ -150,124 +135,6 @@ pub(crate) fn get_dl_error() -> Box<[u8]> { pub use ffi_body::FFI; -// The full `CompileC`/`Source`/`SymbolsMap`/`StringArray`/`CompilerRT` port -// lives in `ffi_body`; the draft duplicates that used to sit here were unused -// and have been removed. - -// ─── Function ──────────────────────────────────────────────────────────────── - -pub struct Function { - pub symbol_from_dynamic_library: Option<*mut c_void>, - pub base_name: Option, - pub state: Option>, - - pub return_type: ABIType, - pub arg_types: Vec, - pub step: Step, - pub threadsafe: bool, - // allocator field dropped — global mimalloc -} - -impl Default for Function { - fn default() -> Self { - Self { - symbol_from_dynamic_library: None, - base_name: None, - state: None, - return_type: ABIType::Void, - arg_types: Vec::new(), - step: Step::Pending, - threadsafe: false, - } - } -} - -// PORTING.md §Global mutable state: written once at startup with the -// resolved tinycc lib dir; read by the FFI compile path. RacyCell over the -// raw C-string pointer (no concurrent writers). -pub static LIB_DIR_Z: bun_core::RacyCell<*const c_char> = bun_core::RacyCell::new(c"".as_ptr()); - -unsafe extern "C" { - fn FFICallbackFunctionWrapper_destroy(_: *mut c_void); -} - -impl Drop for Function { - fn drop(&mut self) { - // base_name, arg_types, Step::Failed.msg are owned and freed by drop glue. - if let Some(state) = self.state.take() { - // SAFETY: `state` is the live TCCState* allocated for this Function's - // trampoline; ownership is unique here (taken from self). - unsafe { TCC::State::destroy(state.as_ptr()) }; - } - if let Step::Compiled(compiled) = &mut self.step { - if let Some(wrapper) = compiled.ffi_callback_function_wrapper.take() { - // SAFETY: wrapper was created by Bun__createFFICallbackFunction - unsafe { FFICallbackFunctionWrapper_destroy(wrapper.as_ptr()) }; - } - } - } -} - -impl Function { - pub fn needs_handle_scope(&self) -> bool { - for arg in self.arg_types.iter() { - if *arg == ABIType::NapiEnv || *arg == ABIType::NapiValue { - return true; - } - } - self.return_type == ABIType::NapiValue - } - - pub fn needs_napi_env(&self) -> bool { - for arg in self.arg_types.iter() { - if *arg == ABIType::NapiEnv || *arg == ABIType::NapiValue { - return true; - } - } - false - } - - pub fn ffi_header() -> &'static [u8] { - // Embedded under - // `codegen_embed`, reloaded from disk otherwise (dev fast iteration). - bun_core::runtime_embed_file!(Src, "runtime/ffi/FFI.h").as_bytes() - } -} - -// ─── Step ──────────────────────────────────────────────────────────────────── - -pub enum Step { - Pending, - Compiled(Compiled), - Failed { msg: Box<[u8]>, allocated: bool }, -} - -/// Draft-path sibling of `ffi_body::Compiled`; see it for JS function rooting. -pub struct Compiled { - pub ptr: *mut c_void, - pub js_context: Option<*mut JSGlobalObject>, - pub ffi_callback_function_wrapper: Option>, -} - -impl Default for Compiled { - fn default() -> Self { - Self { - ptr: core::ptr::null_mut(), - js_context: None, - ffi_callback_function_wrapper: None, - } - } -} - -impl Step { - pub fn compiled_ptr(&self) -> *mut c_void { - match self { - Step::Compiled(c) => c.ptr, - _ => core::ptr::null_mut(), - } - } -} - // ═════════════════════════════════════════════════════════════════════════════ // ABIType — single source of truth lives in abi_type.rs // ═════════════════════════════════════════════════════════════════════════════ diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index e7eac1d2afa..2e6f4c769f8 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -843,15 +843,22 @@ unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) } } -/// `eventLoop().autoTick()`. Needs -/// `timer::All` for the poll-timeout calculation, hence dispatched here. +/// `eventLoop().autoTick()` (`ACTIVE = false`) and `eventLoop().autoTickActive()` +/// (`ACTIVE = true`). Needs `timer::All` for the poll-timeout calculation, +/// hence dispatched here. +/// +/// The active variant skips `runImminentGCTimer` and the +/// `handleRejectedPromises` tails; it is used by `bun_main` / `on_before_exit` +/// drain loops where blocking when the loop is idle would hang shutdown. +/// `ACTIVE` is const-generic so both variants monomorphize with no runtime +/// branch on this hot path. /// /// PERF: the one fn-ptr indirection is dwarfed by the kqueue/epoll syscall it /// gates. /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn auto_tick(vm: *mut VirtualMachine) { +unsafe fn auto_tick(vm: *mut VirtualMachine) { // Note: reshaped for borrowck — `EventLoop` is a value field of // `VirtualMachine`, so holding `&mut EventLoop` while also touching VM // siblings would alias. Dereference per-field via the raw `vm` ptr. @@ -903,8 +910,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { .update_date_header_timer_if_necessary(&*loop_, vm) }; } - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).run_imminent_gc_timer() }; + if !ACTIVE { + // SAFETY: `el` is the live per-thread event loop. + unsafe { (*el).run_imminent_gc_timer() }; + } // ── poll the I/O loop with the next-timer deadline ────────────────── if state.is_null() { @@ -917,8 +926,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // Still run the post-poll hooks. // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; - // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. - unsafe { (*(*vm).global).handle_rejected_promises() }; + if !ACTIVE { + // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. + unsafe { (*(*vm).global).handle_rejected_promises() }; + } return; } @@ -993,115 +1004,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; - // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. - unsafe { (*(*vm).global).handle_rejected_promises() }; -} - -/// `eventLoop().autoTickActive()`. Same shape as -/// [`auto_tick`] but: no `runImminentGCTimer`, no `handleRejectedPromises` at -/// the tail, and no debug sleep-timer logging. Used by `bun_main` / -/// `on_before_exit` drain loops where blocking when the loop is idle would -/// hang shutdown. -/// -/// # Safety -/// `vm` is the live per-thread VM. -unsafe fn auto_tick_active(vm: *mut VirtualMachine) { - // Note: reshaped for borrowck — see `auto_tick` above. - // SAFETY: per fn contract — `vm` is the live per-thread VM. - let el: *mut bun_jsc::event_loop::EventLoop = unsafe { &*vm }.event_loop; - // SAFETY: `el` is the live per-thread event loop (field of `*vm`). - let loop_ = unsafe { (*el).usockets_loop() }; - - // SAFETY: `el` is the live per-thread event loop; `vm` per fn contract. - unsafe { (*el).tick_immediate_tasks(vm) }; - #[cfg(windows)] - if !unsafe { &*el }.immediate_tasks.is_empty() { - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).wakeup() }; - } - - #[cfg(unix)] - { - // SAFETY: per fn contract. `swap(0)` so a concurrent - // `increment_pending_unref_counter()` (cross-thread, see - // `KeepAlive::unref_on_next_tick_concurrently`) can't be lost between - // the read and the reset. - let pending_unref = unsafe { &*vm } - .pending_unref_counter - .swap(0, core::sync::atomic::Ordering::Relaxed); - if pending_unref > 0 { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).unref_count(pending_unref) }; - } - } - - let state = runtime_state(); - if !state.is_null() { - // SAFETY: see the matching call in `auto_tick` above. - unsafe { - (*state) - .timer - .update_date_header_timer_if_necessary(&*loop_, vm) - }; - } - - if state.is_null() { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).tick_without_idle() }; - // SAFETY: per fn contract. - unsafe { (*vm).on_after_event_loop() }; - return; - } - - { - // SAFETY: `el` is the live per-thread event loop. - let has_pending_immediate = !unsafe { &*el }.immediate_tasks.is_empty(); - // SAFETY: `loop_` is the live per-thread uws loop. - let quic_next_tick_us = unsafe { - let ild = &(*loop_).internal_loop_data; - if ild.quic_head.is_null() { - None - } else { - Some(ild.quic_next_tick_us) - } - }; - let mut timespec = bun_core::Timespec { sec: 0, nsec: 0 }; - // SAFETY: `loop_` is the live per-thread uws loop. - if unsafe { (*loop_).is_active() } { - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).process_gc_timer() }; - // SAFETY: `state` is the live per-thread `RuntimeState`; see - // Note on `auto_tick` re: aliased-&mut across `fire()`. - let have_timeout = unsafe { - timer::All::get_timeout( - &mut (*state).timer, - &mut timespec, - has_pending_immediate, - quic_next_tick_us, - vm.cast(), - ) - }; - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { - (*loop_).tick_with_timeout(if have_timeout { Some(×pec) } else { None }) - }; - } else { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).tick_without_idle() }; - } - } - - #[cfg(unix)] - { - // SAFETY: `state` is the live per-thread `RuntimeState`; see Note - // on `auto_tick` re: aliased-&mut across `fire()`. - unsafe { timer::All::drain_timers(&mut (*state).timer, vm.cast()) }; + if !ACTIVE { + // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. + unsafe { (*(*vm).global).handle_rejected_promises() }; } - #[cfg(not(unix))] - let _ = state; - - // SAFETY: per fn contract. - unsafe { (*vm).on_after_event_loop() }; } /// `printException` / `printErrorlikeObject` — formats `value` to stderr via @@ -1452,8 +1358,8 @@ pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks { generate_entry_point, load_preloads, ensure_debugger, - auto_tick, - auto_tick_active, + auto_tick: auto_tick::, + auto_tick_active: auto_tick::, print_exception, timer_insert, timer_remove, diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 600d9dcfac1..4a58d2aad5d 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -641,6 +641,78 @@ pub(super) extern "C" fn napi_create_int64( env.ok() } +/// Code-unit type accepted by the `napi_create_string_*` entry points; selects +/// how a `NAPI_AUTO_LENGTH` (NUL-terminated) input is measured. +trait NapiStringUnit: Copy { + /// # Safety + /// `ptr` must be non-null and point to a NUL-terminated sequence. + unsafe fn cstr_units<'a>(ptr: *const Self) -> &'a [Self]; +} + +impl NapiStringUnit for u8 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u8) -> &'a [u8] { + // SAFETY: forwarded caller contract. + unsafe { bun_core::ffi::cstr(ptr.cast::()) }.to_bytes() + } +} + +impl NapiStringUnit for u16 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u16) -> &'a [u16] { + // SAFETY: forwarded caller contract. Scans to the NUL u16 terminator. + unsafe { bun_core::ffi::wstr_units(ptr) } + } +} + +/// Shared argument-validation prologue for the `napi_create_string_*` entry +/// points: extracts the source code units, or `Err(())` when the arguments +/// are invalid (caller returns `env.invalid_arg()`). +/// +/// # Safety +/// When `str_` is non-null, the NAPI caller contract must hold: if `length == +/// NAPI_AUTO_LENGTH`, `str_` points to a NUL-terminated sequence; otherwise +/// `[str_, str_ + length)` must be readable. The returned borrow has an +/// unconstrained lifetime and must not outlive the caller's buffer. +#[inline(always)] +unsafe fn napi_string_slice<'a, T: NapiStringUnit>( + str_: *const T, + length: usize, +) -> Result<&'a [T], ()> { + if !str_.is_null() { + if NAPI_AUTO_LENGTH == length { + // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. + Ok(unsafe { T::cstr_units(str_) }) + } else if length > i32::MAX as usize { + Err(()) + } else { + // SAFETY: caller guarantees [ptr, ptr+length) is valid. + Ok(unsafe { bun_core::ffi::slice(str_, length) }) + } + } else if length == 0 { + Ok(&[]) + } else { + Err(()) + } +} + +/// Writes a converted string's `to_js` result through the out-param, mapping +/// conversion failure to `generic_failure`. +#[inline(always)] +fn set_string_result( + env: &NapiEnv, + result: &mut napi_value, + js: jsc::JsResult, +) -> napi_status { + match js { + Ok(v) => { + result.set(env, v); + env.ok() + } + Err(_) => NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), + } +} + #[unsafe(no_mangle)] pub(super) extern "C" fn napi_create_string_latin1( env_: napi_env, @@ -651,24 +723,11 @@ pub(super) extern "C" fn napi_create_string_latin1( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!( @@ -678,24 +737,14 @@ pub(super) extern "C" fn napi_create_string_latin1( ); if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (string, bytes) = bun_core::String::create_uninitialized_latin1(slice.len()); // `string` derefs on Drop. bytes.copy_from_slice(slice); - let js = match string.to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - env.ok() + set_string_result(env, result, string.to_js(env.to_js())) } #[unsafe(no_mangle)] @@ -708,24 +757,11 @@ pub(super) extern "C" fn napi_create_string_utf8( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!(napi, "napi_create_string_utf8: {}", bstr::BStr::new(slice)); @@ -749,25 +785,11 @@ pub(super) extern "C" fn napi_create_string_utf16( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u16] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - // Scan to the NUL u16 terminator. - break 'brk unsafe { bun_core::ffi::wstr_units(str_) }; - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; if cfg!(debug_assertions) { @@ -780,23 +802,13 @@ pub(super) extern "C" fn napi_create_string_utf16( } if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (mut string, chars) = bun_core::String::create_uninitialized_utf16(slice.len()); chars.copy_from_slice(slice); - let js = match string.transfer_to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - env.ok() + set_string_result(env, result, string.transfer_to_js(env.to_js())) } // Implemented in C++ (napi.cpp); declared extern here for Rust-side callers. diff --git a/src/runtime/server/ServerWebSocket.rs b/src/runtime/server/ServerWebSocket.rs index 5fb0c0e297b..87d832fb7aa 100644 --- a/src/runtime/server/ServerWebSocket.rs +++ b/src/runtime/server/ServerWebSocket.rs @@ -221,11 +221,9 @@ impl ServerWebSocket { // guard on `compress` even when compress is args[2] (long-standing // user-visible behavior; do not "fix"). // - // A unified `publish_prologue` covering the full callframe header was - // considered and rejected: publishText omits the empty-topic check and - // reuses "publish" in its min-args message (both user-visible), so a single - // prologue would either change user-visible errors or carry per-caller - // bool flags — net more code than three small orthogonal helpers. + // `publish_prologue` parameterizes the two per-method divergences: + // publishText omits the empty-topic check and reuses "publish" in its + // min-args message and debug logs (both user-visible; do not "fix"). // ────────────────────────────────────────────────────────────────────── /// `(app, ssl, publish_to_self)` from the handler, or `None` when the @@ -264,6 +262,60 @@ impl ServerWebSocket { Ok(args_len > 1 && compress_value.to_boolean()) } + /// Shared prologue for `publish`/`publishText`/`publishBinary`: min-arity + /// check, closed-server check, topic validation, and compress parsing. + /// `Ok(None)` means the server is closed (caller returns `0`). + /// + /// `fn_name` is the method name used in error messages; `log_name` is the + /// name used in debug logs and the min-args message (`publishText` reports + /// "publish" there) and `require_non_empty_topic` is `false` only for + /// `publishText` — both long-standing user-visible behavior; do not "fix". + #[inline] + fn publish_prologue( + &self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + fn_name: &'static str, + log_name: &'static str, + require_non_empty_topic: bool, + ) -> JsResult> { + let args = callframe.arguments_old::<4>(); + if args.len < 1 { + bun_output::scoped_log!(WebSocketServer, "{}()", log_name); + return Err(global_this.throw(format_args!("{log_name} requires at least 1 argument"))); + } + + let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { + bun_output::scoped_log!(WebSocketServer, "publish() closed"); + return Ok(None); + }; + + let topic_value = args.ptr[0]; + let message_value = args.ptr[1]; + let compress_value = args.ptr[2]; + + if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { + bun_output::scoped_log!(WebSocketServer, "{}() topic invalid", log_name); + return Err(global_this.throw(format_args!("{fn_name} requires a topic string"))); + } + + let topic_slice = topic_value.to_slice(global_this)?; + if require_non_empty_topic && topic_slice.slice().is_empty() { + return Err(global_this.throw(format_args!("{fn_name} requires a non-empty topic"))); + } + + let compress = Self::parse_compress_arg(global_this, fn_name, compress_value, args.len)?; + + Ok(Some(( + app, + ssl, + publish_to_self, + topic_slice, + message_value, + compress, + ))) + } + /// Route a publish through either the per-socket uWS handle (when /// `!publish_to_self && !closed`) or the app-wide broadcast, then map the /// bool result to the JS number contract: success → `len & 0x7FFF_FFFF`, @@ -764,33 +816,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publish", "publish", true)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publish requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publish requires a non-empty topic"))); - } - - let compress = Self::parse_compress_arg(global_this, "publish", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() { return Err(global_this.throw(format_args!("publish requires a non-empty message"))); } @@ -833,32 +864,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publishText", "publish", false)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publishText requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - - let compress = - Self::parse_compress_arg(global_this, "publishText", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() || !message_value.is_string() { return Err(global_this.throw(format_args!("publishText requires a non-empty message"))); } @@ -886,35 +897,17 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publishBinary()"); - return Err( - global_this.throw(format_args!("publishBinary requires at least 1 argument")) - ); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = self + .publish_prologue( + global_this, + callframe, + "publishBinary", + "publishBinary", + true, + )? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publishBinary() topic invalid"); - return Err(global_this.throw(format_args!("publishBinary requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publishBinary requires a non-empty topic"))); - } - - let compress = - Self::parse_compress_arg(global_this, "publishBinary", compress_value, args.len)?; if message_value.is_empty_or_undefined_or_null() { return Err( diff --git a/src/runtime/shell/builtin/basename.rs b/src/runtime/shell/builtin/basename.rs index 221c95717cd..6a0983c1baa 100644 --- a/src/runtime/shell/builtin/basename.rs +++ b/src/runtime/shell/builtin/basename.rs @@ -3,10 +3,17 @@ use crate::shell::interpreter::{Interpreter, NodeId}; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; +/// One-argument-per-line path transform shared by `basename` and `dirname`. +pub trait PathTransform: Default { + const KIND: Kind; + fn apply(path: &[u8]) -> &[u8]; +} + #[derive(Default)] -pub struct Basename { +pub struct PathBuiltin { state: State, buf: Vec, + _transform: std::marker::PhantomData, } #[derive(Default)] @@ -17,17 +24,32 @@ enum State { Done, } -impl Basename { - pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield { +#[derive(Default)] +pub struct BasenameTransform; + +impl PathTransform for BasenameTransform { + const KIND: Kind = Kind::Basename; + fn apply(path: &[u8]) -> &[u8] { + bun_paths::resolve_path::basename(path) + } +} + +pub type Basename = PathBuiltin; + +impl PathBuiltin { + pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield + where + Self: BuiltinState, + { let buf = { let bltn = Builtin::of(interp, cmd); let argc = bltn.args_slice().len(); if argc == 0 { - return Self::fail(interp, cmd, Kind::Basename.usage_string()); + return Self::fail(interp, cmd, T::KIND.usage_string()); } let mut buf = Vec::new(); for i in 0..argc { - buf.extend_from_slice(bun_paths::resolve_path::basename(bltn.arg_bytes(i))); + buf.extend_from_slice(T::apply(bltn.arg_bytes(i))); buf.push(b'\n'); } buf @@ -46,7 +68,10 @@ impl Basename { Builtin::done(interp, cmd, 0) } - fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield { + fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield + where + Self: BuiltinState, + { Self::state_mut(interp, cmd).state = State::Err; Builtin::write_failing_error(interp, cmd, msg, 1) } @@ -56,7 +81,10 @@ impl Basename { cmd: NodeId, _: usize, err: Option, - ) -> Yield { + ) -> Yield + where + Self: BuiltinState, + { if let Some(e) = err { e.deref(); Self::state_mut(interp, cmd).state = State::Err; @@ -65,7 +93,7 @@ impl Basename { match Self::state_mut(interp, cmd).state { State::Done => Builtin::done(interp, cmd, 0), State::Err => Builtin::done(interp, cmd, 1), - State::Idle => unreachable!("Basename.onIOWriterChunk: idle"), + State::Idle => unreachable!("{}.onIOWriterChunk: idle", T::KIND.as_str()), } } } diff --git a/src/runtime/shell/builtin/cp.rs b/src/runtime/shell/builtin/cp.rs index 854e2bb415f..9cbd5509701 100644 --- a/src/runtime/shell/builtin/cp.rs +++ b/src/runtime/shell/builtin/cp.rs @@ -3,7 +3,7 @@ use bun_paths::resolve_path; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -193,23 +193,6 @@ impl Cp { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - if let Some(task) = Self::state_mut(interp, cmd).output_queue.pop_front() { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// Windows-only post-processing of tasks that failed with EBUSY: if some /// other task already succeeded /// for the same absolute src/tgt, the EBUSY is benign and the task is @@ -320,67 +303,7 @@ impl Cp { } } -impl OutputTaskVTable for Cp { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - Self::state_mut(interp, cmd).output_queue.push_back(child); - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - Self::state_mut(interp, cmd).output_queue.push_back(child); - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Cp, queue_on_self); /// Resolves src/tgt to absolute paths, decides /// which POSIX `cp` synopsis applies, then hands off to the node:fs async cp diff --git a/src/runtime/shell/builtin/dirname.rs b/src/runtime/shell/builtin/dirname.rs index 4edfdb2db59..af1660702b1 100644 --- a/src/runtime/shell/builtin/dirname.rs +++ b/src/runtime/shell/builtin/dirname.rs @@ -1,74 +1,15 @@ -use crate::shell::builtin::{Builtin, BuiltinState, IoKind}; -use crate::shell::interpreter::{Interpreter, NodeId}; -use crate::shell::io_writer::{ChildPtr, WriterTag}; -use crate::shell::yield_::Yield; +use crate::shell::builtin::Kind; +use crate::shell::builtins::basename::{PathBuiltin, PathTransform}; #[derive(Default)] -pub struct Dirname { - state: State, - buf: Vec, -} - -#[derive(Default)] -enum State { - #[default] - Idle, - Err, - Done, -} - -impl Dirname { - pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield { - let bltn = Builtin::of(interp, cmd); - let argc = bltn.args_slice().len(); - if argc == 0 { - return Self::fail(interp, cmd, b"usage: dirname string\n"); - } - - let stdout_needs_io = bltn.stdout.needs_io(); - let mut buf = Vec::new(); - for i in 0..argc { - let path = bltn.arg_bytes(i); - let dir = bun_paths::resolve_path::dirname::(path); - let dir: &[u8] = if dir.is_empty() { b"." } else { dir }; - buf.extend_from_slice(dir); - buf.push(b'\n'); - } +pub struct DirnameTransform; - Self::state_mut(interp, cmd).state = State::Done; - if let Some(safeguard) = stdout_needs_io { - Self::state_mut(interp, cmd).buf = buf; - let owned = Self::state_mut(interp, cmd).buf.clone(); - let child = ChildPtr::new(cmd, WriterTag::Builtin); - return Builtin::of_mut(interp, cmd) - .stdout - .enqueue(child, &owned, safeguard); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - Builtin::done(interp, cmd, 0) - } - - fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield { - Self::state_mut(interp, cmd).state = State::Err; - Builtin::write_failing_error(interp, cmd, msg, 1) - } - - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - _: usize, - err: Option, - ) -> Yield { - if let Some(e) = err { - e.deref(); - Self::state_mut(interp, cmd).state = State::Err; - return Builtin::done(interp, cmd, 1); - } - let exit = match Self::state_mut(interp, cmd).state { - State::Done => 0, - State::Err => 1, - State::Idle => unreachable!("Dirname.onIOWriterChunk: idle"), - }; - Builtin::done(interp, cmd, exit) +impl PathTransform for DirnameTransform { + const KIND: Kind = Kind::Dirname; + fn apply(path: &[u8]) -> &[u8] { + let dir = bun_paths::resolve_path::dirname::(path); + if dir.is_empty() { b"." } else { dir } } } + +pub type Dirname = PathBuiltin; diff --git a/src/runtime/shell/builtin/ls.rs b/src/runtime/shell/builtin/ls.rs index 978adf9b9d8..53af6c31222 100644 --- a/src/runtime/shell/builtin/ls.rs +++ b/src/runtime/shell/builtin/ls.rs @@ -9,7 +9,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, ShellTask, - shell_openat, + impl_output_task_vtable, shell_openat, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -184,28 +184,6 @@ impl Ls { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - let pending = if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.pop_front() - } else { - None - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// # Safety /// `task` must be a live heap allocation produced by /// [`ShellLsTask::create`]; ownership is reclaimed here. @@ -290,71 +268,7 @@ impl Ls { } } -impl OutputTaskVTable for Ls { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Ls, queue_in_exec); #[derive(Clone, Copy, Default)] pub enum ResultKind { diff --git a/src/runtime/shell/builtin/mkdir.rs b/src/runtime/shell/builtin/mkdir.rs index a47d53443ff..05c8163f694 100644 --- a/src/runtime/shell/builtin/mkdir.rs +++ b/src/runtime/shell/builtin/mkdir.rs @@ -4,7 +4,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -137,25 +137,6 @@ impl Mkdir { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - let pending = match &mut Self::state_mut(interp, cmd).state { - State::WaitingWriteErr => return Builtin::done(interp, cmd, 1), - State::Exec(exec) => exec.output_queue.pop_front(), - State::Idle | State::Done => panic!("Invalid state"), - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// The caller ([`ShellMkdirTask::run_from_main_thread`]) owns the heap /// allocation and drops it after this returns. pub(crate) fn on_shell_mkdir_task_done( @@ -186,80 +167,7 @@ enum NextAction { Schedule(usize), } -impl OutputTaskVTable for Mkdir { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // OutputTask has no `WriterTag` of its own (it is not directly - // dispatchable as an IOWriter child), so the enqueue is tagged - // `WriterTag::Builtin` and `child` is stashed on `output_queue`; - // `on_io_writer_chunk` pops it to route the completion back to - // the OutputTask state machine and reclaim the box. - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - // See write_err — stash `child` so the chunk callback routes to - // OutputTask::on_io_writer_chunk. - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Mkdir, queue_in_exec); /// Runs `mkdir`/`mkdir -p` on a worker /// thread, then bounces back to the main thread. diff --git a/src/runtime/shell/builtin/touch.rs b/src/runtime/shell/builtin/touch.rs index 6faa724faa1..d5499bae876 100644 --- a/src/runtime/shell/builtin/touch.rs +++ b/src/runtime/shell/builtin/touch.rs @@ -2,7 +2,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -126,28 +126,6 @@ impl Touch { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - let pending = if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.pop_front() - } else { - None - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// # Safety /// `task` must be a live heap allocation produced by /// [`ShellTouchTask::create`]; ownership is reclaimed here. @@ -174,71 +152,7 @@ impl Touch { } } -impl OutputTaskVTable for Touch { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Touch, queue_in_exec); /// utimes() the path (creating it on ENOENT) on a worker thread. pub struct ShellTouchTask { diff --git a/src/runtime/shell/interpreter.rs b/src/runtime/shell/interpreter.rs index 693b33fa798..5b208c8083f 100644 --- a/src/runtime/shell/interpreter.rs +++ b/src/runtime/shell/interpreter.rs @@ -2773,6 +2773,147 @@ impl OutputTask

{ } } +/// Stamps out the boilerplate [`OutputTaskVTable`] impl shared by the +/// task-based builtins (cp/ls/mkdir/touch): bump +/// `output_waiting`/`output_done` on the `Exec` state, stash the task on the +/// builtin's `output_queue` when the write goes through the IOWriter, and +/// fall back to `write_no_io` otherwise. Also stamps out the builtin's +/// `on_io_writer_chunk`, which pops the queue to route the chunk completion +/// back to the OutputTask state machine. +/// +/// The second argument selects where `output_queue` lives: +/// - `queue_in_exec` — on the `State::Exec` payload (ls/mkdir/touch) +/// - `queue_on_self` — directly on the builtin struct (cp; see the field +/// comment there) +/// +/// Expands in the builtin's module, so `State`, `Builtin`, etc. resolve to +/// the caller's imports and the builtin's own `state_mut`/`next` are used. +macro_rules! impl_output_task_vtable { + ($builtin:ident, queue_in_exec) => { + impl $builtin { + #[inline] + fn output_queue_push(interp: &Interpreter, cmd: NodeId, child: *mut OutputTask) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_queue.push_back(child); + } + } + #[inline] + fn output_queue_pop(interp: &Interpreter, cmd: NodeId) -> Option<*mut OutputTask> { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_queue.pop_front() + } else { + None + } + } + } + crate::shell::interpreter::impl_output_task_vtable!(@impl $builtin); + }; + ($builtin:ident, queue_on_self) => { + impl $builtin { + #[inline] + fn output_queue_push(interp: &Interpreter, cmd: NodeId, child: *mut OutputTask) { + Self::state_mut(interp, cmd).output_queue.push_back(child); + } + #[inline] + fn output_queue_pop(interp: &Interpreter, cmd: NodeId) -> Option<*mut OutputTask> { + Self::state_mut(interp, cmd).output_queue.pop_front() + } + } + crate::shell::interpreter::impl_output_task_vtable!(@impl $builtin); + }; + (@impl $builtin:ident) => { + impl $builtin { + pub(crate) fn on_io_writer_chunk( + interp: &Interpreter, + cmd: NodeId, + written: usize, + e: Option, + ) -> Yield { + if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { + return Builtin::done(interp, cmd, 1); + } + if let Some(task) = Self::output_queue_pop(interp, cmd) { + // SAFETY: `task` was heap-allocated in `OutputTask::new` and + // pushed by `write_err`/`write_out`; not yet freed. + return unsafe { + OutputTask::::on_io_writer_chunk(task, interp, written, e) + }; + } + Self::next(interp, cmd) + } + } + impl OutputTaskVTable for $builtin { + fn write_err( + interp: &Interpreter, + cmd: NodeId, + child: *mut OutputTask, + errbuf: &[u8], + ) -> Option { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_waiting += 1; + } + if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { + // OutputTask has no `WriterTag` of its own (it is not + // directly dispatchable as an IOWriter child), so the + // enqueue is tagged `WriterTag::Builtin` and `child` is + // stashed on `output_queue`; the builtin's + // `on_io_writer_chunk` pops it to route the completion + // back to the OutputTask state machine and reclaim the + // box. + Self::output_queue_push(interp, cmd, child); + let childptr = ChildPtr::new(cmd, WriterTag::Builtin); + return Some( + Builtin::of_mut(interp, cmd) + .stderr + .enqueue(childptr, errbuf, safeguard), + ); + } + let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); + None + } + fn on_write_err(interp: &Interpreter, cmd: NodeId) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_done += 1; + } + } + fn write_out( + interp: &Interpreter, + cmd: NodeId, + child: *mut OutputTask, + output: &mut OutputSrc, + ) -> Option { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_waiting += 1; + } + if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { + // See write_err — stash `child` so the chunk callback + // routes to OutputTask::on_io_writer_chunk. + Self::output_queue_push(interp, cmd, child); + let childptr = ChildPtr::new(cmd, WriterTag::Builtin); + let buf = output.slice().to_vec(); + return Some( + Builtin::of_mut(interp, cmd) + .stdout + .enqueue(childptr, &buf, safeguard), + ); + } + let buf = output.slice().to_vec(); + let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); + None + } + fn on_write_out(interp: &Interpreter, cmd: NodeId) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_done += 1; + } + } + fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { + Self::next(interp, cmd) + } + } + }; +} +pub(crate) use impl_output_task_vtable; + // ──────────────────────────────────────────────────────────────────────────── // ShellTask // ──────────────────────────────────────────────────────────────────────────── diff --git a/src/runtime/shell/shell_body.rs b/src/runtime/shell/shell_body.rs index 858fb9769f0..a1dc213ac8f 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -1108,17 +1108,14 @@ pub mod testing_apis { } } - /// Codegen (`generated_js2native.rs`) wraps this with `host_fn_result`, so we - /// expose the bare `JsHostFnZig` signature here and do the buffer scope inline. - pub fn shell_lex(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|buf| shell_lex_impl(global, callframe, buf)) - } - - fn shell_lex_impl( + /// Shared prologue for the lex/parse testing APIs: extract the two + /// arguments (template strings + interpolated values) and assemble the + /// shell source via `shell_cmd_from_js`. + fn shell_cmd_args_from_js( global: &JSGlobalObject, callframe: &CallFrame, marked_argument_buffer: &mut MarkedArgumentBuffer, - ) -> JsResult { + ) -> JsResult<(Bump, JsStrings, Vec, Vec)> { let arguments_ = callframe.arguments_old::<2>(); // SAFETY: bun_vm() is non-null for a Bun-owned global. let vm = global.bun_vm(); @@ -1142,7 +1139,6 @@ pub mod testing_apis { let mut jsstrings = JsStrings::with_capacity(4); // SAFETY: every JSValue pushed here is also rooted in marked_argument_buffer. let mut jsobjs: Vec = Vec::new(); - let mut script: Vec = Vec::new(); shell_cmd_from_js( global, @@ -1153,6 +1149,22 @@ pub mod testing_apis { &mut script, marked_argument_buffer, )?; + Ok((arena, jsstrings, jsobjs, script)) + } + + /// Codegen (`generated_js2native.rs`) wraps this with `host_fn_result`, so we + /// expose the bare `JsHostFnZig` signature here and do the buffer scope inline. + pub fn shell_lex(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + MarkedArgumentBuffer::new(|buf| shell_lex_impl(global, callframe, buf)) + } + + fn shell_lex_impl( + global: &JSGlobalObject, + callframe: &CallFrame, + marked_argument_buffer: &mut MarkedArgumentBuffer, + ) -> JsResult { + let (arena, mut jsstrings, jsobjs, script) = + shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?; let jsobjs_len: u32 = u32::try_from(jsobjs.len()).expect("int cast"); let lex_result = 'brk: { @@ -1199,39 +1211,8 @@ pub mod testing_apis { callframe: &CallFrame, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { - let arguments_ = callframe.arguments_old::<2>(); - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, arguments_.slice()); - let string_args: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell_parse: expected 2 arguments, got 0"))); - } - }; - - let arena = Bump::new(); - - let template_args_js: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell: expected 2 arguments, got 0"))); - } - }; - let mut template_args = template_args_js.array_iterator(global)?; - let mut jsstrings = JsStrings::with_capacity(4); - // SAFETY: every JSValue pushed here is also rooted in marked_argument_buffer. - let mut jsobjs: Vec = Vec::new(); - let mut script: Vec = Vec::new(); - shell_cmd_from_js( - global, - string_args, - &mut template_args, - &mut jsobjs, - &mut jsstrings, - &mut script, - marked_argument_buffer, - )?; + let (arena, mut jsstrings, mut jsobjs, script) = + shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?; let mut out_parser: Option> = None; let mut out_lex_result: Option> = None; diff --git a/src/runtime/shell/subproc.rs b/src/runtime/shell/subproc.rs index 7051305d3e7..201567330fa 100644 --- a/src/runtime/shell/subproc.rs +++ b/src/runtime/shell/subproc.rs @@ -1025,10 +1025,10 @@ impl Writable { // match (E0509). Dispatch on `&mut` and `mem::take` / ManuallyDrop the // non-Copy payloads. let mut stdio = stdio; - #[cfg(windows)] - { - match &mut stdio { - Stdio::Pipe | Stdio::ReadableStream(_) => { + match &mut stdio { + Stdio::Pipe | Stdio::ReadableStream(_) => { + #[cfg(windows)] + { if let StdioResult::Buffer(buf) = result { // Ownership of the `Box` transfers into the // FileSink's writer. @@ -1057,93 +1057,39 @@ impl Writable { // owned ref; `adopt` takes it over. return Ok(Writable::Pipe(unsafe { FileSinkPtr::adopt(pipe_ptr) })); } - return Ok(Writable::Inherit); - } - - Stdio::Blob(_) => { - // E0509: `Stdio` impls `Drop`, so the payload cannot be - // destructure-moved out. Take ownership via ManuallyDrop + - // ptr::read; the wrapper suppresses the Stdio destructor so - // the blob is moved exactly once. - let old = - core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); - // SAFETY: `old` is Blob (matched above) and ManuallyDrop - // prevents its Drop from running, so this is the sole move. - let blob = match &*old { - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - return Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_blob(blob), - ))); + Ok(Writable::Inherit) } - Stdio::ArrayBuffer(array_buffer) => { - return Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_array_buffer(core::mem::take(array_buffer)), - ))); - } - Stdio::Fd(fd) => { - return Ok(Writable::Fd(*fd)); - } - Stdio::Dup2(dup2) => { - return Ok(Writable::Fd(dup2.to.to_fd())); - } - Stdio::Inherit => { - return Ok(Writable::Inherit); - } - Stdio::Memfd(_) | Stdio::Path(_) | Stdio::Ignore => { - return Ok(Writable::Ignore); - } - Stdio::Ipc | Stdio::Capture(_) => { - return Ok(Writable::Ignore); + #[cfg(not(windows))] + { + // The shell never uses this + panic!("Unimplemented stdin pipe/readable_stream"); } } - } - #[cfg(not(windows))] - { - match &mut stdio { - Stdio::Dup2(_) => { - // The shell never uses this - panic!("Unimplemented stdin dup2"); + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => Ok(Writable::Buffer( + JscSubprocess::writable::buffered_stdin_writer( + &mut stdio, event_loop, subprocess, result, + ), + )), + Stdio::Dup2(dup2) => { + #[cfg(windows)] + { + Ok(Writable::Fd(dup2.to.to_fd())) } - Stdio::Pipe => { + #[cfg(not(windows))] + { + let _ = dup2; // The shell never uses this - panic!("Unimplemented stdin pipe"); + panic!("Unimplemented stdin dup2"); } - - Stdio::Blob(_) => { - // E0509: `Stdio` impls `Drop`, so the payload cannot be - // destructure-moved out. Take ownership via ManuallyDrop + - // ptr::read; the wrapper suppresses the Stdio destructor so - // the blob is moved exactly once. - let old = - core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); - let blob = match &*old { - // SAFETY: `old` is Blob (matched above) and ManuallyDrop - // prevents its Drop from running, so this is the sole move. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_blob(blob), - ))) + } + Stdio::Memfd(memfd) => { + #[cfg(windows)] + { + let _ = memfd; + Ok(Writable::Ignore) } - Stdio::ArrayBuffer(array_buffer) => Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_array_buffer(core::mem::take(array_buffer)), - ))), - Stdio::Memfd(memfd) => { + #[cfg(not(windows))] + { debug_assert!(memfd.is_valid()); let fd = *memfd; // Ownership of the fd transfers to `Writable::Memfd`. @@ -1155,15 +1101,20 @@ impl Writable { core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); Ok(Writable::Memfd(fd)) } - Stdio::Fd(_) => Ok(Writable::Fd(result.unwrap())), - Stdio::Inherit => Ok(Writable::Inherit), - Stdio::Path(_) | Stdio::Ignore => Ok(Writable::Ignore), - Stdio::Ipc | Stdio::Capture(_) => Ok(Writable::Ignore), - Stdio::ReadableStream(_) => { - // The shell never uses this - panic!("Unimplemented stdin readable_stream"); + } + Stdio::Fd(fd) => { + #[cfg(windows)] + { + Ok(Writable::Fd(*fd)) + } + #[cfg(not(windows))] + { + let _ = fd; + Ok(Writable::Fd(result.unwrap())) } } + Stdio::Inherit => Ok(Writable::Inherit), + Stdio::Path(_) | Stdio::Ignore | Stdio::Ipc | Stdio::Capture(_) => Ok(Writable::Ignore), } } @@ -1288,52 +1239,32 @@ impl Readable { // Note: `Stdio` impls Drop, so dispatch on `&mut` and `mem::take` // Default-able payloads instead of partial moves (E0509). let mut stdio = stdio; - #[cfg(windows)] - { - return match &mut stdio { - Stdio::Inherit => Readable::Inherit, - Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, - Stdio::Path(_) => Readable::Ignore, - Stdio::Fd(fd) => Readable::Fd(*fd), - // blobs are immutable, so we should only ever get the case - // where the user passed in a Blob with an fd - Stdio::Blob(_) => Readable::Ignore, - Stdio::Memfd(_) => Readable::Ignore, - Stdio::Pipe => Readable::Pipe(PipeReader::create( - event_loop, process, result, None, out_type, interp, - )), - Stdio::ArrayBuffer(array_buffer) => { - let mut pipe = - PipeReader::create(event_loop, process, result, None, out_type, interp); - // The Arc was just created by `PipeReader::create` and is - // uniquely held (strong=1, weak=0) — `get_mut` is the - // safe route to set `buffered_output` before it's shared. - Arc::get_mut(&mut pipe) - .expect("fresh PipeReader Arc") - .buffered_output = BufferedOutput::ArrayBuffer { - buf: core::mem::take(array_buffer), - i: 0, - }; - Readable::Pipe(pipe) + match &mut stdio { + Stdio::Inherit => Readable::Inherit, + Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, + Stdio::Path(_) => Readable::Ignore, + Stdio::Fd(fd) => { + #[cfg(windows)] + { + Readable::Fd(*fd) } - Stdio::Capture(_) => Readable::Pipe(PipeReader::create( - event_loop, process, result, shellio, out_type, interp, - )), - Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream - }; - } - - #[cfg(not(windows))] - { - match &mut stdio { - Stdio::Inherit => Readable::Inherit, - Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, - Stdio::Path(_) => Readable::Ignore, - Stdio::Fd(_) => Readable::Fd(result.unwrap()), - // blobs are immutable, so we should only ever get the case - // where the user passed in a Blob with an fd - Stdio::Blob(_) => Readable::Ignore, - Stdio::Memfd(memfd) => { + #[cfg(not(windows))] + { + let _ = fd; + Readable::Fd(result.unwrap()) + } + } + // blobs are immutable, so we should only ever get the case + // where the user passed in a Blob with an fd + Stdio::Blob(_) => Readable::Ignore, + Stdio::Memfd(memfd) => { + #[cfg(windows)] + { + let _ = memfd; + Readable::Ignore + } + #[cfg(not(windows))] + { let fd = *memfd; // Ownership of the fd transfers to `Readable::Memfd`. Swap in // `Ignore` and suppress the old value's destructor so @@ -1342,28 +1273,28 @@ impl Readable { core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); Readable::Memfd(fd) } - Stdio::Pipe => Readable::Pipe(PipeReader::create( - event_loop, process, result, None, out_type, interp, - )), - Stdio::ArrayBuffer(array_buffer) => { - let mut pipe = - PipeReader::create(event_loop, process, result, None, out_type, interp); - // The Arc was just created by `PipeReader::create` and is - // uniquely held (strong=1, weak=0) — `get_mut` is the safe - // route to set `buffered_output` before it's shared. - Arc::get_mut(&mut pipe) - .expect("fresh PipeReader Arc") - .buffered_output = BufferedOutput::ArrayBuffer { - buf: core::mem::take(array_buffer), - i: 0, - }; - Readable::Pipe(pipe) - } - Stdio::Capture(_) => Readable::Pipe(PipeReader::create( - event_loop, process, result, shellio, out_type, interp, - )), - Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream } + Stdio::Pipe => Readable::Pipe(PipeReader::create( + event_loop, process, result, None, out_type, interp, + )), + Stdio::ArrayBuffer(array_buffer) => { + let mut pipe = + PipeReader::create(event_loop, process, result, None, out_type, interp); + // The Arc was just created by `PipeReader::create` and is + // uniquely held (strong=1, weak=0) — `get_mut` is the safe + // route to set `buffered_output` before it's shared. + Arc::get_mut(&mut pipe) + .expect("fresh PipeReader Arc") + .buffered_output = BufferedOutput::ArrayBuffer { + buf: core::mem::take(array_buffer), + i: 0, + }; + Readable::Pipe(pipe) + } + Stdio::Capture(_) => Readable::Pipe(PipeReader::create( + event_loop, process, result, shellio, out_type, interp, + )), + Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream } } diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index e29bfb98a03..ad7f58441ae 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -582,6 +582,32 @@ impl NewSocket { } } + /// Shared tail of the simple uws event callbacks (`on_writable`, + /// `on_timeout`, `on_end`, `on_data`): keeps `handlers` alive across the + /// user callback so the error handler can still be reached, routes a + /// thrown exception there, and nulls `self.handlers` when the scope's + /// exit frees the client `Handlers`. `extra_args` (at most one) follow + /// the implicit `this` argument. + #[inline] + fn call_socket_handler( + &self, + handlers: bun_ptr::BackRef, + callback: JSValue, + extra_args: &[JSValue], + ) { + let scope = Handlers::enter_ref(handlers); + let global = handlers.global_object; + let this_value = self.get_this_value(&global); + let mut args = [this_value; 2]; + args[1..1 + extra_args.len()].copy_from_slice(extra_args); + if let Err(err) = callback.call(&global, this_value, &args[..1 + extra_args.len()]) { + let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); + } + if scope.exit() { + self.handlers.set(None); + } + } + /// Noalias re-entrancy: takes `this: *mut Self`, NOT /// `&mut self`. `callback.call(...)` re-enters JS which can call /// `socket.write()`/`socket.end()`/`socket.reload()` on this same wrapper @@ -630,18 +656,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); this.deref(); } @@ -673,18 +688,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); } /// Returns the raw, freely-aliased @@ -1245,18 +1249,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); this.deref(); } @@ -1572,7 +1565,6 @@ impl NewSocket { } let global = handlers.global_object; - let this_value = this.get_this_value(&global); let output_value = match handlers.binary_type.to_js(data, &global) { Ok(v) => v, Err(err) => { @@ -1581,17 +1573,7 @@ impl NewSocket { } }; - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - // const encoding = handlers.encoding; - if let Err(err) = callback.call(&global, this_value, &[this_value, output_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[output_value]); } #[bun_jsc::host_fn(getter)] diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index 760d4fdaeb0..7e051fee2c6 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -7,8 +7,8 @@ use bun_jsc::JsCell; use bun_jsc::array_buffer::BinaryType; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, MarkedArgumentBuffer, Ref as JscRef, - StringJsc, SysErrorJsc, SystemError, + CallFrame, JSGlobalObject, JSValue, JsError, JsRef, JsResult, MarkedArgumentBuffer, + Ref as JscRef, StringJsc, SysErrorJsc, SystemError, }; use bun_ptr::BackRef; @@ -679,63 +679,38 @@ impl UDPSocket { event_loop.exit(); } - #[bun_jsc::host_fn(method)] - pub fn set_broadcast( - this: &Self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) + fn throw_setsockopt_errno(global_this: &JSGlobalObject, errno: SystemErrno) -> JsError { + global_this.throw_value( + bun_sys::Error::from_code_int(errno as c_int, bun_sys::Tag::setsockopt) .to_js(global_this), - )); - } - - let arguments = callframe.arguments(); - if arguments.len() < 1 { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected 1 argument, got {}", - arguments.len() - ))); - } - - let enabled = arguments[0].to_boolean(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), - )); - }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_broadcast(enabled); + ) + } + fn check_setsockopt(global_this: &JSGlobalObject, res: c_int) -> JsResult<()> { if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); } + Ok(()) + } - Ok(arguments[0]) + fn require_socket(&self, global_this: &JSGlobalObject) -> JsResult<&mut uws::udp::Socket> { + let Some(socket) = self.socket.get() else { + return Err(global_this.throw(format_args!("Socket is closed"))); + }; + // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. + Ok(uws::udp::Socket::opaque_mut(socket)) } - #[bun_jsc::host_fn(method)] - pub fn set_multicast_loopback( + fn set_bool_opt( this: &Self, global_this: &JSGlobalObject, callframe: &CallFrame, + function: fn(&mut uws::udp::Socket, bool) -> c_int, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -753,24 +728,46 @@ impl UDPSocket { // test-dgram-multicast-loopback.js). Throw EBADF to match the // `closed` branch above instead of panicking. let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); }; // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_loopback(enabled); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let res = function(uws::udp::Socket::opaque_mut(socket), enabled); + Self::check_setsockopt(global_this, res)?; Ok(arguments[0]) } + #[bun_jsc::host_fn(method)] + pub fn set_broadcast( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_broadcast, + ) + } + + #[bun_jsc::host_fn(method)] + pub fn set_multicast_loopback( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_multicast_loopback, + ) + } + fn set_membership( this: &Self, global_this: &JSGlobalObject, @@ -778,12 +775,9 @@ impl UDPSocket { drop: bool, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -802,20 +796,15 @@ impl UDPSocket { arguments[0], &mut addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } let mut interface: sockaddr_storage = bun_core::ffi::zeroed(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; + let socket = this.require_socket(global_this)?; let res = if arguments.len() > 1 && this.parse_addr( @@ -829,15 +818,12 @@ impl UDPSocket { "Family mismatch between address and interface" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, Some(&interface), drop) + socket.set_membership(&addr, Some(&interface), drop) } else { - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, None, drop) + socket.set_membership(&addr, None, drop) }; - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -867,12 +853,9 @@ impl UDPSocket { drop: bool, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -894,12 +877,9 @@ impl UDPSocket { arguments[0], &mut source_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -910,12 +890,9 @@ impl UDPSocket { arguments[1], &mut group_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -927,9 +904,7 @@ impl UDPSocket { let mut interface: sockaddr_storage = bun_core::ffi::zeroed(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; + let socket = this.require_socket(global_this)?; let res = if arguments.len() > 2 && this.parse_addr( @@ -943,25 +918,12 @@ impl UDPSocket { "Family mismatch among source, group and interface addresses" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( - &source_addr, - &group_addr, - Some(&interface), - drop, - ) + socket.set_source_specific_membership(&source_addr, &group_addr, Some(&interface), drop) } else { - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( - &source_addr, - &group_addr, - None, - drop, - ) + socket.set_source_specific_membership(&source_addr, &group_addr, None, drop) }; - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -991,12 +953,9 @@ impl UDPSocket { callframe: &CallFrame, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -1026,16 +985,9 @@ impl UDPSocket { return Ok(JSValue::FALSE); } - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; - - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_interface(&addr); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let socket = this.require_socket(global_this)?; + let res = socket.set_multicast_interface(&addr); + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -1075,12 +1027,9 @@ impl UDPSocket { function: fn(&mut uws::udp::Socket, i32) -> c_int, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -1093,15 +1042,9 @@ impl UDPSocket { } let ttl = arguments[0].coerce_to_i32(global_this)?; - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = function(uws::udp::Socket::opaque_mut(socket), ttl); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let socket = this.require_socket(global_this)?; + let res = function(socket, ttl); + Self::check_setsockopt(global_this, res)?; Ok(JSValue::js_number(ttl as f64)) } diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bdb88a682c8..f197a55b613 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -3089,6 +3089,175 @@ pub mod mock { } } + /// Compares one `mock.calls` entry (a JSArray of call arguments) against + /// the expected arguments: length pre-check, then per-argument + /// `jest_deep_equals` with early exit on the first mismatch. + pub(crate) fn call_args_equal( + global: &JSGlobalObject, + call_item: JSValue, + expected: &[JSValue], + ) -> JsResult { + if call_item.get_length(global)? != expected.len() as u64 { + return Ok(false); + } + let mut itr = call_item.array_iterator(global)?; + while let Some(call_arg) = itr.next()? { + if !call_arg.jest_deep_equals(expected[itr.i as usize - 1], global)? { + return Ok(false); + } + } + Ok(true) + } + + /// A `mock.results` entry parsed into its `type` tag. + pub(crate) enum MockResult { + /// Carries the entry's `value`. + Return(JSValue), + /// Carries the result object itself; callers that need the thrown + /// `value` fetch it lazily (`toHaveReturnedWith` never reads it). + Throw(JSValue), + /// Not an object, no string `type`, or an unrecognized tag (e.g. + /// "incomplete") — the `*ReturnedWith` matchers skip these entries. + Other, + } + + pub(crate) fn parse_mock_result(global: &JSGlobalObject, result: JSValue) -> JsResult { + if result.is_object() { + let result_type = result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); + if result_type.is_string() { + let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); + if type_str.eql_comptime("return") { + return Ok(MockResult::Return(result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED))); + } + if type_str.eql_comptime("throw") { + return Ok(MockResult::Throw(result)); + } + } + } + Ok(MockResult::Other) + } + + // ── shared failure epilogues for the `toHave*With` matcher family ────── + // Each matcher keeps only its index-selection logic and message verbs; + // the throw shapes below are byte-identical across the family. + + /// `.not` failure: `"\n\n{lead}: {expected}{tail}"` under the + /// `not`-form signature. + pub(crate) fn throw_not_failure( + this: &Expect, + global: &JSGlobalObject, + matcher_name: &'static str, + matcher_params: &'static str, + lead: fmt::Arguments<'_>, + expected: JSValue, + tail: &'static str, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + Expect::get_signature(matcher_name, matcher_params, true), + format_args!("\n\n{}: {}{}", lead, expected.to_fmt(&mut formatter), tail), + ) + } + + /// `"Expected: {expected}\nBut it was not called."` failure. + pub(crate) fn throw_not_called( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + expected: JSValue, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + signature, + format_args!("\n\nExpected: {}\nBut it was not called.", expected.to_fmt(&mut formatter)), + ) + } + + /// `"called N time(s), but call M was requested"` failure (`*Nth*` matchers). + pub(crate) fn throw_nth_call_missing( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + total_calls: u32, + n: u32, + tail: &'static str, + ) -> JsResult { + this.throw( + global, + signature, + format_args!( + "\n\nThe mock function was called {} time{}, but call {} was requested.{}", + total_calls, + if total_calls == 1 { "" } else { "s" }, + n, + tail, + ), + ) + } + + /// Diff failure: `"\n\n{prefix}{DiffFormatter}\n"`. + pub(crate) fn throw_diff( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + prefix: fmt::Arguments<'_>, + expected: JSValue, + received: JSValue, + ) -> JsResult { + let diff_format = DiffFormatter { + expected: Some(expected), + received: Some(received), + expected_string: None, + received_string: None, + global_this: Some(global), + not: false, + }; + this.throw(global, signature, format_args!("\n\n{}{}\n", prefix, diff_format)) + } + + /// `"{prefix}Expected: X\nReceived: Y"` failure. Two formatters because the + /// `ZigFormatter` adapter holds `&mut Formatter`, so two live adapters + /// cannot alias the same backing formatter. + pub(crate) fn throw_expected_received( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + prefix: fmt::Arguments<'_>, + expected: JSValue, + received: JSValue, + ) -> JsResult { + let mut f1 = make_formatter(global); + let mut f2 = make_formatter(global); + this.throw( + global, + signature, + format_args!( + "\n\n{}Expected: {}\nReceived: {}", + prefix, + expected.to_fmt(&mut f1), + received.to_fmt(&mut f2), + ), + ) + } + + /// `"{which} threw an error: …"` failure (`toHave{Last,Nth}ReturnedWith`). + pub(crate) fn throw_call_threw( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + which: fmt::Arguments<'_>, + error: JSValue, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + signature, + format_args!("\n\n{} threw an error: {}\n", which, error.to_fmt(&mut formatter)), + ) + } + pub(crate) fn jest_mock_return_object_type(global_this: &JSGlobalObject, value: JSValue) -> JsResult { if let Some(type_string) = value.fast_get(global_this, bun_jsc::BuiltinName::Type)? { if type_string.is_string() { diff --git a/src/runtime/test_runner/expect/toEqual.rs b/src/runtime/test_runner/expect/toEqual.rs index 8cc083a3f7c..61b80fba9a7 100644 --- a/src/runtime/test_runner/expect/toEqual.rs +++ b/src/runtime/test_runner/expect/toEqual.rs @@ -9,19 +9,40 @@ impl Expect { &self, global: &JSGlobalObject, frame: &CallFrame, + ) -> JsResult { + self.equals_impl(global, frame, "toEqual", JSValue::jest_deep_equals) + } + + #[bun_jsc::host_fn(method)] + pub fn to_strict_equal( + &self, + global: &JSGlobalObject, + frame: &CallFrame, + ) -> JsResult { + self.equals_impl(global, frame, "toStrictEqual", JSValue::jest_strict_deep_equals) + } + + fn equals_impl( + &self, + global: &JSGlobalObject, + frame: &CallFrame, + name: &'static str, + deep_equals: fn(JSValue, JSValue, &JSGlobalObject) -> JsResult, ) -> JsResult { let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toEqual", "expected")?; + self.matcher_prelude(global, frame.this(), name, "expected")?; let _arguments = frame.arguments_old::<1>(); let arguments: &[JSValue] = _arguments.slice(); if arguments.len() < 1 { - return Err(global.throw_invalid_arguments(format_args!("toEqual() requires 1 argument"))); + return Err( + global.throw_invalid_arguments(format_args!("{name}() requires 1 argument")) + ); } let expected = arguments[0]; - let mut pass = value.jest_deep_equals(expected, global)?; + let mut pass = deep_equals(value, expected, global)?; if not { pass = !pass; @@ -40,12 +61,7 @@ impl Expect { not, }; - if not { - let signature: &str = Expect::get_signature("toEqual", "expected", true); - return this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)); - } - - let signature: &str = Expect::get_signature("toEqual", "expected", false); + let signature: &str = Expect::get_signature(name, "expected", not); this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)) } } diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs index defb78cb26f..f177dbdc7e0 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs @@ -1,6 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; use super::mock; use super::Expect; @@ -32,20 +31,7 @@ pub(crate) fn to_have_been_called_with( ))); } - if call_item.get_length(global)? != arguments.len() as u64 { - continue; - } - - let mut call_itr = call_item.array_iterator(global)?; - let mut matched = true; - while let Some(call_arg) = call_itr.next()? { - if !call_arg.jest_deep_equals(arguments[call_itr.i as usize - 1], global)? { - matched = false; - break; - } - } - - if matched { + if mock::call_args_equal(global, call_item, arguments)? { pass = true; break; } @@ -57,52 +43,31 @@ pub(crate) fn to_have_been_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - let expected_args_js_array = JSValue::create_array_from_slice(global, arguments)?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = Expect::get_signature("toHaveBeenCalledWith", "...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected mock function not to have been called with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenCalledWith", "...expected", + format_args!("Expected mock function not to have been called with"), expected_args_js_array, "\nBut it was.", ); } let signature = Expect::get_signature("toHaveBeenCalledWith", "...expected", false); if calls_count == 0 { - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nBut it was not called.", - expected_args_js_array.to_fmt(&mut formatter), - ), - ); + return mock::throw_not_called(&this, global, signature, expected_args_js_array); } // If there's only one call, provide a nice diff. if calls_count == 1 { let received_call_args = calls.get_index(global, 0)?; - let diff_format = DiffFormatter { - received_string: None, - expected_string: None, - expected: Some(expected_args_js_array), - received: Some(received_call_args), - global_this: Some(global), - not: false, - }; - return this.throw(global, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global, signature, format_args!(""), expected_args_js_array, received_call_args); } // If there are multiple calls, list them all to help debugging. // The AllCallsWithArgsFormatter holds an exclusive borrow of the formatter, so // we allocate a second ConsoleObject formatter for the list. + let mut formatter = super::make_formatter(global); let mut list_fmt = super::make_formatter(global); let list_formatter = mock::AllCallsWithArgsFormatter { global_this: global, diff --git a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs index 735c24befba..0142baba9a4 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs @@ -1,6 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_been_last_called_with( @@ -15,7 +15,7 @@ pub(crate) fn to_have_been_last_called_with( frame.this(), "toHaveBeenLastCalledWith", "...expected", - super::mock::MockKind::CallsWithSig, + mock::MockKind::CallsWithSig, )?; let total_calls: u32 = calls.get_length(global)? as u32; @@ -34,17 +34,7 @@ pub(crate) fn to_have_been_last_called_with( ))); } - if last_call_value.get_length(global)? != arguments.len() as u64 { - pass = false; - } else { - let mut itr = last_call_value.array_iterator(global)?; - while let Some(call_arg) = itr.next()? { - if !call_arg.jest_deep_equals(arguments[itr.i as usize - 1], global)? { - pass = false; - break; - } - } - } + pass = mock::call_args_equal(global, last_call_value, arguments)?; } if pass != this.flags.get().not() { @@ -52,42 +42,20 @@ pub(crate) fn to_have_been_last_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - let expected_args_js_array = JSValue::create_array_from_slice(global, arguments)?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = get_signature("toHaveBeenLastCalledWith", "...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected last call not to be with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenLastCalledWith", "...expected", + format_args!("Expected last call not to be with"), expected_args_js_array, "\nBut it was.", ); } let signature = get_signature("toHaveBeenLastCalledWith", "...expected", false); if total_calls == 0 { - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nBut it was not called.", - expected_args_js_array.to_fmt(&mut formatter), - ), - ); + return mock::throw_not_called(&this, global, signature, expected_args_js_array); } - let diff_format = DiffFormatter { - expected: Some(expected_args_js_array), - received: Some(last_call_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - this.throw(global, signature, format_args!("\n\n{}\n", diff_format)) + mock::throw_diff(&this, global, signature, format_args!(""), expected_args_js_array, last_call_value) } diff --git a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs index fe270de99f3..691aeac36fe 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs @@ -1,5 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::Expect; pub(crate) fn to_have_been_nth_called_with( @@ -13,7 +13,7 @@ pub(crate) fn to_have_been_nth_called_with( frame.this(), "toHaveBeenNthCalledWith", "n, ...expected", - super::mock::MockKind::CallsWithSig, + mock::MockKind::CallsWithSig, )?; if arguments.is_empty() || !arguments[0].is_any_int() { @@ -36,7 +36,6 @@ pub(crate) fn to_have_been_nth_called_with( if pass { nth_call_value = calls.get_index(global, nth_call_num - 1)?; - let expected_args = &arguments[1..]; if !nth_call_value.js_type().is_array() { return Err(global.throw(format_args!( @@ -44,17 +43,7 @@ pub(crate) fn to_have_been_nth_called_with( ))); } - if nth_call_value.get_length(global)? != expected_args.len() as u64 { - pass = false; - } else { - let mut itr = nth_call_value.array_iterator(global)?; - while let Some(call_arg) = itr.next()? { - if !call_arg.jest_deep_equals(expected_args[(itr.i - 1) as usize], global)? { - pass = false; - break; - } - } - } + pass = mock::call_args_equal(global, nth_call_value, &arguments[1..])?; } if pass != this.flags.get().not() { @@ -62,52 +51,25 @@ pub(crate) fn to_have_been_nth_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - - let expected_args_slice = &arguments[1..]; - let expected_args_js_array = JSValue::create_array_from_slice(global, expected_args_slice)?; + let expected_args_js_array = JSValue::create_array_from_slice(global, &arguments[1..])?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = Expect::get_signature("toHaveBeenNthCalledWith", "n, ...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected call #{} not to be with: {}\nBut it was.", - nth_call_num, - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenNthCalledWith", "n, ...expected", + format_args!("Expected call #{} not to be with", nth_call_num), expected_args_js_array, "\nBut it was.", ); } let signature = Expect::get_signature("toHaveBeenNthCalledWith", "n, ...expected", false); // Handle case where function was not called enough times if total_calls < nth_call_num { - return this.throw( - global, - signature, - format_args!( - "\n\nThe mock function was called {} time{}, but call {} was requested.", - total_calls, - if total_calls == 1 { "" } else { "s" }, - nth_call_num, - ), - ); + return mock::throw_nth_call_missing(&this, global, signature, total_calls, nth_call_num, ""); } // The call existed but didn't match. Show a diff. - let diff_format = DiffFormatter { - expected: Some(expected_args_js_array), - received: Some(nth_call_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - this.throw( - global, - signature, - format_args!("\n\nCall #{}:\n{}\n", nth_call_num, diff_format), + mock::throw_diff( + &this, global, signature, + format_args!("Call #{}:\n", nth_call_num), expected_args_js_array, nth_call_value, ) } diff --git a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs index 750bd4b81ee..41e3df359b8 100644 --- a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs @@ -1,8 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::FormatterTestExt; -use bun_jsc::console_object::Formatter; -use super::DiffFormatter; +use super::mock; use super::Expect; pub(crate) fn to_have_last_returned_with( @@ -17,7 +15,7 @@ pub(crate) fn to_have_last_returned_with( callframe.this(), "toHaveBeenLastReturnedWith", "expected", - super::mock::MockKind::Returns, + mock::MockKind::Returns, )?; let calls_count = u32::try_from(returns.get_length(global_this)?).unwrap(); @@ -29,24 +27,18 @@ pub(crate) fn to_have_last_returned_with( if calls_count > 0 { let last_result = returns.get_direct_index(global_this, calls_count - 1); - if last_result.is_object() { - let result_type = last_result.get(global_this, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global_this)?); - - if type_str.eql_comptime("return") { - last_return_value = - last_result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); - - if last_return_value.jest_deep_equals(expected, global_this)? { - pass = true; - } - } else if type_str.eql_comptime("throw") { - last_call_threw = true; - last_error_value = - last_result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); + match mock::parse_mock_result(global_this, last_result)? { + mock::MockResult::Return(value) => { + last_return_value = value; + if last_return_value.jest_deep_equals(expected, global_this)? { + pass = true; } } + mock::MockResult::Throw(result) => { + last_call_threw = true; + last_error_value = result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); + } + mock::MockResult::Other => {} } } @@ -55,68 +47,27 @@ pub(crate) fn to_have_last_returned_with( } // Handle failure - let mut formatter = Formatter::new(global_this).with_quote_strings(true); - let signature = Expect::get_signature("toHaveBeenLastReturnedWith", "expected", false); if this.flags.get().not() { - return this.throw( - global_this, - Expect::get_signature("toHaveBeenLastReturnedWith", "expected", true), - format_args!( - concat!( - "\n\n", - "Expected mock function not to have last returned: {}\n", - "But it did.\n", - ), - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global_this, "toHaveBeenLastReturnedWith", "expected", + format_args!("Expected mock function not to have last returned"), expected, "\nBut it did.\n", ); } if calls_count == 0 { - return this.throw( - global_this, - signature, - format_args!(concat!("\n\n", "The mock function was not called.")), - ); + return this.throw(global_this, signature, format_args!("\n\nThe mock function was not called.")); } if last_call_threw { - return this.throw( - global_this, - signature, - format_args!( - concat!("\n\n", "The last call threw an error: {}\n"), - last_error_value.to_fmt(&mut formatter), - ), - ); + return mock::throw_call_threw(&this, global_this, signature, format_args!("The last call"), last_error_value); } // Diff if possible if expected.is_string() && last_return_value.is_string() { - let diff_format = DiffFormatter { - received_string: None, - expected_string: None, - expected: Some(expected), - received: Some(last_return_value), - global_this: Some(global_this), - not: false, - }; - return this.throw(global_this, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global_this, signature, format_args!(""), expected, last_return_value); } - // The `ZigFormatter` adapter holds `&'a mut Formatter`, so two live adapters cannot alias - // the same backing formatter. Use a second formatter for the received value — - // `make_formatter` is a trivial struct init with no shared state between values. - let mut formatter2 = super::make_formatter(global_this); - this.throw( - global_this, - signature, - format_args!( - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - last_return_value.to_fmt(&mut formatter2), - ), - ) + mock::throw_expected_received(&this, global_this, signature, format_args!(""), expected, last_return_value) } diff --git a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs index 8367a6e9c85..20fc77ef931 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs @@ -1,6 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_nth_returned_with( @@ -15,7 +15,7 @@ pub(crate) fn to_have_nth_returned_with( frame.this(), "toHaveNthReturnedWith", "n, expected", - super::mock::MockKind::Returns, + mock::MockKind::Returns, )?; // Validate n is a number @@ -38,25 +38,22 @@ pub(crate) fn to_have_nth_returned_with( let mut nth_return_value: JSValue = JSValue::UNDEFINED; let mut nth_call_threw = false; let mut nth_error_value: JSValue = JSValue::UNDEFINED; - let mut nth_call_exists = false; + let nth_call_exists = index < calls_count; - if index < calls_count { - nth_call_exists = true; + if nth_call_exists { let nth_result = returns.get_direct_index(global, index); - if nth_result.is_object() { - let result_type = nth_result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); - if type_str.eql_comptime("return") { - nth_return_value = nth_result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); - if nth_return_value.jest_deep_equals(expected, global)? { - pass = true; - } - } else if type_str.eql_comptime("throw") { - nth_call_threw = true; - nth_error_value = nth_result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); + match mock::parse_mock_result(global, nth_result)? { + mock::MockResult::Return(value) => { + nth_return_value = value; + if nth_return_value.jest_deep_equals(expected, global)? { + pass = true; } } + mock::MockResult::Throw(result) => { + nth_call_threw = true; + nth_error_value = result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); + } + mock::MockResult::Other => {} } } @@ -65,74 +62,27 @@ pub(crate) fn to_have_nth_returned_with( } // Handle failure - let mut formatter = super::make_formatter(global); - let mut formatter2 = super::make_formatter(global); - // defer formatter.deinit() — handled by Drop - let signature = get_signature("toHaveNthReturnedWith", "n, expected", false); if this.flags.get().not() { - return this.throw( - global, - get_signature("toHaveNthReturnedWith", "n, expected", true), - format_args!( - "\n\nExpected mock function not to have returned on call {}: {}\nBut it did.\n", - n, - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveNthReturnedWith", "n, expected", + format_args!("Expected mock function not to have returned on call {}", n), expected, "\nBut it did.\n", ); } if !nth_call_exists { - return this.throw( - global, - signature, - format_args!( - "\n\nThe mock function was called {} time{}, but call {} was requested.\n", - calls_count, - if calls_count == 1 { "" } else { "s" }, - n, - ), - ); + return mock::throw_nth_call_missing(&this, global, signature, calls_count, index + 1, "\n"); } if nth_call_threw { - return this.throw( - global, - signature, - format_args!( - "\n\nCall {} threw an error: {}\n", - n, - nth_error_value.to_fmt(&mut formatter), - ), - ); + return mock::throw_call_threw(&this, global, signature, format_args!("Call {}", n), nth_error_value); } // Diff if possible if expected.is_string() && nth_return_value.is_string() { - let diff_format = DiffFormatter { - expected: Some(expected), - received: Some(nth_return_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - return this.throw( - global, - signature, - format_args!("\n\nCall {}:\n{}\n", n, diff_format), - ); + return mock::throw_diff(&this, global, signature, format_args!("Call {}:\n", n), expected, nth_return_value); } - this.throw( - global, - signature, - format_args!( - "\n\nCall {}:\nExpected: {}\nReceived: {}", - n, - expected.to_fmt(&mut formatter), - nth_return_value.to_fmt(&mut formatter2), - ), - ) + mock::throw_expected_received(&this, global, signature, format_args!("Call {}:\n", n), expected, nth_return_value) } diff --git a/src/runtime/test_runner/expect/toHaveReturnedWith.rs b/src/runtime/test_runner/expect/toHaveReturnedWith.rs index d6bcd036583..d293fca74d4 100644 --- a/src/runtime/test_runner/expect/toHaveReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveReturnedWith.rs @@ -1,6 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; use super::mock; use super::Expect; @@ -32,25 +31,17 @@ pub(crate) fn to_have_returned_with( for i in 0..calls_count { let result = returns.get_direct_index(global, i); - if result.is_object() { - let result_type = result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); - - if type_str.eql_comptime("return") { - let result_value = result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); - successful_returns.push(result_value); - - // Check for pass condition only if not already passed - if !pass { - if result_value.jest_deep_equals(expected, global)? { - pass = true; - } - } - } else if type_str.eql_comptime("throw") { - has_errors = true; + match mock::parse_mock_result(global, result)? { + mock::MockResult::Return(result_value) => { + successful_returns.push(result_value); + + // Check for pass condition only if not already passed + if !pass && result_value.jest_deep_equals(expected, global)? { + pass = true; } } + mock::MockResult::Throw(_) => has_errors = true, + mock::MockResult::Other => {} } } @@ -59,19 +50,12 @@ pub(crate) fn to_have_returned_with( } // Handle failure - let mut formatter = super::make_formatter(global); - let signature: &str = Expect::get_signature("toHaveReturnedWith", "expected", false); if this.flags.get().not() { - let not_signature: &str = Expect::get_signature("toHaveReturnedWith", "expected", true); - return this.throw( - global, - not_signature, - format_args!( - "\n\nExpected mock function not to have returned: {}\n", - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveReturnedWith", "expected", + format_args!("Expected mock function not to have returned"), expected, "\n", ); } @@ -82,34 +66,15 @@ pub(crate) fn to_have_returned_with( if calls_count == 1 && successful_returns_count == 1 { let received = successful_returns[0]; if expected.is_string() && received.is_string() { - let diff_format = DiffFormatter { - expected: Some(expected), - received: Some(received), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - return this.throw(global, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global, signature, format_args!(""), expected, received); } - // The `ZigFormatter` adapter holds `&'a mut Formatter`, so two live adapters cannot alias - // the same backing formatter. Use a second formatter for the received value — - // `make_formatter` is a trivial struct init with no shared state between values. - let mut formatter2 = super::make_formatter(global); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - received.to_fmt(&mut formatter2), - ), - ); + return mock::throw_expected_received(&this, global, signature, format_args!(""), expected, received); } // list_formatter holds &mut Formatter via RefCell, so a separate formatter is // required for the inline `expected.to_fmt` argument used alongside it in the same format_args!. + let mut formatter = super::make_formatter(global); let mut list_fmt = super::make_formatter(global); if has_errors { @@ -119,7 +84,7 @@ pub(crate) fn to_have_returned_with( returns, formatter: core::cell::RefCell::new(&mut list_fmt), }; - return this.throw( + this.throw( global, signature, format_args!( @@ -129,7 +94,7 @@ pub(crate) fn to_have_returned_with( successful_returns_count, calls_count, ), - ); + ) } else { // Case: No errors, but no match (and multiple returns) let list_formatter = mock::SuccessfulReturnsFormatter { @@ -137,7 +102,7 @@ pub(crate) fn to_have_returned_with( successful_returns: &successful_returns, formatter: core::cell::RefCell::new(&mut list_fmt), }; - return this.throw( + this.throw( global, signature, format_args!( @@ -146,6 +111,6 @@ pub(crate) fn to_have_returned_with( list_formatter, successful_returns_count, ), - ); + ) } } diff --git a/src/runtime/test_runner/expect/toStrictEqual.rs b/src/runtime/test_runner/expect/toStrictEqual.rs deleted file mode 100644 index 9c246be1022..00000000000 --- a/src/runtime/test_runner/expect/toStrictEqual.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; - -use super::DiffFormatter; -use super::Expect; - -impl Expect { - #[bun_jsc::host_fn(method)] - pub fn to_strict_equal( - &self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toStrictEqual", "expected")?; - - let _arguments = frame.arguments_old::<1>(); - let arguments: &[JSValue] = _arguments.slice(); - - if arguments.len() < 1 { - return Err(global.throw_invalid_arguments( - format_args!("toStrictEqual() requires 1 argument"), - )); - } - - let expected = arguments[0]; - let mut pass = value.jest_strict_deep_equals(expected, global)?; - - if not { - pass = !pass; - } - if pass { - return Ok(JSValue::UNDEFINED); - } - - // handle failure - let diff_formatter = DiffFormatter { - received: Some(value), - expected: Some(expected), - received_string: None, - expected_string: None, - global_this: Some(global), - not, - }; - - if not { - let signature = Expect::get_signature("toStrictEqual", "expected", true); - return this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)); - } - - let signature = Expect::get_signature("toStrictEqual", "expected", false); - this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)) - } -} diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 5b16e24b857..986af8bf178 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -577,7 +577,6 @@ pub mod expect { "toMatchSnapshot.rs" => to_match_snapshot, "toSatisfy.rs" => to_satisfy, "toStartWith.rs" => to_start_with, - "toStrictEqual.rs" => to_strict_equal, "toThrow.rs" => to_throw, "toThrowErrorMatchingInlineSnapshot.rs" => to_throw_error_matching_inline_snapshot, "toThrowErrorMatchingSnapshot.rs" => to_throw_error_matching_snapshot, diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 2f1dd496b23..c3d03d76153 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1,4 +1,4 @@ -use core::cell::{Cell, RefCell}; +use core::cell::RefCell; use crate::test_runner::expect::JSValueTestExt; use core::ffi::c_void; @@ -126,25 +126,6 @@ pub enum MessageLevel { Info = 4, } -#[repr(u32)] -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum MessageType { - Log = 0, - Dir = 1, - DirXML = 2, - Table = 3, - Trace = 4, - StartGroup = 5, - StartGroupCollapsed = 6, - EndGroup = 7, - Clear = 8, - Assert = 9, - Timing = 10, - Profile = 11, - ProfileEnd = 12, - Image = 13, -} - #[derive(Copy, Clone, Default)] pub struct FormatOptions { pub enable_colors: bool, @@ -400,61 +381,6 @@ impl Drop for Formatter<'_> { } } -/// `Display` adapter for formatting a single [`JSValue`]. -/// -/// `Display::fmt` only gives us `&self`, so the -/// mutable handle is parked behind a `Cell` and moved out for the duration of -/// the call — this preserves unique-borrow provenance without the -/// `&shared → *const → *mut` cast that would be UB under Stacked Borrows. -pub struct ZigFormatter<'a, 'b> { - pub formatter: Cell>>, - pub global: &'b JSGlobalObject, - pub value: JSValue, -} - -impl<'a, 'b> ZigFormatter<'a, 'b> { - pub fn new(formatter: &'a mut Formatter<'b>, global: &'b JSGlobalObject, value: JSValue) -> Self { - Self { formatter: Cell::new(Some(formatter)), global, value } - } -} - -#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] -pub enum WriteError { - #[error("UhOh")] - UhOh, -} - -impl core::fmt::Display for ZigFormatter<'_, '_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // Move the unique `&mut Formatter` out of the cell for the body; - // re-seat it (and clear `remaining_values`) on the way out so the - // adapter stays reusable. - let formatter: &mut Formatter<'_> = self - .formatter - .take() - .expect("ZigFormatter::fmt re-entered or used after consumption"); - - // Assigning a stack-local slice into `Formatter<'b>` would require `'b: 'local`, - // which borrowck rejects. The single-value path never reads `remaining_values` - // (only `StringPossiblyFormatted` consumes it, and `ZigFormatter` always emits a - // single tag), so leaving it `&[]` is observationally equivalent. - formatter.remaining_values = &[]; - formatter.global_this = self.global; - - let result = (|| { - let tag = Tag::get(self.value, self.global).map_err(|_| core::fmt::Error)?; - let mut adapter = bun_io::FmtAdapter::new(f); - formatter - .format::<_, false>(tag, &mut adapter, self.value, self.global) - .map_err(|_| core::fmt::Error) - })(); - - formatter.remaining_values = &[]; - self.formatter.set(Some(formatter)); - result - } -} - #[repr(u8)] #[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] pub enum Tag { @@ -620,20 +546,7 @@ impl Tag { | JSType::ModuleNamespaceObject | JSType::GlobalObject => Tag::Object, - JSType::ArrayBuffer - | JSType::Int8Array - | JSType::Uint8Array - | JSType::Uint8ClampedArray - | JSType::Int16Array - | JSType::Uint16Array - | JSType::Int32Array - | JSType::Uint32Array - | JSType::Float16Array - | JSType::Float32Array - | JSType::Float64Array - | JSType::BigInt64Array - | JSType::BigUint64Array - | JSType::DataView => Tag::TypedArray, + t if t.is_array_buffer_like() => Tag::TypedArray, JSType::HeapBigInt => Tag::BigInt, @@ -2866,7 +2779,7 @@ impl AsymmetricMatcherFormatter for bun_jsc::console_object::Formatter<'_> { ) -> JsResult<()> { let global = self.global_this; self.format::( - bun_jsc::console_object::formatter::TagResult { tag: tag.into(), cell }, + bun_jsc::console_object::formatter::TagResult { tag, cell, custom: None }, w, v, global, diff --git a/src/runtime/valkey_jsc/js_valkey_functions.rs b/src/runtime/valkey_jsc/js_valkey_functions.rs index 5789403e5d6..34c04abaac5 100644 --- a/src/runtime/valkey_jsc/js_valkey_functions.rs +++ b/src/runtime/valkey_jsc/js_valkey_functions.rs @@ -102,7 +102,7 @@ fn promise_to_js(p: *mut JSPromise) -> JSValue { /// `this.send()` it, and convert the result to a `JsResult` — /// `Ok(promise.toJS())` on success, a JS-side Redis error value on failure. /// -/// All 7 `cmd_*!` macros and ~24 hand-written methods (`get`, `getBuffer`, +/// Both `cmd_*!` macros and ~24 hand-written methods (`get`, `getBuffer`, /// `set`, `incr`, `decr`, `exists`, `expire`, `ttl`, `srem`, `sadd`, /// `sismember`, `hmget`, `hincrby`, `hset`, `smove`, `publish`, /// `send_unsubscribe_request_and_cleanup`, …) duplicated this 15-line block @@ -162,15 +162,18 @@ pub(crate) mod compile { // Note: each command-shape generator is a `macro_rules!` that emits a // `#[bun_jsc::host_fn(method)]` inside the `impl JSValkeyClient` block: -// cmd_noargs! (), cmd_key! (key: RedisKey), -// cmd_key_varargs! (key: RedisKey, ...args: RedisKey[]), -// cmd_key_value! (key: RedisKey, value: RedisValue), -// cmd_key_value_value2! (key: RedisKey, value: RedisValue, value2: RedisValue), -// cmd_strings_varargs! (...strings: string[]), -// cmd_key_value_varargs! (key: RedisKey, value: RedisValue, ...args: RedisValue) - -macro_rules! cmd_noargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { +// +// - cmd! extracts one positional argument per name, in order: +// cmd!(f, name, "CMD", state) () +// cmd!(f, name, "CMD", "key", state) (key: RedisKey) +// cmd!(f, name, "CMD", "key", "value", state) (key: RedisKey, value: RedisValue) +// - cmd_varargs! forwards every provided argument: `skip_null` silently drops +// undefined/null arguments, `strict` throws on them, and `required "arg"` +// additionally throws when the first argument is missing (implies +// skip_null). + +macro_rules! cmd { + ($fn_name:ident, $name:literal, $command:literal, $($argname:literal,)* $state:ident $(,)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -180,84 +183,23 @@ macro_rules! cmd_noargs { compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( this, $name, )?; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } - }; -} - -macro_rules! cmd_key { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } - }; -} - -macro_rules! cmd_key_varargs { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - if frame.argument(0).is_undefined_or_null() { - return Err(global.throw_missing_arguments_value(&[$arg0_name])); - } - - let arguments = frame.arguments(); - let mut args: Vec = Vec::with_capacity(arguments.len()); - for arg in arguments { - if arg.is_undefined_or_null() { - continue; - } - - let Some(another) = from_js(global, *arg)? else { - return Err(global.throw_invalid_argument_type( - bname($name), - "additional arguments", - "string or buffer", - )); - }; - args.push(another); - } + #[allow(unused_mut)] + let mut arg_index = 0; + let args = [$( + { + let Some(arg) = from_js(global, frame.argument(arg_index))? else { + return Err(global.throw_invalid_argument_type( + bname($name), + $argname, + "string or buffer", + )); + }; + arg_index += 1; + arg + }, + )*]; + let _ = arg_index; send_cmd( this, global, @@ -271,130 +213,17 @@ macro_rules! cmd_key_varargs { }; } -macro_rules! cmd_key_value { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } +macro_rules! cmd_varargs { + ($fn_name:ident, $name:literal, $command:literal, required $arg0_name:literal, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state, $arg0_name); }; -} - -macro_rules! cmd_key_value_value2 { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $arg2_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - let Some(value2) = from_js(global, frame.argument(2))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg2_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value, value2]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, skip_null, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state); }; -} - -macro_rules! cmd_strings_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let mut args: Vec = Vec::with_capacity(frame.arguments().len()); - - for arg in frame.arguments() { - let Some(another) = from_js(global, *arg)? else { - return Err(global.throw_invalid_argument_type( - bname($name), - "additional arguments", - "string or buffer", - )); - }; - args.push(another); - } - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&args), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, strict, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, false, $state); }; -} - -macro_rules! cmd_key_value_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { + (@impl $fn_name:ident, $name:literal, $command:literal, $skip_null:literal, $state:ident $(, $arg0_name:literal)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -405,11 +234,20 @@ macro_rules! cmd_key_value_varargs { this, $name, )?; - let mut args: Vec = Vec::with_capacity(frame.arguments().len()); + $( + if frame.argument(0).is_undefined_or_null() { + return Err(global.throw_missing_arguments_value(&[$arg0_name])); + } + )? - for arg in frame.arguments() { - if arg.is_undefined_or_null() { - continue; + let arguments = frame.arguments(); + let mut args: Vec = Vec::with_capacity(arguments.len()); + + for arg in arguments { + if $skip_null { + if arg.is_undefined_or_null() { + continue; + } } let Some(another) = from_js(global, *arg)? else { @@ -1118,27 +956,45 @@ impl JSValkeyClient { Self::hset_impl(this, global, frame, b"HMSET") } - cmd_key_varargs!(hdel, b"hdel", "HDEL", "key", NotSubscriber); - cmd_key_varargs!( + cmd_varargs!(hdel, b"hdel", "HDEL", required "key", NotSubscriber); + cmd_varargs!( hrandfield, b"hrandfield", "HRANDFIELD", - "key", + required "key", + NotSubscriber + ); + cmd_varargs!(hscan, b"hscan", "HSCAN", required "key", NotSubscriber); + cmd_varargs!(hgetdel, b"hgetdel", "HGETDEL", strict, NotSubscriber); + cmd_varargs!(hgetex, b"hgetex", "HGETEX", strict, NotSubscriber); + cmd_varargs!(hsetex, b"hsetex", "HSETEX", strict, NotSubscriber); + cmd_varargs!(hexpire, b"hexpire", "HEXPIRE", strict, NotSubscriber); + cmd_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", strict, NotSubscriber); + cmd_varargs!( + hexpiretime, + b"hexpiretime", + "HEXPIRETIME", + strict, NotSubscriber ); - cmd_key_varargs!(hscan, b"hscan", "HSCAN", "key", NotSubscriber); - cmd_strings_varargs!(hgetdel, b"hgetdel", "HGETDEL", NotSubscriber); - cmd_strings_varargs!(hgetex, b"hgetex", "HGETEX", NotSubscriber); - cmd_strings_varargs!(hsetex, b"hsetex", "HSETEX", NotSubscriber); - cmd_strings_varargs!(hexpire, b"hexpire", "HEXPIRE", NotSubscriber); - cmd_strings_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hexpiretime, b"hexpiretime", "HEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpersist, b"hpersist", "HPERSIST", NotSubscriber); - cmd_strings_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", NotSubscriber); - cmd_strings_varargs!(hpexpireat, b"hpexpireat", "HPEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hpexpiretime, b"hpexpiretime", "HPEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpttl, b"hpttl", "HPTTL", NotSubscriber); - cmd_strings_varargs!(httl, b"httl", "HTTL", NotSubscriber); + cmd_varargs!(hpersist, b"hpersist", "HPERSIST", strict, NotSubscriber); + cmd_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", strict, NotSubscriber); + cmd_varargs!( + hpexpireat, + b"hpexpireat", + "HPEXPIREAT", + strict, + NotSubscriber + ); + cmd_varargs!( + hpexpiretime, + b"hpexpiretime", + "HPEXPIRETIME", + strict, + NotSubscriber + ); + cmd_varargs!(hpttl, b"hpttl", "HPTTL", strict, NotSubscriber); + cmd_varargs!(httl, b"httl", "HTTL", strict, NotSubscriber); #[bun_jsc::host_fn(method)] pub fn hsetnx(this: &Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { @@ -1217,12 +1073,12 @@ impl JSValkeyClient { ) } - cmd_key!(bitcount, b"bitcount", "BITCOUNT", "key", NotSubscriber); - cmd_strings_varargs!(blmove, b"blmove", "BLMOVE", NotSubscriber); - cmd_strings_varargs!(blmpop, b"blmpop", "BLMPOP", NotSubscriber); - cmd_strings_varargs!(blpop, b"blpop", "BLPOP", NotSubscriber); - cmd_strings_varargs!(brpop, b"brpop", "BRPOP", NotSubscriber); - cmd_key_value_value2!( + cmd!(bitcount, b"bitcount", "BITCOUNT", "key", NotSubscriber); + cmd_varargs!(blmove, b"blmove", "BLMOVE", strict, NotSubscriber); + cmd_varargs!(blmpop, b"blmpop", "BLMPOP", strict, NotSubscriber); + cmd_varargs!(blpop, b"blpop", "BLPOP", strict, NotSubscriber); + cmd_varargs!(brpop, b"brpop", "BRPOP", strict, NotSubscriber); + cmd!( brpoplpush, b"brpoplpush", "BRPOPLPUSH", @@ -1231,8 +1087,8 @@ impl JSValkeyClient { "timeout", NotSubscriber ); - cmd_key_value!(getbit, b"getbit", "GETBIT", "key", "offset", NotSubscriber); - cmd_key_value_value2!( + cmd!(getbit, b"getbit", "GETBIT", "key", "offset", NotSubscriber); + cmd!( setbit, b"setbit", "SETBIT", @@ -1241,7 +1097,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( getrange, b"getrange", "GETRANGE", @@ -1250,7 +1106,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value_value2!( + cmd!( setrange, b"setrange", "SETRANGE", @@ -1259,8 +1115,8 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key!(dump, b"dump", "DUMP", "key", NotSubscriber); - cmd_key_value!( + cmd!(dump, b"dump", "DUMP", "key", NotSubscriber); + cmd!( expireat, b"expireat", "EXPIREAT", @@ -1268,28 +1124,28 @@ impl JSValkeyClient { "timestamp", NotSubscriber ); - cmd_key!( + cmd!( expiretime, b"expiretime", "EXPIRETIME", "key", NotSubscriber ); - cmd_key!(getdel, b"getdel", "GETDEL", "key", NotSubscriber); - cmd_strings_varargs!(getex, b"getex", "GETEX", NotSubscriber); - cmd_key!(hgetall, b"hgetall", "HGETALL", "key", NotSubscriber); - cmd_key!(hkeys, b"hkeys", "HKEYS", "key", NotSubscriber); - cmd_key!(hlen, b"hlen", "HLEN", "key", NotSubscriber); - cmd_key!(hvals, b"hvals", "HVALS", "key", NotSubscriber); - cmd_key!(keys, b"keys", "KEYS", "key", NotSubscriber); - cmd_key_value!(lindex, b"lindex", "LINDEX", "key", "index", NotSubscriber); - cmd_strings_varargs!(linsert, b"linsert", "LINSERT", NotSubscriber); - cmd_key!(llen, b"llen", "LLEN", "key", NotSubscriber); - cmd_strings_varargs!(lmove, b"lmove", "LMOVE", NotSubscriber); - cmd_strings_varargs!(lmpop, b"lmpop", "LMPOP", NotSubscriber); - cmd_key_varargs!(lpop, b"lpop", "LPOP", "key", NotSubscriber); - cmd_strings_varargs!(lpos, b"lpos", "LPOS", NotSubscriber); - cmd_key_value_value2!( + cmd!(getdel, b"getdel", "GETDEL", "key", NotSubscriber); + cmd_varargs!(getex, b"getex", "GETEX", strict, NotSubscriber); + cmd!(hgetall, b"hgetall", "HGETALL", "key", NotSubscriber); + cmd!(hkeys, b"hkeys", "HKEYS", "key", NotSubscriber); + cmd!(hlen, b"hlen", "HLEN", "key", NotSubscriber); + cmd!(hvals, b"hvals", "HVALS", "key", NotSubscriber); + cmd!(keys, b"keys", "KEYS", "key", NotSubscriber); + cmd!(lindex, b"lindex", "LINDEX", "key", "index", NotSubscriber); + cmd_varargs!(linsert, b"linsert", "LINSERT", strict, NotSubscriber); + cmd!(llen, b"llen", "LLEN", "key", NotSubscriber); + cmd_varargs!(lmove, b"lmove", "LMOVE", strict, NotSubscriber); + cmd_varargs!(lmpop, b"lmpop", "LMPOP", strict, NotSubscriber); + cmd_varargs!(lpop, b"lpop", "LPOP", required "key", NotSubscriber); + cmd_varargs!(lpos, b"lpos", "LPOS", strict, NotSubscriber); + cmd!( lrange, b"lrange", "LRANGE", @@ -1298,7 +1154,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lrem, b"lrem", "LREM", @@ -1307,7 +1163,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lset, b"lset", "LSET", @@ -1316,7 +1172,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( ltrim, b"ltrim", "LTRIM", @@ -1325,8 +1181,8 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key!(persist, b"persist", "PERSIST", "key", NotSubscriber); - cmd_key_value!( + cmd!(persist, b"persist", "PERSIST", "key", NotSubscriber); + cmd!( pexpire, b"pexpire", "PEXPIRE", @@ -1334,7 +1190,7 @@ impl JSValkeyClient { "milliseconds", NotSubscriber ); - cmd_key_value!( + cmd!( pexpireat, b"pexpireat", "PEXPIREAT", @@ -1342,17 +1198,17 @@ impl JSValkeyClient { "milliseconds-timestamp", NotSubscriber ); - cmd_key!( + cmd!( pexpiretime, b"pexpiretime", "PEXPIRETIME", "key", NotSubscriber ); - cmd_key!(pttl, b"pttl", "PTTL", "key", NotSubscriber); - cmd_noargs!(randomkey, b"randomkey", "RANDOMKEY", NotSubscriber); - cmd_key_varargs!(rpop, b"rpop", "RPOP", "key", NotSubscriber); - cmd_key_value!( + cmd!(pttl, b"pttl", "PTTL", "key", NotSubscriber); + cmd!(randomkey, b"randomkey", "RANDOMKEY", NotSubscriber); + cmd_varargs!(rpop, b"rpop", "RPOP", required "key", NotSubscriber); + cmd!( rpoplpush, b"rpoplpush", "RPOPLPUSH", @@ -1360,21 +1216,51 @@ impl JSValkeyClient { "destination", NotSubscriber ); - cmd_strings_varargs!(scan, b"scan", "SCAN", NotSubscriber); - cmd_key!(scard, b"scard", "SCARD", "key", NotSubscriber); - cmd_strings_varargs!(sdiff, b"sdiff", "SDIFF", NotSubscriber); - cmd_strings_varargs!(sdiffstore, b"sdiffstore", "SDIFFSTORE", NotSubscriber); - cmd_strings_varargs!(sinter, b"sinter", "SINTER", NotSubscriber); - cmd_strings_varargs!(sintercard, b"sintercard", "SINTERCARD", NotSubscriber); - cmd_strings_varargs!(sinterstore, b"sinterstore", "SINTERSTORE", NotSubscriber); - cmd_strings_varargs!(smismember, b"smismember", "SMISMEMBER", NotSubscriber); - cmd_strings_varargs!(sscan, b"sscan", "SSCAN", NotSubscriber); - cmd_key!(strlen, b"strlen", "STRLEN", "key", NotSubscriber); - cmd_strings_varargs!(sunion, b"sunion", "SUNION", NotSubscriber); - cmd_strings_varargs!(sunionstore, b"sunionstore", "SUNIONSTORE", NotSubscriber); - cmd_key!(r#type, b"type", "TYPE", "key", NotSubscriber); - cmd_key!(zcard, b"zcard", "ZCARD", "key", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(scan, b"scan", "SCAN", strict, NotSubscriber); + cmd!(scard, b"scard", "SCARD", "key", NotSubscriber); + cmd_varargs!(sdiff, b"sdiff", "SDIFF", strict, NotSubscriber); + cmd_varargs!( + sdiffstore, + b"sdiffstore", + "SDIFFSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(sinter, b"sinter", "SINTER", strict, NotSubscriber); + cmd_varargs!( + sintercard, + b"sintercard", + "SINTERCARD", + strict, + NotSubscriber + ); + cmd_varargs!( + sinterstore, + b"sinterstore", + "SINTERSTORE", + strict, + NotSubscriber + ); + cmd_varargs!( + smismember, + b"smismember", + "SMISMEMBER", + strict, + NotSubscriber + ); + cmd_varargs!(sscan, b"sscan", "SSCAN", strict, NotSubscriber); + cmd!(strlen, b"strlen", "STRLEN", "key", NotSubscriber); + cmd_varargs!(sunion, b"sunion", "SUNION", strict, NotSubscriber); + cmd_varargs!( + sunionstore, + b"sunionstore", + "SUNIONSTORE", + strict, + NotSubscriber + ); + cmd!(r#type, b"type", "TYPE", "key", NotSubscriber); + cmd!(zcard, b"zcard", "ZCARD", "key", NotSubscriber); + cmd!( zcount, b"zcount", "ZCOUNT", @@ -1383,7 +1269,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zlexcount, b"zlexcount", "ZLEXCOUNT", @@ -1392,47 +1278,49 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zpopmax, b"zpopmax", "ZPOPMAX", "key", NotSubscriber); - cmd_key_varargs!(zpopmin, b"zpopmin", "ZPOPMIN", "key", NotSubscriber); - cmd_key_varargs!( + cmd_varargs!(zpopmax, b"zpopmax", "ZPOPMAX", required "key", NotSubscriber); + cmd_varargs!(zpopmin, b"zpopmin", "ZPOPMIN", required "key", NotSubscriber); + cmd_varargs!( zrandmember, b"zrandmember", "ZRANDMEMBER", - "key", + required "key", NotSubscriber ); - cmd_strings_varargs!(zrange, b"zrange", "ZRANGE", NotSubscriber); - cmd_strings_varargs!(zrevrange, b"zrevrange", "ZREVRANGE", NotSubscriber); - cmd_strings_varargs!( + cmd_varargs!(zrange, b"zrange", "ZRANGE", strict, NotSubscriber); + cmd_varargs!(zrevrange, b"zrevrange", "ZREVRANGE", strict, NotSubscriber); + cmd_varargs!( zrangebyscore, b"zrangebyscore", "ZRANGEBYSCORE", + strict, NotSubscriber ); - cmd_strings_varargs!( + cmd_varargs!( zrevrangebyscore, b"zrevrangebyscore", "ZREVRANGEBYSCORE", + strict, NotSubscriber ); - cmd_key_varargs!( + cmd_varargs!( zrangebylex, b"zrangebylex", "ZRANGEBYLEX", - "key", + required "key", NotSubscriber ); - cmd_key_varargs!( + cmd_varargs!( zrevrangebylex, b"zrevrangebylex", "ZREVRANGEBYLEX", - "key", + required "key", NotSubscriber ); - cmd_key_value!(append, b"append", "APPEND", "key", "value", NotSubscriber); - cmd_key_value!(getset, b"getset", "GETSET", "key", "value", NotSubscriber); - cmd_key_value!(hget, b"hget", "HGET", "key", "field", NotSubscriber); - cmd_key_value!( + cmd!(append, b"append", "APPEND", "key", "value", NotSubscriber); + cmd!(getset, b"getset", "GETSET", "key", "value", NotSubscriber); + cmd!(hget, b"hget", "HGET", "key", "field", NotSubscriber); + cmd!( incrby, b"incrby", "INCRBY", @@ -1440,7 +1328,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( incrbyfloat, b"incrbyfloat", "INCRBYFLOAT", @@ -1448,7 +1336,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( decrby, b"decrby", "DECRBY", @@ -1456,13 +1344,13 @@ impl JSValkeyClient { "decrement", NotSubscriber ); - cmd_key_value_varargs!(lpush, b"lpush", "LPUSH", NotSubscriber); - cmd_key_value_varargs!(lpushx, b"lpushx", "LPUSHX", NotSubscriber); - cmd_key_value!(pfadd, b"pfadd", "PFADD", "key", "value", NotSubscriber); - cmd_key_value_varargs!(rpush, b"rpush", "RPUSH", NotSubscriber); - cmd_key_value_varargs!(rpushx, b"rpushx", "RPUSHX", NotSubscriber); - cmd_key_value!(setnx, b"setnx", "SETNX", "key", "value", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(lpush, b"lpush", "LPUSH", skip_null, NotSubscriber); + cmd_varargs!(lpushx, b"lpushx", "LPUSHX", skip_null, NotSubscriber); + cmd!(pfadd, b"pfadd", "PFADD", "key", "value", NotSubscriber); + cmd_varargs!(rpush, b"rpush", "RPUSH", skip_null, NotSubscriber); + cmd_varargs!(rpushx, b"rpushx", "RPUSHX", skip_null, NotSubscriber); + cmd!(setnx, b"setnx", "SETNX", "key", "value", NotSubscriber); + cmd!( setex, b"setex", "SETEX", @@ -1471,7 +1359,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( psetex, b"psetex", "PSETEX", @@ -1480,8 +1368,8 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value!(zscore, b"zscore", "ZSCORE", "key", "value", NotSubscriber); - cmd_key_value_value2!( + cmd!(zscore, b"zscore", "ZSCORE", "key", "value", NotSubscriber); + cmd!( zincrby, b"zincrby", "ZINCRBY", @@ -1490,27 +1378,51 @@ impl JSValkeyClient { "member", NotSubscriber ); - cmd_key_value_varargs!(zmscore, b"zmscore", "ZMSCORE", NotSubscriber); - cmd_strings_varargs!(zadd, b"zadd", "ZADD", NotSubscriber); - cmd_strings_varargs!(zscan, b"zscan", "ZSCAN", NotSubscriber); - cmd_strings_varargs!(zdiff, b"zdiff", "ZDIFF", NotSubscriber); - cmd_strings_varargs!(zdiffstore, b"zdiffstore", "ZDIFFSTORE", NotSubscriber); - cmd_strings_varargs!(zinter, b"zinter", "ZINTER", NotSubscriber); - cmd_strings_varargs!(zintercard, b"zintercard", "ZINTERCARD", NotSubscriber); - cmd_strings_varargs!(zinterstore, b"zinterstore", "ZINTERSTORE", NotSubscriber); - cmd_strings_varargs!(zunion, b"zunion", "ZUNION", NotSubscriber); - cmd_strings_varargs!(zunionstore, b"zunionstore", "ZUNIONSTORE", NotSubscriber); - cmd_strings_varargs!(zmpop, b"zmpop", "ZMPOP", NotSubscriber); - cmd_strings_varargs!(bzmpop, b"bzmpop", "BZMPOP", NotSubscriber); - cmd_strings_varargs!(bzpopmin, b"bzpopmin", "BZPOPMIN", NotSubscriber); - cmd_strings_varargs!(bzpopmax, b"bzpopmax", "BZPOPMAX", NotSubscriber); - cmd_key_varargs!(del, b"del", "DEL", "key", NotSubscriber); - cmd_key_varargs!(mget, b"mget", "MGET", "key", NotSubscriber); - cmd_strings_varargs!(mset, b"mset", "MSET", NotSubscriber); - cmd_strings_varargs!(msetnx, b"msetnx", "MSETNX", NotSubscriber); - cmd_strings_varargs!(script, b"script", "SCRIPT", NotSubscriber); - cmd_strings_varargs!(select, b"select", "SELECT", NotSubscriber); - cmd_key_value!( + cmd_varargs!(zmscore, b"zmscore", "ZMSCORE", skip_null, NotSubscriber); + cmd_varargs!(zadd, b"zadd", "ZADD", strict, NotSubscriber); + cmd_varargs!(zscan, b"zscan", "ZSCAN", strict, NotSubscriber); + cmd_varargs!(zdiff, b"zdiff", "ZDIFF", strict, NotSubscriber); + cmd_varargs!( + zdiffstore, + b"zdiffstore", + "ZDIFFSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zinter, b"zinter", "ZINTER", strict, NotSubscriber); + cmd_varargs!( + zintercard, + b"zintercard", + "ZINTERCARD", + strict, + NotSubscriber + ); + cmd_varargs!( + zinterstore, + b"zinterstore", + "ZINTERSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zunion, b"zunion", "ZUNION", strict, NotSubscriber); + cmd_varargs!( + zunionstore, + b"zunionstore", + "ZUNIONSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zmpop, b"zmpop", "ZMPOP", strict, NotSubscriber); + cmd_varargs!(bzmpop, b"bzmpop", "BZMPOP", strict, NotSubscriber); + cmd_varargs!(bzpopmin, b"bzpopmin", "BZPOPMIN", strict, NotSubscriber); + cmd_varargs!(bzpopmax, b"bzpopmax", "BZPOPMAX", strict, NotSubscriber); + cmd_varargs!(del, b"del", "DEL", required "key", NotSubscriber); + cmd_varargs!(mget, b"mget", "MGET", required "key", NotSubscriber); + cmd_varargs!(mset, b"mset", "MSET", strict, NotSubscriber); + cmd_varargs!(msetnx, b"msetnx", "MSETNX", strict, NotSubscriber); + cmd_varargs!(script, b"script", "SCRIPT", strict, NotSubscriber); + cmd_varargs!(select, b"select", "SELECT", strict, NotSubscriber); + cmd!( spublish, b"spublish", "SPUBLISH", @@ -1547,7 +1459,7 @@ impl JSValkeyClient { ) } - cmd_key_value_value2!( + cmd!( substr, b"substr", "SUBSTR", @@ -1556,7 +1468,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value!( + cmd!( hstrlen, b"hstrlen", "HSTRLEN", @@ -1564,10 +1476,16 @@ impl JSValkeyClient { "field", NotSubscriber ); - cmd_key_varargs!(zrank, b"zrank", "ZRANK", "key", NotSubscriber); - cmd_strings_varargs!(zrangestore, b"zrangestore", "ZRANGESTORE", NotSubscriber); - cmd_key_varargs!(zrem, b"zrem", "ZREM", "key", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(zrank, b"zrank", "ZRANK", required "key", NotSubscriber); + cmd_varargs!( + zrangestore, + b"zrangestore", + "ZRANGESTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zrem, b"zrem", "ZREM", required "key", NotSubscriber); + cmd!( zremrangebylex, b"zremrangebylex", "ZREMRANGEBYLEX", @@ -1576,7 +1494,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyrank, b"zremrangebyrank", "ZREMRANGEBYRANK", @@ -1585,7 +1503,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyscore, b"zremrangebyscore", "ZREMRANGEBYSCORE", @@ -1594,15 +1512,27 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zrevrank, b"zrevrank", "ZREVRANK", "key", NotSubscriber); - cmd_strings_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", DontCare); - cmd_strings_varargs!(punsubscribe, b"punsubscribe", "PUNSUBSCRIBE", DontCare); - cmd_strings_varargs!(pubsub, b"pubsub", "PUBSUB", DontCare); - cmd_strings_varargs!(copy, b"copy", "COPY", NotSubscriber); - cmd_key_varargs!(unlink, b"unlink", "UNLINK", "key", NotSubscriber); - cmd_key_varargs!(touch, b"touch", "TOUCH", "key", NotSubscriber); - cmd_key_value!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); - cmd_key_value!( + cmd_varargs!( + zrevrank, + b"zrevrank", + "ZREVRANK", + required "key", + NotSubscriber + ); + cmd_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", strict, DontCare); + cmd_varargs!( + punsubscribe, + b"punsubscribe", + "PUNSUBSCRIBE", + strict, + DontCare + ); + cmd_varargs!(pubsub, b"pubsub", "PUBSUB", strict, DontCare); + cmd_varargs!(copy, b"copy", "COPY", strict, NotSubscriber); + cmd_varargs!(unlink, b"unlink", "UNLINK", required "key", NotSubscriber); + cmd_varargs!(touch, b"touch", "TOUCH", required "key", NotSubscriber); + cmd!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); + cmd!( renamenx, b"renamenx", "RENAMENX", diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index e1e4441351d..9115c5467e6 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -54,7 +54,7 @@ use bun_core::{String as BunString, Tag as BunStringTag, ZigStringSlice}; use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType}; use bun_http_jsc::method_jsc; use bun_http_types::Method::Method; -use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _}; +use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _, UrlJsc as _}; use bun_paths::{self, PathBuffer}; use bun_sys::FdExt as _; // `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate diff --git a/src/sourcemap_jsc/CodeCoverage.rs b/src/sourcemap_jsc/CodeCoverage.rs index 4211602d374..4b79e381f83 100644 --- a/src/sourcemap_jsc/CodeCoverage.rs +++ b/src/sourcemap_jsc/CodeCoverage.rs @@ -516,6 +516,25 @@ impl ByteRangeMapping { let line_count: u32; + // Resolves a byte offset to a zero-based (line, column) pair, or `None` + // when the offset does not land strictly after a known line start. + let resolve_line = |byte_offset: usize| -> Option<(u32, usize)> { + let new_line_index = LineOffsetTable::find_index( + line_starts, + Loc { + start: i32::try_from(byte_offset).expect("int cast"), + }, + )?; + let line_start_byte_offset = line_starts[new_line_index]; + if (line_start_byte_offset as usize) >= byte_offset { + return None; + } + Some(( + u32::try_from(new_line_index).expect("int cast"), + byte_offset.saturating_sub(line_start_byte_offset as usize), + )) + }; + if ignore_sourcemap || parsed_mappings_.is_none() { line_count = line_starts.len() as u32; executable_lines = Bitset::init_empty(line_count as usize)?; @@ -538,20 +557,9 @@ impl ByteRangeMapping { let has_executed = block.has_executed || block.execution_count > 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((line, _)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { - continue; - } - - let line: u32 = u32::try_from(new_line_index).expect("int cast"); min_line = min_line.min(line); max_line = max_line.max(line); @@ -587,20 +595,9 @@ impl ByteRangeMapping { let mut max_line: u32 = 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((line, _)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { - continue; - } - - let line: u32 = u32::try_from(new_line_index).expect("int cast"); min_line = min_line.min(line); max_line = max_line.max(line); } @@ -636,6 +633,26 @@ impl ByteRangeMapping { let mut cur_: Option = parsed_mapping.internal_cursor(); + // Maps a generated (line, column) to the original zero-based line, + // or `None` when no in-range original mapping exists. + let mut map_to_original = |line: u32, column: usize| -> Option { + let generated_line = + Ordinal::from_zero_based(i32::try_from(line).expect("int cast")); + let generated_column = + Ordinal::from_zero_based(i32::try_from(column).expect("int cast")); + let point: bun_sourcemap::Mapping = if let Some(c) = cur_.as_mut() { + c.move_to(generated_line, generated_column) + } else { + parsed_mapping.find_mapping(generated_line, generated_column) + }?; + if point.original.lines.zero_based() < 0 { + return None; + } + let original_line: u32 = + u32::try_from(point.original.lines.zero_based()).expect("int cast"); + (original_line < line_count).then_some(original_line) + }; + for (i, block) in blocks.iter().enumerate() { if block.end_offset < 0 || block.start_offset < 0 { continue; // does not map to anything @@ -650,60 +667,21 @@ impl ByteRangeMapping { let has_executed = block.has_executed || block.execution_count > 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((generated_line, column_position)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { + let Some(line) = map_to_original(generated_line, column_position) else { continue; - } - let column_position = - byte_offset.saturating_sub(line_start_byte_offset as usize); - - let found: Option = if let Some(c) = cur_.as_mut() { - c.move_to( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) - } else { - parsed_mapping.find_mapping( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) }; - if let Some(point) = found.as_ref() { - if point.original.lines.zero_based() < 0 { - continue; - } - - let line: u32 = - u32::try_from(point.original.lines.zero_based()).expect("int cast"); - if line >= line_count { - continue; - } - executable_lines.set(line as usize); - if has_executed { - lines_which_have_executed.set(line as usize); - line_hits_slice[line as usize] += 1; - } - - min_line = min_line.min(line); - max_line = max_line.max(line); + executable_lines.set(line as usize); + if has_executed { + lines_which_have_executed.set(line as usize); + line_hits_slice[line as usize] += 1; } + + min_line = min_line.min(line); + max_line = max_line.max(line); } if min_line != u32::MAX { @@ -731,54 +709,14 @@ impl ByteRangeMapping { let mut max_line: u32 = 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((generated_line, column_position)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { + let Some(line) = map_to_original(generated_line, column_position) else { continue; - } - - let column_position = - byte_offset.saturating_sub(line_start_byte_offset as usize); - - let found: Option = if let Some(c) = cur_.as_mut() { - c.move_to( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) - } else { - parsed_mapping.find_mapping( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) }; - if let Some(point) = found { - if point.original.lines.zero_based() < 0 { - continue; - } - - let line: u32 = - u32::try_from(point.original.lines.zero_based()).expect("int cast"); - if line >= line_count { - continue; - } - min_line = min_line.min(line); - max_line = max_line.max(line); - } + min_line = min_line.min(line); + max_line = max_line.max(line); } // no sourcemaps? ignore it