diff --git a/Cargo.lock b/Cargo.lock index 5c8ed1083cc9..132bda70c628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2076,6 +2076,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 01e4d563a11d..c2dc59b1e55d 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -155,7 +155,7 @@ let joined = resolve_path::join_string_buf::(&mut *buf, &[a, b] `bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows and the narrow (`u8`) variant on POSIX. -## URL Parsing (`bun_jsc::URL`) +## URL Parsing (`bun_url::whatwg::URL`, re-exported as `bun_jsc::URL`) WHATWG-compliant, backed by WebKit's URL parser. Returns `None` for invalid input. @@ -169,14 +169,15 @@ let url = URL::from_utf8(href)?; // Option> url.protocol() // bun_core::String url.pathname() // bun_core::String url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!) +url.hostname() // bun_core::String — the host WITH the port (opposite of JS `hostname`!) url.port() // u32 (u32::MAX = unset; otherwise u16 range) ``` -`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url` -do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes -`hostname()`, which returns the host WITH the port (also the opposite of JS -`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname` -are effectively swapped relative to their JS namesakes. +`URL::file_url_from_string` / `URL::path_from_file_url` (and the free +`bun_url::href_from_string` / `bun_url::join`) do whole-string conversions and +are usable from JSC-free crates. The entry points that take a `JSValue` +(`href_from_js`, `from_js`) live on the `bun_jsc::UrlJsc` extension trait, so +callers need `use bun_jsc::UrlJsc as _;`. ## MIME Types (`bun_http_types::MimeType`) diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index 4d6e8a203f53..e0c3d7487860 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -584,9 +584,7 @@ impl<'a> Run<'a> { pub(crate) 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/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index 5bfc14267a0c..af00159f25ed 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -750,6 +750,116 @@ impl AsyncModule { }); } + /// 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. @@ -863,32 +973,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", @@ -896,63 +982,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(()) } @@ -1075,50 +1109,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", @@ -1130,45 +1126,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 29152e29c5cf..47fb83dfc141 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -697,10 +697,8 @@ impl<'a> TablePrinter<'a> { let offset = cell_text.len(); 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)); value_formatter.format::(tag, cell_text, value, self.global_object)?; let text = &cell_text[offset..]; @@ -1398,7 +1396,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()); @@ -1474,8 +1472,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)?; @@ -1496,8 +1494,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)?; @@ -1526,7 +1524,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::*; @@ -1968,147 +1966,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) - } - pub(crate) 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, } } } @@ -2135,37 +2006,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() }); } @@ -2174,15 +2045,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, }); } @@ -2194,17 +2067,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, }); } _ => {} @@ -2213,8 +2087,9 @@ pub mod formatter { if js_type == jsc::JSType::DOMWrapper { return Ok(TagResult { - tag: TagPayload::Private, + tag: Tag::Private, cell: js_type, + custom: None, }); } @@ -2225,8 +2100,9 @@ pub mod formatter { { if value.is_class(global_this) { return Ok(TagResult { - tag: TagPayload::Class, + tag: Tag::Class, cell: js_type, + custom: None, }); } @@ -2238,11 +2114,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, }); } @@ -2251,8 +2128,9 @@ pub mod formatter { return Tag::get(value.get_proxy_target(), global_this); } return Ok(TagResult { - tag: TagPayload::GlobalObject, + tag: Tag::GlobalObject, cell: js_type, + custom: None, }); } @@ -2277,8 +2155,9 @@ pub mod formatter { global_this, )? { return Ok(TagResult { - tag: TagPayload::JSX, + tag: Tag::JSX, cell: js_type, + custom: None, }); } } @@ -2286,24 +2165,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 @@ -2312,43 +2191,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 @@ -2371,18 +2238,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, + }) } } @@ -2405,11 +2276,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; @@ -2486,11 +2353,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 @@ -2638,11 +2502,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 => { @@ -2685,6 +2546,17 @@ pub mod formatter { } impl<'w> WrappedWriter<'w> { + pub(crate) 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. @@ -3004,11 +2876,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} ")); @@ -3164,11 +3033,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::(); } @@ -3455,11 +3321,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{}", @@ -3474,11 +3336,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{}", @@ -3497,11 +3355,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: "); @@ -3522,11 +3376,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()); @@ -3541,11 +3391,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!( "{}{}", @@ -3600,11 +3446,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 { @@ -3654,11 +3496,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(), @@ -3711,11 +3549,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!( @@ -3736,11 +3570,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()); @@ -3762,16 +3592,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)?; @@ -3785,10 +3606,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; @@ -3799,10 +3620,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; @@ -3814,26 +3635,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 { @@ -3888,11 +3717,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()); @@ -3955,16 +3780,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`, @@ -3994,31 +3810,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 { @@ -4033,16 +3849,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); @@ -4051,29 +3858,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 { @@ -4088,11 +3899,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")); @@ -4134,11 +3941,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); @@ -4170,16 +3973,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)?; @@ -4191,10 +3985,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; @@ -4204,9 +3998,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; @@ -4215,10 +4009,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; @@ -4260,11 +4054,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)?; @@ -4318,16 +4108,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)?; @@ -4381,11 +4162,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()); @@ -4426,9 +4203,9 @@ pub mod formatter { "... N more items".len(), format_args!( "{}... {} more items{}", - pf!(""), + pfmt!("", C), len - u64::from(i), - pf!("") + pfmt!("", C) ), ); break; @@ -4453,7 +4230,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)); @@ -4461,9 +4238,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4484,11 +4261,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()); @@ -4515,7 +4288,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)); @@ -4523,9 +4296,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4555,11 +4328,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); } } @@ -4884,12 +4653,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; @@ -4928,9 +4691,9 @@ pub mod formatter { let _ = writeln!( writer_, "{}{}{} {{", - pf!(""), + pfmt!("", C), event_tag_name, - pf!("") + pfmt!("", C) ); { self.indent += 1; @@ -4946,23 +4709,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) ); } @@ -4976,9 +4739,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())?; @@ -5001,9 +4764,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)? @@ -5028,9 +4791,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())?; @@ -5064,22 +4827,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 @@ -5117,13 +4871,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")? { @@ -5147,11 +4901,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; } @@ -5207,10 +4957,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 { @@ -5221,11 +4971,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()); @@ -5256,12 +5002,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)?; @@ -5302,12 +5047,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) @@ -5350,12 +5093,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) @@ -5373,13 +5114,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">"); } @@ -5487,11 +5228,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() { @@ -5507,9 +5243,9 @@ pub mod formatter { let _ = write!( writer_, "{}[{} ...]{}", - pf!(""), + pfmt!("", C), display_name, - pf!("") + pfmt!("", C) ); Ok(()) } @@ -5559,11 +5295,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(); @@ -5702,10 +5434,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) } /// Format a single value into `writer`, propagating a JS exception diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index 36c2dc07e26d..67fa8981ed48 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -3,102 +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__username(url: &URL) -> String; - safe fn URL__password(url: &URL) -> String; - safe fn URL__host(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__getFileURLString(input: &mut String) -> String; - safe fn URL__pathFromFileURL(input: &mut String) -> String; } -impl URL { - 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 username(&self) -> String { - URL__username(self) - } - - pub fn password(&self) -> String { - URL__password(self) - } - - /// Returns the host WITHOUT the port. - /// - /// Note that this does NOT match JS behavior, which returns the host with the port. The - /// with-port form lives on the JSC-free shim as `bun_url::whatwg::URL::hostname`. - /// - /// ```text - /// URL("http://example.com:8080").host() => "example.com" - /// ``` - pub fn host(&self) -> String { - URL__host(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 0ee8a9c4075e..d2e62cf61269 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -2768,17 +2768,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], - ) -> crate::CrateResult<*mut JSInternalPromise> { - 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 { @@ -2800,12 +2797,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(); let _ = 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], + ) -> crate::CrateResult<*mut JSInternalPromise> { + 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)) } } @@ -4869,36 +4878,9 @@ impl VirtualMachine { entry_path: &[u8], ) -> crate::CrateResult<*mut JSInternalPromise> { 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(); - let _ = self.wait_for_promise(jsc::AnyPromise::Internal(promise)); + if self.wait_for_entry_point_promise(promise) { + return Ok(promise); } - // Pre-arm the waker so this settled-promise tick cannot park (#36450). self.wakeup(); self.auto_tick(); @@ -5938,7 +5920,7 @@ impl VirtualMachine { ) -> crate::CrateResult<()> { 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; @@ -6403,7 +6385,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/lib.rs b/src/jsc/lib.rs index b59ace1f259f..f864d00ed57d 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -336,8 +336,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) @@ -833,7 +834,7 @@ mod __macro_smoke { // newtypes; the real opaque-FFI structs now live in their own files and are // surfaced here at the crate root. 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}; @@ -1306,8 +1307,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 1c1b7cb9208d..25c73974c4eb 100644 --- a/src/runtime/api/HashObject.rs +++ b/src/runtime/api/HashObject.rs @@ -259,20 +259,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/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index 525f590bdc70..941193cfb4f9 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -97,6 +97,35 @@ 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(), + } +} + pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { bun_jsc::create_host_function_object( global_this, @@ -140,21 +169,8 @@ pub(crate) fn render_to_ansi( ) -> 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, @@ -208,21 +224,8 @@ pub(crate) fn render_to_ansi( fn render_to_html(global_this: &JSGlobalObject, callframe: &CallFrame) -> 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)?; @@ -311,21 +314,8 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< 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)?; @@ -344,9 +334,8 @@ fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult 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)?; @@ -441,9 +417,8 @@ fn render_ast( JSValue::UNDEFINED })?; - if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { - return Err(parser_err_to_js(global_this, err, input.len())); - } + md::render_with_renderer(input, options, renderer.renderer()) + .map_err(|err| parser_err_to_js(global_this, err, input.len()))?; Ok(renderer.get_result()) } diff --git a/src/runtime/api/bun/subprocess/Writable.rs b/src/runtime/api/bun/subprocess/Writable.rs index 23c6205e6a79..c470bbbcd20e 100644 --- a/src/runtime/api/bun/subprocess/Writable.rs +++ b/src/runtime/api/bun/subprocess/Writable.rs @@ -16,6 +16,40 @@ 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. The payload is moved out of `stdio`: a Blob is +/// replaced with `Stdio::Ignore`, an ArrayBuffer is `mem::take`n and leaves an +/// empty `Stdio::ArrayBuffer` behind; callers do not read `stdio` afterwards. +/// 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. @@ -243,29 +277,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) => { @@ -347,29 +364,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 21a816afe646..d57f95fb763f 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -67,6 +67,116 @@ use crate::jsc_hooks::timer_all_mut as timer_all; // CronJobBase — shared base for CronRegisterJob and CronRemoveJob // ============================================================================ +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum CronJobState { + ReadingCrontab, + InstallingCrontab, + #[cfg(target_os = "macos")] + WritingPlist, + BootingOut, + #[cfg(target_os = "macos")] + Bootstrapping, +} + +/// Fields shared by [`CronRegisterJob`] and [`CronRemoveJob`]. +struct CronJobCommon { + promise: jsc::JSPromiseStrong, + // LIFETIMES.tsv: JSC_BORROW → GlobalRef + global: GlobalRef, + poll: KeepAlive, + title: ZString, + + state: CronJobState, + // 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, +} + +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::<()>()), + } + } + + /// Refs the keep-alive (balanced in `finish`) and returns the promise handed + /// back to JS. Runs on the `Box` before it is leaked to `start_*`, which may + /// free the job synchronously. + fn arm(&mut self) -> JSValue { + self.poll.ref_(bun_io::js_vm_ctx()); + self.promise.value() + } + + 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); + } + } + } + + fn note_reader_done(&mut self) { + debug_assert!(self.remaining_fds > 0); + self.remaining_fds -= 1; + } + + fn note_reader_error(&mut self, err: &sys::Error) { + self.note_reader_done(); + self.set_err(format_args!( + "Failed to read process output: {}", + <&'static str>::from(err.get_errno()) + )); + } +} + +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. + } +} + /// 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. @@ -74,61 +184,175 @@ use crate::jsc_hooks::timer_all_mut as timer_all; // making the in-flight dealloc UB; each entry point instead confines its // exclusive access to a temporary `(*this).method(..)` borrow that ends // before any call that may free `this`. -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; +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); - type State: Copy; - #[cfg(all(not(target_os = "macos"), not(windows)))] - const READING_CRONTAB: Self::State; - #[cfg(target_os = "macos")] - const BOOTING_OUT: Self::State; - #[cfg(not(windows))] - fn set_state(&mut self, state: Self::State); - #[cfg(all(not(target_os = "macos"), not(windows)))] - fn stdout_reader_slot(&mut self) -> &mut OutputReader; - #[cfg(target_os = "macos")] - fn title_bytes(&self) -> &[u8]; + /// 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 + } + + /// 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 + } #[cfg(all(not(target_os = "macos"), not(windows)))] - fn prepare_list_crontab(&mut self, this_ptr: *mut core::ffi::c_void) -> Option<*const c_char> - where - Self: BufferedReaderParent, - { - self.set_state(Self::READING_CRONTAB); - *self.stdout_reader_slot() = OutputReader::init::(); - self.stdout_reader_slot().set_parent(this_ptr); + fn prepare_list_crontab(&mut self, this_ptr: *mut core::ffi::c_void) -> Option<*const c_char> { + let b = self.base_mut(); + b.state = CronJobState::ReadingCrontab; + b.stdout_reader = OutputReader::init::(); + b.stdout_reader.set_parent(this_ptr); let crontab_path = find_crontab(); if crontab_path.is_none() { - self.set_err(format_args!("crontab not found in PATH")); + b.set_err(format_args!("crontab not found in PATH")); } crontab_path } + /// Read the captured `crontab -l` output and drop any existing entry for + /// `title`. + #[cfg(not(target_os = "macos"))] + fn filtered_crontab(&mut self) -> Result, bun_alloc::AllocError> { + let b = self.base_mut(); + let existing_content = b.stdout_reader.final_buffer().as_slice(); + let mut result: Vec = Vec::new(); + filter_crontab(existing_content, b.title.as_bytes(), &mut result)?; + Ok(result) + } + + /// Write `content` to a fresh temp file and resolve the argv for + /// `crontab `; returns `(crontab_path, tmp_file_path)`. + #[cfg(not(target_os = "macos"))] + fn prepare_install_crontab( + &mut self, + content: &[u8], + tmp_prefix: &'static str, + ) -> Result<(*const c_char, *const c_char), ()> { + let b = self.base_mut(); + let tmp_path = match make_temp_path(tmp_prefix) { + Ok(p) => p, + Err(_) => { + b.set_err(format_args!("Out of memory")); + return Err(()); + } + }; + let tmp_path_ptr = tmp_path.as_ptr(); + b.tmp_path = Some(tmp_path); + + let file = match File::openat( + Fd::cwd(), + b.tmp_path.as_ref().unwrap(), + sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, + 0o600, + ) { + Ok(f) => f, + Err(_) => { + b.set_err(format_args!("Failed to create temp file")); + return Err(()); + } + }; + if file.write_all(content).is_err() { + let _ = file.close(); // close error is non-actionable + b.set_err(format_args!("Failed to write temp file")); + return Err(()); + } + let _ = file.close(); // close error is non-actionable + + b.state = CronJobState::InstallingCrontab; + // Note: explicit deinit of old reader before reassign — Drop handles it. + b.stdout_reader = OutputReader::init::(); + let Some(crontab_path) = find_crontab() else { + b.set_err(format_args!("crontab not found in PATH")); + return Err(()); + }; + Ok((crontab_path, tmp_path_ptr.cast())) + } + #[cfg(target_os = "macos")] fn prepare_bootout(&mut self) -> Result { - self.set_state(Self::BOOTING_OUT); + let b = self.base_mut(); + b.state = CronJobState::BootingOut; alloc_print_z(format_args!( "gui/{}/bun.cron.{}", get_uid(), - bstr::BStr::new(self.title_bytes()) + bstr::BStr::new(b.title.as_bytes()) )) - .map_err(|_| self.set_err(format_args!("Out of memory"))) + .map_err(|_| b.set_err(format_args!("Out of memory"))) } - fn check_finished(&mut self) -> JobAction; - /// Consumes and frees `this`. - unsafe fn finish(this: *mut Self); - unsafe fn advance_state(this: *mut Self); - - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg_mut().is_none() { - let mut msg = Vec::new(); - let _ = msg.write_fmt(args); - *self.err_msg_mut() = Some(msg); + fn check_finished(&mut self) -> JobAction { + let b = self.base_mut(); + if !b.has_called_process_exit || b.remaining_fds != 0 { + return JobAction::Pending; + } + b.detach_process(); + if b.err_msg.is_some() { + return JobAction::Finish; + } + let Some(status) = b.exit_status.take() else { + return JobAction::Pending; + }; + match status { + Status::Exited(exited) => { + if exited.code != 0 && !self.accepts_nonzero_exit(exited.code) { + // Materialize the trimmed stderr into an owned buffer: + // `final_buffer()` borrows the reader mutably, and + // `set_err` below borrows the base again — copy out so the + // two borrows do not overlap (Windows only; POSIX ignores + // stderr here). + #[cfg(windows)] + let stderr_owned: Vec = bun_core::strings::trim( + self.base_mut().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""; + #[cfg(windows)] + if self.exit_err_override(stderr_output) { + return JobAction::Finish; + } + if !stderr_output.is_empty() { + self.base_mut() + .set_err(format_args!("{}", bstr::BStr::new(stderr_output))); + } else { + self.base_mut() + .set_err(format_args!("Process exited with code {}", exited.code)); + } + return JobAction::Finish; + } + } + Status::Signaled(sig) => { + if self.base().state != CronJobState::BootingOut { + self.base_mut() + .set_err(format_args!("Process killed by signal {}", sig as i32)); + return JobAction::Finish; + } + } + Status::Err(err) => { + self.base_mut().set_err(format_args!( + "Process error: {}", + <&'static str>::from(err.get_errno()) + )); + return JobAction::Finish; + } + Status::Running => return JobAction::Pending, } + JobAction::Advance } unsafe fn maybe_finished(this: *mut Self) { @@ -144,6 +368,30 @@ trait CronJobBase: Sized { } } + /// Consumes and frees `this` (`heap::take`). + unsafe fn finish(this: *mut Self) { + // SAFETY: caller transfers the unique Box leaked in + // cron_register / cron_remove. + let mut job = unsafe { bun_core::heap::take(this) }; + let b = job.base_mut(); + b.poll.unref(bun_io::js_vm_ctx()); + let ev = VirtualMachine::get().event_loop_mut(); + ev.enter(); + 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 _ = 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. + drop(job); + ev.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 @@ -151,29 +399,11 @@ trait CronJobBase: Sized { vm_mut().uv_loop() } - fn note_reader_done(&mut self) { - debug_assert!(*self.remaining_fds_mut() > 0); - *self.remaining_fds_mut() -= 1; - } - - fn note_reader_error(&mut self, err: sys::Error) { - self.note_reader_done(); - if self.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()) - ); - *self.err_msg_mut() = Some(msg); - } - } - /// May free `this` via `maybe_finished`. unsafe fn on_reader_done(this: *mut Self) { // SAFETY: temporary exclusive borrow; ends at this statement, before // `maybe_finished` may free `this`. - unsafe { (*this).note_reader_done() }; + unsafe { (*this).base_mut().note_reader_done() }; // SAFETY: no borrows of `this` remain; `this` is the live heap job. unsafe { Self::maybe_finished(this) }; } @@ -182,22 +412,88 @@ trait CronJobBase: Sized { unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { // SAFETY: temporary exclusive borrow; ends at this statement, before // `maybe_finished` may free `this`. - unsafe { (*this).note_reader_error(err) }; + unsafe { (*this).base_mut().note_reader_error(&err) }; // SAFETY: no borrows of `this` remain; `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: temporary exclusive borrow; ends at this statement, before + // SAFETY: temporary exclusive borrow; ends with this block, before // `maybe_finished` may free `this`. unsafe { - *(*this).has_called_process_exit_mut() = true; - *(*this).exit_status_mut() = Some(status); + let b = (*this).base_mut(); + b.has_called_process_exit = true; + b.exit_status = Some(status); } // SAFETY: no borrows of `this` remain; `this` is the live heap job. unsafe { Self::maybe_finished(this) }; } + + /// 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) }; + } + + /// Spawn `crontab -l` and buffer its output. May free `this`. + /// Raw-ptr receiver: see trait-level note. + #[cfg(all(not(target_os = "macos"), not(windows)))] + unsafe fn start_linux(this: *mut Self) { + // SAFETY: exclusive borrow is confined to `prepare_list_crontab`; it + // ends before the freeing calls below. + let crontab_path = unsafe { (*this).prepare_list_crontab(this.cast()) }; + let Some(crontab_path) = crontab_path else { + // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; + } + + /// Write `content` to a fresh temp file and spawn `crontab ` to + /// install it. May free `this`. Raw-ptr receiver: see trait-level note. + #[cfg(not(target_os = "macos"))] + unsafe fn install_crontab(this: *mut Self, content: &[u8], tmp_prefix: &'static str) { + // SAFETY: exclusive borrow is confined to `prepare_install_crontab`; + // it ends before the freeing calls below. + let prepared = unsafe { (*this).prepare_install_crontab(content, tmp_prefix) }; + let Ok((crontab_path, tmp_path_ptr)) = prepared else { + // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. + return unsafe { Self::finish(this) }; + }; + let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr, core::ptr::null()]; + // SAFETY: no borrows of `this` remain; `spawn_cmd` may free `this`. + 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`. + /// Raw-ptr receiver: see trait-level note. + #[cfg(target_os = "macos")] + unsafe fn spawn_bootout(this: *mut Self) { + // SAFETY: exclusive borrow is confined to `prepare_bootout`; it ends + // before the freeing calls below. + let uid_str = unsafe { (*this).prepare_bootout() }; + let Ok(uid_str) = uid_str else { + // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + drop(uid_str); + } } enum JobAction { @@ -211,46 +507,14 @@ enum JobAction { // ============================================================================ struct CronRegisterJob { - promise: jsc::JSPromiseStrong, - // LIFETIMES.tsv: JSC_BORROW → GlobalRef - global: GlobalRef, - poll: KeepAlive, + base: CronJobCommon, bun_exe: &'static ZStr, abs_path: ZString, /// normalized numeric form for crontab/launchd schedule: ZString, - title: ZString, #[cfg(windows)] parsed_cron: CronExpression, - - state: RegisterState, - // 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 RegisterState { - ReadingCrontab, - #[cfg(not(target_os = "macos"))] - InstallingCrontab, - #[cfg(target_os = "macos")] - WritingPlist, - BootingOut, - #[cfg(target_os = "macos")] - Bootstrapping, } // Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. @@ -260,140 +524,52 @@ 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 CronRegisterJob { - type State = RegisterState; - #[cfg(all(not(target_os = "macos"), not(windows)))] - const READING_CRONTAB: RegisterState = RegisterState::ReadingCrontab; - #[cfg(target_os = "macos")] - const BOOTING_OUT: RegisterState = RegisterState::BootingOut; - #[cfg(not(windows))] - fn set_state(&mut self, state: RegisterState) { - self.state = state; - } - #[cfg(all(not(target_os = "macos"), not(windows)))] - fn stdout_reader_slot(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(target_os = "macos")] - fn title_bytes(&self) -> &[u8] { - self.title.as_bytes() - } - 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 + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; + fn base(&self) -> &CronJobCommon { + &self.base } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base } - fn check_finished(&mut self) -> JobAction { - if !self.has_called_process_exit || self.remaining_fds != 0 { - return JobAction::Pending; - } - if let Some(proc) = self.process.take() { - // SAFETY: `proc` is the intrusive-RC pointer returned by `to_process`. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if self.err_msg.is_some() { - return JobAction::Finish; - } - let Some(status) = self.exit_status.take() else { - return JobAction::Pending; - }; - match status { - Status::Exited(exited) => { - if exited.code != 0 - && !(self.state == RegisterState::ReadingCrontab && exited.code == 1) - && self.state != RegisterState::BootingOut - { - // Materialize the trimmed stderr into an owned buffer: - // `final_buffer()` borrows the reader mutably, and - // `set_err` below needs `&mut self` — copy out so the two - // borrows do not overlap (Windows only; POSIX ignores - // stderr here). - #[cfg(windows)] - let stderr_owned: Vec = bun_core::strings::trim( - self.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""; - // On Windows, detect the SID resolution error and provide - // a clear message instead of the raw schtasks output. - #[cfg(windows)] - { - if self.state == RegisterState::InstallingCrontab - && bun_core::index_of( - stderr_output, - b"No mapping between account names", - ) - .is_some() - { - self.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 JobAction::Finish; - } - } - if !stderr_output.is_empty() { - self.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); - } else { - self.set_err(format_args!("Process exited with code {}", exited.code)); - } - return JobAction::Finish; - } - } - Status::Signaled(sig) => { - if self.state != RegisterState::BootingOut { - self.set_err(format_args!("Process killed by signal {}", sig as i32)); - return JobAction::Finish; - } - } - Status::Err(err) => { - self.set_err(format_args!( - "Process error: {}", - <&'static str>::from(err.get_errno()) - )); - return JobAction::Finish; - } - Status::Running => return JobAction::Pending, + /// 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; } - JobAction::Advance + false } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn advance_state(this: *mut Self) { // SAFETY: shared read of a Copy field; the borrow ends at this statement. - let state = unsafe { (*this).state }; + let state = unsafe { (*this).base.state }; #[cfg(target_os = "macos")] { match state { // SAFETY: no borrows of `this` remain; `this` is the live heap job. - RegisterState::WritingPlist => unsafe { Self::spawn_bootout(this) }, + CronJobState::WritingPlist => unsafe { Self::spawn_bootout(this) }, // SAFETY: no borrows of `this` remain; `this` is the live heap job. - RegisterState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, + CronJobState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. - RegisterState::Bootstrapping => unsafe { Self::finish(this) }, + CronJobState::Bootstrapping => unsafe { Self::finish(this) }, _ => { // SAFETY: temporary exclusive borrow ending at this statement. - unsafe { (*this).set_err(format_args!("Unexpected state")) }; + unsafe { (*this).base.set_err(format_args!("Unexpected state")) }; // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. unsafe { Self::finish(this) }; } @@ -403,151 +579,62 @@ impl CronJobBase for CronRegisterJob { { match state { // SAFETY: no borrows of `this` remain; `this` is the live heap job. - RegisterState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, + CronJobState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. - RegisterState::InstallingCrontab => unsafe { Self::finish(this) }, + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, _ => { // SAFETY: temporary exclusive borrow ending at this statement. - unsafe { (*this).set_err(format_args!("Unexpected state")) }; + unsafe { (*this).base.set_err(format_args!("Unexpected state")) }; // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. unsafe { Self::finish(this) }; } } } } - - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller transfers the unique Box leaked in cron_register. - let mut job = unsafe { bun_core::heap::take(this) }; - job.poll.unref(bun_io::js_vm_ctx()); - let ev = VirtualMachine::get().event_loop_mut(); - ev.enter(); - if let Some(msg) = &job.err_msg { - let _ = job.promise.reject_with_async_stack( - &job.global, - Ok(job - .global - .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), - ); - } else { - let _ = job.promise.resolve(&job.global, JSValue::UNDEFINED); - } - // Drop runs INSIDE the enter/exit scope so Process detach/deref and - // reader teardown observe the entered event-loop state. - drop(job); - ev.exit(); - } } impl CronRegisterJob { - /// 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) }; - } - // -- Linux -- - /// 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: exclusive borrow is confined to `prepare_list_crontab`; it - // ends before the freeing calls below. - let crontab_path = unsafe { (*this).prepare_list_crontab(this.cast()) }; - let Some(crontab_path) = crontab_path else { - // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; - } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn process_crontab_and_install(this: *mut Self) { - // SAFETY: exclusive borrow is confined to `prepare_install_crontab`; - // it ends before the freeing calls below. - let prepared = unsafe { (*this).prepare_install_crontab() }; - let Ok((crontab_path, tmp_path_ptr)) = prepared else { + // SAFETY: exclusive borrow is confined to `crontab_with_entry`; it + // ends before the freeing calls below. + let prepared = unsafe { (*this).crontab_with_entry() }; + let Ok(content) = prepared else { // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. return unsafe { Self::finish(this) }; }; - let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr, core::ptr::null()]; - // SAFETY: no borrows of `this` remain; `spawn_cmd` may free `this`. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + // SAFETY: no borrows of `this` remain; `install_crontab` may free `this`. + unsafe { Self::install_crontab(this, &content, "bun-cron-") }; } #[cfg(not(target_os = "macos"))] - fn prepare_install_crontab(&mut self) -> Result<(*const c_char, *const c_char), ()> { - let existing_content = self.stdout_reader.final_buffer().as_slice(); - let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, self.title.as_bytes(), &mut result).is_err() { - self.set_err(format_args!("Out of memory building crontab")); + fn crontab_with_entry(&mut self) -> Result, ()> { + let Ok(mut result) = self.filtered_crontab() else { + self.base + .set_err(format_args!("Out of memory building crontab")); return Err(()); - } + }; // 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(self.title.as_bytes()), + title = bstr::BStr::new(self.base.title.as_bytes()), sched = bstr::BStr::new(self.schedule.as_bytes()), exe = bstr::BStr::new(self.bun_exe.as_bytes()), path = bstr::BStr::new(self.abs_path.as_bytes()), ) .is_err() { - self.set_err(format_args!("Out of memory")); - return Err(()); - } - result.extend_from_slice(&new_entry); - - let tmp_path = match make_temp_path("bun-cron-") { - Ok(p) => p, - Err(_) => { - self.set_err(format_args!("Out of memory")); - return Err(()); - } - }; - let tmp_path_ptr = tmp_path.as_ptr(); - self.tmp_path = Some(tmp_path); - - let file = match File::openat( - Fd::cwd(), - self.tmp_path.as_ref().unwrap(), - sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, - 0o600, - ) { - Ok(f) => f, - Err(_) => { - self.set_err(format_args!("Failed to create temp file")); - return Err(()); - } - }; - if file.write_all(&result).is_err() { - let _ = file.close(); // close error is non-actionable - self.set_err(format_args!("Failed to write temp file")); - return Err(()); - } - let _ = file.close(); // close error is non-actionable - - self.state = RegisterState::InstallingCrontab; - // Note: explicit deinit of old reader before reassign — Drop handles it. - self.stdout_reader = OutputReader::init::(); - let Some(crontab_path) = find_crontab() else { - self.set_err(format_args!("crontab not found in PATH")); + self.base.set_err(format_args!("Out of memory")); return Err(()); - }; - Ok((crontab_path, tmp_path_ptr.cast())) + } + result.extend_from_slice(&new_entry); + Ok(result) } // -- macOS -- @@ -567,18 +654,19 @@ impl CronRegisterJob { #[cfg(target_os = "macos")] fn prepare_plist(&mut self) -> Result<(), ()> { - self.state = RegisterState::WritingPlist; + self.base.state = CronJobState::WritingPlist; let calendar_xml = match cron_to_calendar_interval(self.schedule.as_bytes()) { Ok(x) => x, Err(_) => { - self.set_err(format_args!("Invalid cron expression")); + self.base.set_err(format_args!("Invalid cron expression")); return Err(()); } }; let Some(home) = env_var::HOME.get() else { - self.set_err(format_args!("HOME environment variable not set")); + self.base + .set_err(format_args!("HOME environment variable not set")); return Err(()); }; @@ -589,7 +677,7 @@ impl CronRegisterJob { bstr::BStr::new(home) ); if Fd::cwd().make_path(&launch_agents_dir).is_err() { - self.set_err(format_args!( + self.base.set_err(format_args!( "Failed to create ~/Library/LaunchAgents directory" )); return Err(()); @@ -598,15 +686,15 @@ impl CronRegisterJob { let plist_path = match alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(self.title.as_bytes()) + bstr::BStr::new(self.base.title.as_bytes()) )) { Ok(p) => p, Err(_) => { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } }; - self.tmp_path = Some(plist_path); + self.base.tmp_path = Some(plist_path); // XML-escape all dynamic values macro_rules! try_escape { @@ -614,13 +702,13 @@ impl CronRegisterJob { match xml_escape($e) { Ok(v) => v, Err(_) => { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } } }; } - let xml_title = try_escape!(self.title.as_bytes()); + let xml_title = try_escape!(self.base.title.as_bytes()); let xml_bun = try_escape!(self.bun_exe.as_bytes()); let xml_path = try_escape!(self.abs_path.as_bytes()); let xml_sched = try_escape!(self.schedule.as_bytes()); @@ -658,52 +746,32 @@ impl CronRegisterJob { ) .is_err() { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } let file = match File::openat( Fd::cwd(), - self.tmp_path.as_ref().unwrap(), + self.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, 0o644, ) { Ok(f) => f, Err(_) => { - self.set_err(format_args!("Failed to create plist file")); + self.base + .set_err(format_args!("Failed to create plist file")); return Err(()); } }; if file.write_all(&plist).is_err() { let _ = file.close(); // close error is non-actionable - self.set_err(format_args!("Failed to write plist")); + self.base.set_err(format_args!("Failed to write plist")); return Err(()); } let _ = file.close(); // close error is non-actionable Ok(()) } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn spawn_bootout(this: *mut Self) { - // SAFETY: exclusive borrow is confined to `prepare_bootout`; it ends - // before the freeing calls below. - let uid_str = unsafe { (*this).prepare_bootout() }; - let Ok(uid_str) = uid_str else { - // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. - 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) { @@ -729,15 +797,15 @@ impl CronRegisterJob { #[cfg(target_os = "macos")] fn prepare_bootstrap(&mut self) -> Result<(ZString, ZString), ()> { - self.state = RegisterState::Bootstrapping; - let Some(plist_path) = self.tmp_path.take() else { - self.set_err(format_args!("No plist path")); + self.base.state = CronJobState::Bootstrapping; + let Some(plist_path) = self.base.tmp_path.take() else { + self.base.set_err(format_args!("No plist path")); return Err(()); }; let uid_str = match alloc_print_z(format_args!("gui/{}", get_uid())) { Ok(v) => v, Err(_) => { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } }; @@ -883,30 +951,14 @@ pub(crate) fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsRes ))); } let mut job_box = Box::new(CronRegisterJob { - promise: jsc::JSPromiseStrong::init(global), - global: GlobalRef::from(global), - poll: KeepAlive::default(), + base: CronJobCommon::init::(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::<()>()), }); - job_box.poll.ref_(bun_io::js_vm_ctx()); - let promise_value = job_box.promise.value(); + let promise_value = job_box.base.arm(); let job = bun_core::heap::into_raw(job_box); // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on @@ -958,15 +1010,15 @@ impl CronRegisterJob { } fn prepare_schtasks_create(&mut self) -> Result<(ZString, *const c_char), ()> { - self.state = RegisterState::InstallingCrontab; + self.base.state = CronJobState::InstallingCrontab; let task_name = match alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(self.title.as_bytes()) + bstr::BStr::new(self.base.title.as_bytes()) )) { Ok(v) => v, Err(_) => { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } }; @@ -974,18 +1026,18 @@ impl CronRegisterJob { let xml = match cron_to_task_xml( &self.parsed_cron, self.bun_exe.as_bytes(), - self.title.as_bytes(), + self.base.title.as_bytes(), self.schedule.as_bytes(), self.abs_path.as_bytes(), ) { Ok(x) => x, Err(e) => { if e == TaskXmlError::TooManyTriggers { - self.set_err(format_args!( + self.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 { - self.set_err(format_args!("Failed to build task XML")); + self.base.set_err(format_args!("Failed to build task XML")); } return Err(()); } @@ -994,28 +1046,30 @@ impl CronRegisterJob { let xml_path = match make_temp_path("bun-cron-xml-") { Ok(p) => p, Err(_) => { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); return Err(()); } }; let xml_path_ptr = xml_path.as_ptr(); - self.tmp_path = Some(xml_path); + self.base.tmp_path = Some(xml_path); let file = match File::openat( Fd::cwd(), - self.tmp_path.as_ref().unwrap(), + self.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { - self.set_err(format_args!("Failed to create temp XML file")); + self.base + .set_err(format_args!("Failed to create temp XML file")); return Err(()); } }; if file.write_all(&xml).is_err() { let _ = file.close(); // close error is non-actionable - self.set_err(format_args!("Failed to write temp XML file")); + self.base + .set_err(format_args!("Failed to write temp XML file")); return Err(()); } let _ = file.close(); // close error is non-actionable @@ -1024,23 +1078,6 @@ impl CronRegisterJob { } } -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. - } -} - #[cfg(windows)] const ASCII_WHITESPACE: [u8; 6] = *b" \t\n\r\x0b\x0c"; @@ -1049,34 +1086,7 @@ const ASCII_WHITESPACE: [u8; 6] = *b" \t\n\r\x0b\x0c"; // ============================================================================ 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, + base: CronJobCommon, } // Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. @@ -1086,110 +1096,35 @@ 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 { - type State = RemoveState; - #[cfg(all(not(target_os = "macos"), not(windows)))] - const READING_CRONTAB: RemoveState = RemoveState::ReadingCrontab; - #[cfg(target_os = "macos")] - const BOOTING_OUT: RemoveState = RemoveState::BootingOut; - #[cfg(not(windows))] - fn set_state(&mut self, state: RemoveState) { - self.state = state; - } - #[cfg(all(not(target_os = "macos"), not(windows)))] - fn stdout_reader_slot(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(target_os = "macos")] - fn title_bytes(&self) -> &[u8] { - self.title.as_bytes() - } - 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 + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; + fn base(&self) -> &CronJobCommon { + &self.base } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base } - fn check_finished(&mut self) -> JobAction { - if !self.has_called_process_exit || self.remaining_fds != 0 { - return JobAction::Pending; - } - if let Some(proc) = self.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if self.err_msg.is_some() { - return JobAction::Finish; - } - let Some(status) = self.exit_status.take() else { - return JobAction::Pending; - }; - match status { - Status::Exited(exited) => { - let is_acceptable_nonzero = (self.state == RemoveState::ReadingCrontab - && exited.code == 1) - || self.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) && self.state == RemoveState::InstallingCrontab); - if exited.code != 0 && !is_acceptable_nonzero { - #[cfg(windows)] - let stderr_owned: Vec = bun_core::strings::trim( - self.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() { - self.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); - } else { - self.set_err(format_args!("Process exited with code {}", exited.code)); - } - return JobAction::Finish; - } - } - Status::Signaled(sig) => { - if self.state != RemoveState::BootingOut { - self.set_err(format_args!("Process killed by signal {}", sig as i32)); - return JobAction::Finish; - } - } - Status::Err(err) => { - self.set_err(format_args!( - "Process error: {}", - <&'static str>::from(err.get_errno()) - )); - return JobAction::Finish; - } - Status::Running => return JobAction::Pending, - } - JobAction::Advance + 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. unsafe fn advance_state(this: *mut Self) { // SAFETY: shared read of a Copy field; the borrow ends at this statement. - let state = unsafe { (*this).state }; + let state = unsafe { (*this).base.state }; #[cfg(target_os = "macos")] { match state { - RemoveState::BootingOut => { + CronJobState::BootingOut => { // SAFETY: exclusive borrow ends when `unlink_plist` returns. unsafe { (*this).unlink_plist() }; // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. @@ -1197,7 +1132,7 @@ impl CronJobBase for CronRemoveJob { } _ => { // SAFETY: temporary exclusive borrow ending at this statement. - unsafe { (*this).set_err(format_args!("Unexpected state")) }; + unsafe { (*this).base.set_err(format_args!("Unexpected state")) }; // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. unsafe { Self::finish(this) }; } @@ -1207,169 +1142,52 @@ impl CronJobBase for CronRemoveJob { { match state { // SAFETY: no borrows of `this` remain; `this` is the live heap job. - RemoveState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, + CronJobState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. - RemoveState::InstallingCrontab => unsafe { Self::finish(this) }, + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, _ => { // SAFETY: temporary exclusive borrow ending at this statement. - unsafe { (*this).set_err(format_args!("Unexpected state")) }; + unsafe { (*this).base.set_err(format_args!("Unexpected state")) }; // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. unsafe { Self::finish(this) }; } } } } - - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller transfers the unique Box leaked in cron_remove. - let mut job = unsafe { bun_core::heap::take(this) }; - job.poll.unref(bun_io::js_vm_ctx()); - let ev = VirtualMachine::get().event_loop_mut(); - ev.enter(); - if let Some(msg) = &job.err_msg { - let _ = job.promise.reject_with_async_stack( - &job.global, - Ok(job - .global - .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), - ); - } else { - let _ = job.promise.resolve(&job.global, JSValue::UNDEFINED); - } - // Drop runs INSIDE the enter/exit scope so Process detach/deref and - // reader teardown observe the entered event-loop state. - drop(job); - ev.exit(); - } } impl CronRemoveJob { #[cfg(target_os = "macos")] fn unlink_plist(&mut self) { let Some(home) = env_var::HOME.get() else { - self.set_err(format_args!("HOME not set")); + self.base.set_err(format_args!("HOME not set")); return; }; if let Ok(plist_path) = alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(self.title.as_bytes()) + bstr::BStr::new(self.base.title.as_bytes()) )) { let _ = sys::unlink(&plist_path); } else { - self.set_err(format_args!("Out of memory")); + self.base.set_err(format_args!("Out of memory")); } } - /// 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: exclusive borrow is confined to `prepare_list_crontab`; it - // ends before the freeing calls below. - let crontab_path = unsafe { (*this).prepare_list_crontab(this.cast()) }; - let Some(crontab_path) = crontab_path else { - // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; - } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn remove_crontab_entry(this: *mut Self) { - // SAFETY: exclusive borrow is confined to `prepare_filtered_crontab`; - // it ends before the freeing calls below. - let prepared = unsafe { (*this).prepare_filtered_crontab() }; - let Ok((crontab_path, tmp_path_ptr)) = prepared else { - // SAFETY: no borrows of `this` remain; `finish` consumes the live heap job. - return unsafe { Self::finish(this) }; - }; - let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr, core::ptr::null()]; - // SAFETY: no borrows of `this` remain; `spawn_cmd` may free `this`. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - } - - #[cfg(not(target_os = "macos"))] - fn prepare_filtered_crontab(&mut self) -> Result<(*const c_char, *const c_char), ()> { - let existing_content = self.stdout_reader.final_buffer().as_slice(); - let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, self.title.as_bytes(), &mut result).is_err() { - self.set_err(format_args!("Out of memory")); - return Err(()); - } - - let tmp_path = match make_temp_path("bun-cron-rm-") { - Ok(p) => p, - Err(_) => { - self.set_err(format_args!("Out of memory")); - return Err(()); - } - }; - let tmp_path_ptr = tmp_path.as_ptr(); - self.tmp_path = Some(tmp_path); - - let file = match File::openat( - Fd::cwd(), - self.tmp_path.as_ref().unwrap(), - sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, - 0o600, - ) { - Ok(f) => f, - Err(_) => { - self.set_err(format_args!("Failed to create temp file")); - return Err(()); - } - }; - if file.write_all(&result).is_err() { - let _ = file.close(); // close error is non-actionable - self.set_err(format_args!("Failed to write temp file")); - return Err(()); - } - let _ = file.close(); // close error is non-actionable - - self.state = RemoveState::InstallingCrontab; - self.stdout_reader = OutputReader::init::(); - let Some(crontab_path) = find_crontab() else { - self.set_err(format_args!("crontab not found in PATH")); - return Err(()); - }; - Ok((crontab_path, tmp_path_ptr.cast())) - } - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn start_mac(this: *mut Self) { - // SAFETY: exclusive borrow is confined to `prepare_bootout`; it ends + // SAFETY: exclusive borrow is confined to `filtered_crontab`; it ends // before the freeing calls below. - let uid_str = unsafe { (*this).prepare_bootout() }; - let Ok(uid_str) = uid_str else { + let prepared = unsafe { (*this).filtered_crontab() }; + let Ok(content) = prepared else { + // SAFETY: temporary exclusive borrow ending at this statement. + unsafe { (*this).base.set_err(format_args!("Out of memory")) }; // SAFETY: no borrows of `this` remain; `finish` consumes 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: no borrows of `this` remain; `spawn_cmd` may free `this`. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(uid_str); + // SAFETY: no borrows of `this` remain; `install_crontab` may free `this`. + unsafe { Self::install_crontab(this, &content, "bun-cron-rm-") }; } } @@ -1392,31 +1210,15 @@ pub(crate) fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResul } let mut job_box = Box::new(CronRemoveJob { - promise: jsc::JSPromiseStrong::init(global), - global: GlobalRef::from(global), - poll: KeepAlive::default(), - title: ZString::from_bytes(title_slice.slice()), - state: RemoveState::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::<()>()), + base: CronJobCommon::init::(global, title_slice.slice()), }); - job_box.poll.ref_(bun_io::js_vm_ctx()); - let promise_value = job_box.promise.value(); + let promise_value = job_box.base.arm(); let job = bun_core::heap::into_raw(job_box); // 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 { @@ -1455,27 +1257,12 @@ impl CronRemoveJob { } fn prepare_schtasks_delete(&mut self) -> Result { - self.state = RemoveState::InstallingCrontab; + self.base.state = CronJobState::InstallingCrontab; alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(self.title.as_bytes()) + bstr::BStr::new(self.base.title.as_bytes()) )) - .map_err(|_| self.set_err(format_args!("Out of memory"))) - } -} - -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); - } + .map_err(|_| self.base.set_err(format_args!("Out of memory"))) } } @@ -2165,17 +1952,6 @@ pub(crate) fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult // Shared helpers // ============================================================================ -/// Trait abstracting over CronRegisterJob/CronRemoveJob for `spawn_cmd_generic`. -trait SpawnCmdTarget: CronJobBase + BufferedReaderParent { - const EXIT_KIND: bun_spawn::ProcessExitKind; - 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. @@ -2190,48 +1966,13 @@ bun_spawn::link_impl_ProcessExit! { } } -impl SpawnCmdTarget for CronRegisterJob { - const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; - 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 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, @@ -2252,7 +1993,7 @@ unsafe fn spawn_cmd_generic( // 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::(); @@ -2268,7 +2009,7 @@ unsafe fn spawn_cmd_generic( /// via `set_err` (for the caller to `finish`). `this_ptr` is stored (never /// dereferenced) as the readers' parent pointer; it must be the raw pointer /// `s` was derived from. -/// `s` is raw, not `&mut`: the `stdout_reader().start(..)` failure path +/// `s` is raw, not `&mut`: the `stdout_reader.start(..)` failure path /// synchronously re-enters this job (`on_reader_error` -> `note_reader_error` /// writes `remaining_fds`/`err_msg` through the parent backref), so a `&mut T` /// protector spanning that call would make the re-entrant sibling-field write @@ -2277,23 +2018,23 @@ unsafe fn spawn_cmd_generic( /// /// # Safety /// `s` is the live job (the same allocation `this_ptr` addresses). -unsafe fn spawn_cmd_prepare( +unsafe fn spawn_cmd_prepare( s: *mut T, this_ptr: *mut core::ffi::c_void, argv: &mut [*const c_char], stdin_opt: spawn::Stdio, stdout_opt: spawn::Stdio, ) -> Result<*mut Process, ()> { - macro_rules! s { + macro_rules! base { () => {{ // SAFETY: `s` is the live job (caller contract); the reborrow is // scoped to the enclosing statement's expression. - unsafe { &mut *s } + unsafe { (*s).base_mut() } }}; } - *s!().has_called_process_exit_mut() = false; - *s!().exit_status_mut() = None; - *s!().remaining_fds() = 0; + base!().has_called_process_exit = false; + base!().exit_status = None; + base!().remaining_fds = 0; #[cfg(not(windows))] let resolved_argv0: Option<*const c_char> = None; @@ -2314,7 +2055,7 @@ unsafe fn spawn_cmd_prepare( 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!( + base!().set_err(format_args!( "Could not find '{}' in PATH", bstr::BStr::new(argv0) )); @@ -2341,7 +2082,7 @@ unsafe fn spawn_cmd_prepare( envp_owned.as_ptr().cast() } Err(_) => { - s!().set_err(format_args!("Failed to create environment block")); + base!().set_err(format_args!("Failed to create environment block")); return Err(()); } } @@ -2401,7 +2142,7 @@ unsafe fn spawn_cmd_prepare( // `Drop`. Reclaim it (uv_close + free if init'd) here. #[cfg(windows)] spawn_options.stderr.deinit(); - s!().set_err(format_args!( + base!().set_err(format_args!( "Failed to spawn process: {}", bstr::BStr::new(err.name()) )); @@ -2410,7 +2151,7 @@ unsafe fn spawn_cmd_prepare( Err(e) => { #[cfg(windows)] spawn_options.stderr.deinit(); - s!().set_err(format_args!("Failed to spawn process: {}", e.name())); + base!().set_err(format_args!("Failed to spawn process: {}", e.name())); return Err(()); } }; @@ -2421,12 +2162,12 @@ unsafe fn spawn_cmd_prepare( { if let Some(stdout) = spawned.stdout { if !spawned.memfds[1] { - s!().stdout_reader().set_parent(this_ptr); + base!().stdout_reader.set_parent(this_ptr); let _ = sys::set_nonblocking(stdout); - *s!().remaining_fds() += 1; + base!().remaining_fds += 1; { use bun_io::pipe_reader::PosixFlags; - let flags = &mut s!().stdout_reader().flags; + let flags = &mut base!().stdout_reader.flags; flags.insert(PosixFlags::NONBLOCKING | PosixFlags::SOCKET); flags.remove( PosixFlags::MEMFD @@ -2434,16 +2175,16 @@ unsafe fn spawn_cmd_prepare( | PosixFlags::CLOSED_WITHOUT_REPORTING, ); } - if s!().stdout_reader().start(stdout, true).is_err() { - s!().set_err(format_args!("Failed to start reading stdout")); + if base!().stdout_reader.start(stdout, true).is_err() { + base!().set_err(format_args!("Failed to start reading stdout")); return Err(()); } - if let Some(p) = s!().stdout_reader().handle.get_poll() { + if let Some(p) = base!().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); + base!().stdout_reader.set_parent(this_ptr); + base!().stdout_reader.start_memfd(stdout); } } } @@ -2460,11 +2201,11 @@ unsafe fn spawn_cmd_prepare( // 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().set_source(bun_io::Source::Pipe(pipe)); - s!().stderr_reader().set_parent(this_ptr); - *s!().remaining_fds() += 1; - if s!().stderr_reader().start_with_current_pipe().is_err() { - s!().set_err(format_args!("Failed to start reading stderr")); + base!().stderr_reader.set_source(bun_io::Source::Pipe(pipe)); + base!().stderr_reader.set_parent(this_ptr); + base!().remaining_fds += 1; + if base!().stderr_reader.start_with_current_pipe().is_err() { + base!().set_err(format_args!("Failed to start reading stderr")); return Err(()); } } @@ -2473,7 +2214,7 @@ unsafe fn spawn_cmd_prepare( // 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); - *s!().process_slot() = Some(process); + base!().process = Some(process); Ok(process) } diff --git a/src/runtime/api/csrf_jsc.rs b/src/runtime/api/csrf_jsc.rs index 3efc49d7c461..678939903dd3 100644 --- a/src/runtime/api/csrf_jsc.rs +++ b/src/runtime/api/csrf_jsc.rs @@ -48,6 +48,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 options.get_optional_slice(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] @@ -86,64 +147,16 @@ pub(crate) fn csrf__generate(global: &JSGlobalObject, frame: &CallFrame) -> JsRe } // Extract sessionId (optional) - if let Some(session_id_slice) = options_value.get_optional_slice(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")) - ); - } - } } } @@ -226,24 +239,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) = options_value.get_optional_slice(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) = options_value.get_optional_slice(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")? { @@ -251,53 +251,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 3ca367b6f914..4bd77e415ef3 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -431,6 +431,66 @@ 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: _, + vm_handle: _, + 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: _, + emit_incremental_visualizer_events: _, + emit_memory_visualizer_events: _, + memory_visualizer_timer: _, + assume_perfect_incremental_bundling: _, + broadcast_console_log_from_browser_to_server: _, + } = $e; + }; +} +pub(crate) use destructure_dev_server_fields; + const INTERNAL_PREFIX: &str = "/_bun"; /// Assets which are routed to the `Assets` storage. const ASSET_PREFIX: &str = const_format::concatcp!(INTERNAL_PREFIX, "/asset"); @@ -1012,61 +1072,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: _, - vm_handle: _, - 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: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - 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/bake_body.rs b/src/runtime/bake/bake_body.rs index ded4de92aea2..109711801ef5 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -21,11 +21,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); @@ -1148,149 +1148,27 @@ impl Framework { minify_syntax: Option, minify_identifiers: Option, ) -> crate::Result<()> { - // `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/dev_server/memory_cost.rs b/src/runtime/bake/dev_server/memory_cost.rs index 766c627921cd..69945b874173 100644 --- a/src/runtime/bake/dev_server/memory_cost.rs +++ b/src/runtime/bake/dev_server/memory_cost.rs @@ -4,6 +4,7 @@ use crate::api::server::html_bundle::HTMLBundleRoute; use crate::bake::dev_server::{ DevServer, HmrSocket, IncrementalResult, TestingBatchEvents, deferred_request, packed_map, }; +use crate::bake::dev_server_body::destructure_dev_server_fields; use bun_collections::ArrayHashMap; #[derive(Clone, Copy, Default)] @@ -33,61 +34,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: _, - vm_handle: _, - 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: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - 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. + destructure_dev_server_fields!(dev); // does not contain pointers // .assume_perfect_incremental_bundling diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 12136842c27d..2d6ac21fb8f2 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -190,12 +190,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(crate) fn init_transpiler<'a>( &mut self, @@ -206,79 +205,7 @@ impl Framework { out: &mut core::mem::MaybeUninit>, bundler_options: &BuildConfigSubset, ) -> crate::Result<*mut bun_bundler::bake_types::Framework> { - 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. @@ -288,65 +215,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 @@ -463,6 +348,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(crate) source_map: bun_bundler::options::SourceMapOption, + pub(crate) minify_whitespace: Option, + pub(crate) minify_syntax: Option, + pub(crate) minify_identifiers: Option, + pub(crate) has_react_fast_refresh: bool, + pub(crate) has_server_components: bool, + pub(crate) 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, +) -> crate::Result<*mut bun_bundler::bake_types::Framework> { + 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 { @@ -543,50 +603,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 `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, - } - } -} 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(crate) ignore_dce_annotations: Option, - pub(crate) conditions: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub(crate) drop: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub(crate) env: bun_options_types::schema::api::DotEnvBehavior, - pub(crate) env_prefix: Option<&'static [u8]>, - pub(crate) define: bun_options_types::schema::api::StringMap, - // `source_map` intentionally omitted — only - // `init_transpiler_with_options` (bake_body) honours it, and DevServer - // never calls that path. -} +/// lives in `bake_body.rs`; DevServer's `init_transpiler` reads everything +/// except `source_map` and the `minify_*` overrides, which only +/// `init_transpiler_with_options` honours. +pub(crate) 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/crypto/CryptoHasher.rs b/src/runtime/crypto/CryptoHasher.rs index dbe6ba2b5942..379f130df0e3 100644 --- a/src/runtime/crypto/CryptoHasher.rs +++ b/src/runtime/crypto/CryptoHasher.rs @@ -49,6 +49,98 @@ 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 [arg] = callframe.arguments_as_array::<1>(); + if callframe.arguments_count() == 0 || arg.is_empty_or_undefined_or_null() { + return Ok(None); + } + match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))), + } +} + +/// Hand-expanded static-method decode for the trailing +/// `(Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)` parameters +/// (static-method arm: only `undefined` → None for the output). +/// +/// Both arguments are coerced before either buffer is read: the output is +/// decoded first, and its buffer view is re-read after the input coercion, +/// since either coercion can run user code that detaches or resizes the other. +fn hash_arguments( + global: &JSGlobalObject, + arguments: &[JSValue], +) -> JsResult<(BlobOrStringOrBuffer, Option)> { + let Some(&input_arg) = arguments.first() else { + return Err(global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))); + }; + + let mut output: Option = match arguments.get(1) { + 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 Some(input) = BlobOrStringOrBuffer::from_js(global, input_arg)? else { + return Err(global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))); + }; + if let Some(StringOrBuffer::Buffer(buffer)) = &mut output { + buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); + } + + Ok((input, output)) +} + /// `union(enum)` → Rust enum with payload variants. /// `.classes.ts`-backed type: the C++ JSCell wrapper stays generated; this is the `m_ctx` payload. /// @@ -218,23 +310,7 @@ impl CryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let [arg] = callframe.arguments_as_array::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if callframe.arguments_count() > 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) } @@ -242,19 +318,9 @@ impl CryptoHasher { /// `(algorithm string, input, optional output buffer/encoding)`. pub(crate) fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len() { - let v = arguments[i]; - i += 1; - Some(v) - } else { - None - } - }; let algorithm = { - let Some(string_value) = next_eat() else { + let Some(&string_value) = arguments.first() else { return Err(global.throw_invalid_arguments(format_args!("Missing argument"))); }; if string_value.is_undefined_or_null() { @@ -263,41 +329,7 @@ impl CryptoHasher { string_value.get_zig_string(global)? }; - // Node.BlobOrStringOrBuffer - let Some(input_arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let mut 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 = match BlobOrStringOrBuffer::from_js(global, input_arg)? { - Some(b) => b, - None => { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - } - }; - if let Some(StringOrBuffer::Buffer(buffer)) = &mut output { - buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); - } - + let (input, output) = hash_arguments(global, &arguments[1..])?; Self::hash_(global, algorithm, &input, output) } @@ -439,28 +471,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) } } @@ -688,27 +703,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), } } @@ -928,30 +925,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( @@ -1206,23 +1188,7 @@ impl StaticCryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let [arg] = callframe.arguments_as_array::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if callframe.arguments_count() > 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) } @@ -1231,53 +1197,7 @@ impl StaticCryptoHasher { /// Hand-expanded `wrapStaticMethod` decode for the parameter list /// `(*JSGlobalObject, Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)`. pub(crate) fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len() { - let v = arguments[i]; - i += 1; - Some(v) - } else { - None - } - }; - - // Node.BlobOrStringOrBuffer - let Some(input_arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let mut 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 = match BlobOrStringOrBuffer::from_js(global, input_arg)? { - Some(b) => b, - None => { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - } - }; - if let Some(StringOrBuffer::Buffer(buffer)) = &mut output { - buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); - } - + let (input, output) = hash_arguments(global, callframe.arguments())?; Self::hash_(global, &input, output) } @@ -1319,29 +1239,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. @@ -1373,26 +1301,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), } } @@ -1463,26 +1374,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), } } @@ -1492,23 +1386,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); @@ -1516,7 +1395,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 2b2bb24c255b..5dbd835f37da 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -742,6 +742,40 @@ 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] fn js_password_object_verify( @@ -754,29 +788,7 @@ 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. // @@ -834,29 +846,7 @@ 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(mut 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 950620131d25..cd1fbbafa0c2 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -134,7 +134,7 @@ 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); @@ -229,7 +229,7 @@ pub(crate) 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); @@ -371,7 +371,7 @@ impl CacheConfig { // ────────────────────────────────────────────────────────────────────────── /// Each c-ares reply struct implements this with its record-type tag. -pub trait CAresRecordType: Sized { +pub(crate) trait CAresRecordType: Sized { const TYPE_NAME: &'static str; /// `"query" + ucfirst(TYPE_NAME)` — each impl carries the precomputed /// literal so error paths report the right syscall. @@ -402,34 +402,97 @@ pub(crate) 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(crate) hash: u64, - pub(crate) len: u16, - pub name: Box<[u8]>, - pub(crate) 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(crate) hash: u64, + pub(crate) len: u16, + pub(crate) name: Box<[u8]>, + pub(crate) 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(), } } } @@ -539,37 +602,7 @@ pub(crate) struct GetHostByAddrInfoRequest { pub tail: *mut CAresReverse, // INTRUSIVE } -pub mod get_host_by_addr_info_request { - use super::*; - - pub struct PendingCacheKey { - pub(crate) hash: u64, - pub(crate) len: u16, - pub name: Box<[u8]>, - pub(crate) 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 @@ -789,37 +822,7 @@ pub(crate) struct GetNameInfoRequest { pub tail: *mut CAresNameInfo, // INTRUSIVE } -pub mod get_name_info_request { - use super::*; - - pub struct PendingCacheKey { - pub(crate) hash: u64, - pub(crate) len: u16, - pub name: Box<[u8]>, - pub(crate) 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 { fn init( @@ -928,6 +931,8 @@ pub struct GetAddrInfoRequest { pub(crate) tail: *mut DNSLookup, // INTRUSIVE } +impl_has_tail!(GetAddrInfoRequest => DNSLookup); + pub mod get_addr_info_request { use super::*; @@ -999,33 +1004,6 @@ pub mod get_addr_info_request { } } - pub struct PendingCacheKey { - pub(crate) hash: u64, - pub(crate) len: u16, - pub name: Box<[u8]>, - pub(crate) 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(), - } - } - } - #[cfg(target_os = "macos")] pub struct BackendDnsSd { pub(crate) query: dns_sd::QueryState, @@ -1730,7 +1708,7 @@ impl Drop for CAresLookup { // DNSLookup // ────────────────────────────────────────────────────────────────────────── -pub(crate) struct DNSLookup { +pub struct DNSLookup { pub resolver: Option>, // SHARED (intrusive — Resolver embeds ref_count and crosses FFI as m_ctx) pub global_this: bun_ptr::BackRef, // JSC_BORROW (BACKREF — JSGlobalObject outlives the request) pub promise: JSPromiseStrong, @@ -3565,28 +3543,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; @@ -3687,18 +3659,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(crate) 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 +pub enum LookupCacheHit { + Inflight(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer + New(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer Disabled, } @@ -3709,11 +3674,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(crate) 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}`). @@ -3725,122 +3688,50 @@ pub(crate) 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) } } @@ -3901,6 +3792,109 @@ impl RecordType { pub(crate) 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 answer already converted for `prev_global` (the head's global); +/// `to_js` re-converts it whenever a tail node belongs to a different global. +/// `keep_alive` brackets every `on_complete` so the conservative stack scan +/// keeps the shared value 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: Outcome, + mut prev_global: &'a JSGlobalObject, + mut to_js: impl FnMut(&JSGlobalObject) -> Outcome, + mut on_complete: impl FnMut(*mut Node, Outcome), + 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(); + keep_alive(&array); + on_complete(head, array); + consume_head(); + keep_alive(&array); + + 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(); + + keep_alive(&array); + on_complete(value.as_ptr(), array); + keep_alive(&array); + } + } +} + impl Resolver { pub(crate) fn vm(&self) -> &VirtualMachine { self.vm.get() @@ -4187,7 +4181,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. @@ -4195,21 +4189,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 { @@ -4240,24 +4229,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() @@ -4277,63 +4266,43 @@ 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 = Outcome::of( - prev_global, - (*addr).to_js_response(prev_global, T::TYPE_NAME), + let head_global = (*head).global_this(); + let array = Outcome::of( + head_global, + (*addr).to_js_response(head_global, T::TYPE_NAME), ); // SAFETY: addr is the c-ares-allocated reply; freed once after all consumers run. let _free_addr = scopeguard::guard(addr, |a| T::destroy(a)); - keep_alive(&array); - CAresLookup::::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - keep_alive(&array); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = - Outcome::of(new_global, (*addr).to_js_response(new_global, T::TYPE_NAME)); - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - keep_alive(&array); - CAresLookup::::on_complete(value.as_ptr(), array); - keep_alive(&array); - } + drain_chain_ok( + head, + array, + head_global, + |global| Outcome::of(global, (*addr).to_js_response(global, T::TYPE_NAME)), + |node, value| CAresLookup::::on_complete(node, value), + consume_head, + ); } } @@ -4349,60 +4318,43 @@ 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 = Outcome::of( - prev_global, - super::cares_jsc::addr_info_to_js_array(&mut *addr, prev_global), + let head_global = (*head).global_this(); + let array = Outcome::of( + head_global, + super::cares_jsc::addr_info_to_js_array(&mut *addr, head_global), ); // 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`. + // Move the raw pointer into the guard so `to_js` can keep borrowing `*addr`. let _free_addr = scopeguard::guard(addr, |a| c_ares::AddrInfo::destroy(a)); - keep_alive(&array); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - keep_alive(&array); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = Outcome::of( - new_global, - super::cares_jsc::addr_info_to_js_array(&mut *addr, new_global), - ); - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - keep_alive(&array); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - keep_alive(&array); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + Outcome::of( + global, + super::cares_jsc::addr_info_to_js_array(&mut *addr, global), + ) + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + consume_head, + ); } } @@ -4419,7 +4371,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: Outcome = match super::options_jsc::result_any_to_js(result, global_object) + let array: Outcome = match super::options_jsc::result_any_to_js(result, global_object) .transpose() { Some(a) => Outcome::of(global_object, a), @@ -4447,35 +4399,24 @@ 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(); - - { - keep_alive(&array); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - keep_alive(&array); - } - - 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) { + let head = ptr::addr_of_mut!((*key.lookup).head); + drain_chain_ok( + head, + array, + (*head).global_this(), + |global| { // Non-null addrinfo (checked above): never `None`. - array = Outcome::of( - new_global, - super::options_jsc::result_any_to_js(result, new_global) + Outcome::of( + global, + super::options_jsc::result_any_to_js(result, global) .map(|a| a.expect("addrinfo present")), - ); - prev_global = new_global; - } - - keep_alive(&array); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - keep_alive(&array); - } + ) + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + || drop(bun_core::heap::take(key.lookup)), + ); } } @@ -4491,60 +4432,43 @@ 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 = Outcome::of( - prev_global, - super::cares_jsc::hostent_to_js_response(&mut *addr, prev_global, b""), + let head_global = (*head).global_this(); + let array = Outcome::of( + head_global, + super::cares_jsc::hostent_to_js_response(&mut *addr, head_global, b""), + ); + drain_chain_ok( + head, + array, + head_global, + |global| { + Outcome::of( + global, + super::cares_jsc::hostent_to_js_response(&mut *addr, global, b""), + ) + }, + |node, value| CAresReverse::on_complete(node, value), + consume_head, ); - keep_alive(&array); - CAresReverse::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - keep_alive(&array); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = Outcome::of( - new_global, - super::cares_jsc::hostent_to_js_response(&mut *addr, new_global, b""), - ); - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - keep_alive(&array); - CAresReverse::on_complete(value.as_ptr(), array); - keep_alive(&array); - } } } @@ -4560,64 +4484,45 @@ 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 = Outcome::of( - prev_global, - super::cares_jsc::nameinfo_to_js_response(&mut name_info, prev_global), + let head_global = (*head).global_this(); + let array = Outcome::of( + head_global, + super::cares_jsc::nameinfo_to_js_response(&mut name_info, head_global), + ); + drain_chain_ok( + head, + array, + head_global, + |global| { + Outcome::of( + global, + super::cares_jsc::nameinfo_to_js_response(&mut name_info, global), + ) + }, + |node, value| CAresNameInfo::on_complete(node, value), + consume_head, ); - keep_alive(&array); - CAresNameInfo::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - keep_alive(&array); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = Outcome::of( - new_global, - super::cares_jsc::nameinfo_to_js_response(&mut name_info, new_global), - ); - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - keep_alive(&array); - CAresNameInfo::on_complete(value.as_ptr(), array); - keep_alive(&array); - } } } - pub(crate) fn get_or_put_into_resolve_pending_cache( + pub(crate) fn get_or_put_into_pending_cache( &self, - key: &R::PendingCacheKey, + key: &PendingCacheKey, field: PendingCacheField, ) -> LookupCacheHit { // Dispatch via `HasPendingCacheKey::pending_cache`; the body is @@ -4628,49 +4533,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(crate) 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(crate) fn get_channel(&self) -> ChannelResult<'_> { if self.channel.get().is_none() { let opts = self.options.get(); @@ -5084,8 +4958,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, ); @@ -5403,10 +5277,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); @@ -5457,7 +5330,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); @@ -5982,8 +5855,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/dns_sd.rs b/src/runtime/dns_jsc/dns_sd.rs index 9edaa8d6d1ac..fabd2dd37b2b 100644 --- a/src/runtime/dns_jsc/dns_sd.rs +++ b/src/runtime/dns_jsc/dns_sd.rs @@ -699,7 +699,7 @@ pub(crate) fn lookup( 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); if let CacheHit::Inflight(inflight) = cache { diff --git a/src/runtime/dns_jsc/mod.rs b/src/runtime/dns_jsc/mod.rs index f4327d5cee5e..88b0cd134f89 100644 --- a/src/runtime/dns_jsc/mod.rs +++ b/src/runtime/dns_jsc/mod.rs @@ -29,11 +29,8 @@ pub mod options_jsc; // GetAddrInfo.Options ↔ JSValue #[cfg(target_os = "macos")] pub(crate) use dns_body::dns_sd; +pub use dns_body::get_addr_info_request; pub use dns_body::{ CacheConfig, CacheHit, 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/ipc.rs b/src/runtime/ipc.rs index 95f9d1e6c0bb..3369348ad0f3 100644 --- a/src/runtime/ipc.rs +++ b/src/runtime/ipc.rs @@ -2059,6 +2059,35 @@ fn decode_next_advanced( }) } +/// Dispatches every complete JSON-mode message buffered in +/// `send_queue.incoming`. Shared by the POSIX `on_data` and Windows libuv +/// `on_read` callbacks. +fn drain_json_messages(send_queue: &SendQueue, global_this: &JSGlobalObject) { + loop { + match decode_next_json(&send_queue.incoming, global_this) { + DecodeStep::Message(result) => { + handle_ipc_message(send_queue, result.message, global_this); + } + step => return finish_decode(send_queue, &step), + } + } +} + +/// Dispatches every complete Advanced-mode message buffered in +/// `send_queue.incoming`. Shared by the POSIX `on_data` and Windows libuv +/// `on_read` callbacks. +fn drain_advanced_messages(send_queue: &SendQueue, global_this: &JSGlobalObject) { + let mut slice_start: usize = 0; + loop { + match decode_next_advanced(&send_queue.incoming, global_this, &mut slice_start) { + DecodeStep::Message(result) => { + handle_ipc_message(send_queue, result.message, global_this); + } + step => return finish_decode(send_queue, &step), + } + } +} + fn on_data2(send_queue: &SendQueue, all_data: &[u8]) { let mut data = all_data; @@ -2076,15 +2105,7 @@ fn on_data2(send_queue: &SendQueue, all_data: &[u8]) { }; json_buf.append(data); }); - - loop { - match decode_next_json(&send_queue.incoming, &global_this) { - DecodeStep::Message(result) => { - handle_ipc_message(send_queue, result.message, &global_this); - } - step => return finish_decode(send_queue, &step), - } - } + drain_json_messages(send_queue, &global_this); } Mode::Advanced => { // Advanced mode: uses length-prefix, no newline scanning needed. @@ -2129,15 +2150,7 @@ fn on_data2(send_queue: &SendQueue, all_data: &[u8]) { }; handle_oom(adv_buf.write(data)); }); - let mut slice_start: usize = 0; - loop { - match decode_next_advanced(&send_queue.incoming, &global_this, &mut slice_start) { - DecodeStep::Message(result) => { - handle_ipc_message(send_queue, result.message, &global_this); - } - step => return finish_decode(send_queue, &step), - } - } + drain_advanced_messages(send_queue, &global_this); } } } @@ -2254,16 +2267,7 @@ pub mod IPCHandlers { // forwarded. json_buf.notify_written(nread); }); - - // Process complete messages using next() - avoids O(n²) re-scanning - loop { - match decode_next_json(&send_queue.incoming, &global_this) { - DecodeStep::Message(result) => { - handle_ipc_message(send_queue, result.message, &global_this); - } - step => return finish_decode(send_queue, &step), - } - } + drain_json_messages(send_queue, &global_this); } Mode::Advanced => { send_queue.incoming.with_mut(|inc| { @@ -2273,19 +2277,7 @@ pub mod IPCHandlers { // SAFETY: `on_read_alloc` reserved ≥ nread bytes; libuv initialised them. unsafe { adv_buf.uv_commit(nread) }; }); - let mut slice_start: usize = 0; - loop { - match decode_next_advanced( - &send_queue.incoming, - &global_this, - &mut slice_start, - ) { - DecodeStep::Message(result) => { - handle_ipc_message(send_queue, result.message, &global_this); - } - step => return finish_decode(send_queue, &step), - } - } + drain_advanced_messages(send_queue, &global_this); } } } diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 60b81f6a0412..b29d258414e2 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -948,15 +948,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. @@ -1008,8 +1015,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() { @@ -1022,8 +1031,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; } @@ -1110,128 +1121,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) }; - // SAFETY: as above. - let has_yielded_tasks = unsafe { (*el).promote_yield_tasks() }; - #[cfg(windows)] - if has_yielded_tasks || !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. - // SAFETY: `el` is the live per-thread event loop. - let has_pending_immediate = has_yielded_tasks - || !unsafe { &*el }.immediate_tasks.is_empty() - || unsafe { &*el }.has_pending_tasks(); - // 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() } { - // Before `get_timeout` — see the matching call in `auto_tick`. - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).process_gc_timer() }; - // `get_timeout` reads CLOCK_MONOTONIC to compare against the timer heap; hand that - // same reading to the tick for the park hook's idle-sweep rate limit. It is lazy, - // and so is the hook: NOW_NS_UNKNOWN means it took none. - let mut now: Option = None; - // 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(), - &mut now, - ) - }; - let now_ns = now.map_or(bun_uws::NOW_NS_UNKNOWN, |t| t.ns()); - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { - (*loop_) - .tick_with_timeout(if have_timeout { Some(×pec) } else { None }, now_ns) - }; - } 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 @@ -1526,8 +1419,8 @@ 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 4ad87609e8e7..7dffa70a0313 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -640,6 +640,78 @@ 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)] extern "C" fn napi_create_string_latin1( env_: napi_env, @@ -650,24 +722,11 @@ 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!( @@ -677,23 +736,13 @@ 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 (mut string, bytes) = bun_core::String::create_uninitialized_latin1(slice.len()); bytes.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())) } #[unsafe(no_mangle)] @@ -706,24 +755,11 @@ 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)); @@ -747,25 +783,11 @@ 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) { @@ -778,23 +800,13 @@ 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 8c01305a3f4c..6d1c83cf6390 100644 --- a/src/runtime/server/ServerWebSocket.rs +++ b/src/runtime/server/ServerWebSocket.rs @@ -246,17 +246,13 @@ 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 - /// server has been torn down (`handler.app == None`). The "publish() closed" - /// log + `0` return is the caller's responsibility (it varies in nothing, - /// but keeping it inline preserves the per-method `scoped_log!` callsite). + /// server has been torn down (`handler.app == None`). #[inline] fn publish_ctx(&self) -> Option<(*mut c_void, bool, bool)> { let handler = self.handler(); @@ -289,6 +285,61 @@ 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 [topic_value, message_value, compress_value] = callframe.arguments_as_array::<3>(); + if callframe.arguments_count() < 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); + }; + + 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, + callframe.arguments_count() as usize, + )?; + + 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 /// aggregated `SendStatus` to the JS number contract shared with `send()`. @@ -821,34 +872,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let [topic_value, message_value, compress_value] = callframe.arguments_as_array::<3>(); - if callframe.arguments_count() < 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)); }; - 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, - callframe.arguments_count() as usize, - )?; - if message_value.is_empty_or_undefined_or_null() { return Err(global_this.throw(format_args!("publish requires a non-empty message"))); } @@ -905,32 +934,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let [topic_value, message_value, compress_value] = callframe.arguments_as_array::<3>(); - - if callframe.arguments_count() < 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)); }; - 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, - callframe.arguments_count() as usize, - )?; - 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"))); } @@ -958,37 +967,18 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let [topic_value, message_value, compress_value] = callframe.arguments_as_array::<3>(); - - if callframe.arguments_count() < 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)); }; - 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, - callframe.arguments_count() as usize, - )?; - if message_value.is_empty_or_undefined_or_null() { return Err( global_this.throw(format_args!("publishBinary requires a non-empty message")) diff --git a/src/runtime/shell/builtin/basename.rs b/src/runtime/shell/builtin/basename.rs index 43bc4dcb7423..f36ec05398a9 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(_err) = err { Self::state_mut(interp, cmd).state = State::Err; return Builtin::done(interp, cmd, 1); @@ -64,7 +92,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 bb84b01e0054..e5c7cc4158ff 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; @@ -189,23 +189,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 @@ -326,67 +309,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 0b7dd1fe5b57..af1660702b14 100644 --- a/src/runtime/shell/builtin/dirname.rs +++ b/src/runtime/shell/builtin/dirname.rs @@ -1,73 +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(_err) = err { - 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 8f6213a5dcad..265f4aa55e7a 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; @@ -173,28 +173,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. @@ -274,71 +252,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); /// Opens the path, iterates its entries (or /// prints the path itself for files / `-d`), accumulating into `output`. diff --git a/src/runtime/shell/builtin/mkdir.rs b/src/runtime/shell/builtin/mkdir.rs index a4695d61c6b2..667b5838343f 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. fn on_shell_mkdir_task_done(interp: &Interpreter, cmd: NodeId, task: &mut ShellMkdirTask) { @@ -182,80 +163,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 95ae1398deee..73997245d3bd 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; @@ -122,28 +122,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. @@ -166,71 +144,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 2379323e78cb..cf9f856a35cd 100644 --- a/src/runtime/shell/interpreter.rs +++ b/src/runtime/shell/interpreter.rs @@ -2536,6 +2536,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 43197645ae17..d3220eb83d90 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -768,17 +768,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(crate) 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)> { // SAFETY: bun_vm() is non-null for a Bun-owned global. let vm = global.bun_vm(); let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments()); @@ -801,7 +798,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, @@ -812,6 +808,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(crate) 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: { @@ -858,38 +870,8 @@ pub mod testing_apis { callframe: &CallFrame, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments()); - 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 a6ea81124a14..763a18530653 100644 --- a/src/runtime/shell/subproc.rs +++ b/src/runtime/shell/subproc.rs @@ -1037,10 +1037,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. @@ -1069,97 +1069,43 @@ 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); - } - Stdio::SocketFd => { - // The shell never uses this; rejected at i < 3 anyway. - panic!("Unimplemented stdin socket-fd"); + #[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::SocketFd => { + // The shell never uses this; rejected at i < 3 anyway. + panic!("Unimplemented stdin socket-fd"); + } + 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`. @@ -1171,19 +1117,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)) } - Stdio::SocketFd => { - // The shell never uses this; rejected at i < 3 anyway. - panic!("Unimplemented stdin socket-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), } } @@ -1300,54 +1247,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 - // The shell never uses this; rejected at i < 3 anyway. - Stdio::SocketFd => Readable::Ignore, - }; - } - - #[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 @@ -1356,30 +1281,30 @@ 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 - // The shell never uses this; rejected at i < 3 anyway. - Stdio::SocketFd => 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) + } + Stdio::Capture(_) => Readable::Pipe(PipeReader::create( + event_loop, process, result, shellio, out_type, interp, + )), + Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream + // The shell never uses this; rejected at i < 3 anyway. + Stdio::SocketFd => Readable::Ignore, } } diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index 1465b20cce40..a0cfc215d24a 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -847,6 +847,29 @@ impl NewSocket { self.exit_scope(scope); } + /// 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 releases `self.handlers` via `exit_scope`. + /// `extra_args` (at most one) follow the implicit `this` argument. + #[inline] + fn call_socket_handler( + &self, + handlers: &Rc, + callback: JSValue, + extra_args: &[JSValue], + ) { + let scope = handlers.enter(); + 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)]); + } + self.exit_scope(scope); + } + /// Takes `ThisPtr`, not `&mut self`: `callback.call(...)` re-enters /// JS which can call `socket.write()`/`end()`/`reload()` on this same /// wrapper via the JS object's `m_ptr`, re-deriving a borrow and mutating @@ -926,16 +949,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(); - - 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)]); - } - this.exit_scope(scope); + this.call_socket_handler(&handlers, callback, &[]); } /// Takes `ThisPtr` for the same re-entrancy reason as `on_writable`. @@ -965,16 +979,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(); - - 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)]); - } - this.exit_scope(scope); + this.call_socket_handler(&handlers, callback, &[]); } /// This socket's callbacks. Panics if it has none — every dispatch entry @@ -1639,16 +1644,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(); - - 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)]); - } - this.exit_scope(scope); + this.call_socket_handler(&handlers, callback, &[]); } /// Takes `ThisPtr` for the same re-entrancy reason as `on_writable`. @@ -2155,7 +2151,6 @@ impl NewSocket { } let global = handlers.global_object; - let this_value = this.get_this_value(&global); let output_value = match handlers.binary_type.get().to_js(data, &global) { Ok(v) => v, Err(err) => { @@ -2164,15 +2159,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(); - - // 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)]); - } - this.exit_scope(scope); + 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 8c8b8367f18e..dc2d5cd0f7df 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -9,7 +9,7 @@ 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, StringJsc, + CallFrame, JSGlobalObject, JSValue, JsError, JsRef, JsResult, MarkedArgumentBuffer, StringJsc, SysErrorJsc, SystemError, }; use bun_ptr::BackRef; @@ -821,63 +821,38 @@ impl UDPSocket { event_loop.exit(); } - #[bun_jsc::host_fn(method)] - pub(crate) 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(crate) 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, )); } @@ -895,24 +870,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(crate) 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(crate) 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, @@ -920,12 +917,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, )); } @@ -939,20 +933,15 @@ impl UDPSocket { let mut addr: sockaddr_storage = bun_core::ffi::zeroed(); if !this.parse_addr(global_this, 0, 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(global_this, 0, arguments[1], &mut interface)? @@ -962,15 +951,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) } @@ -1000,12 +986,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, )); } @@ -1022,23 +1005,17 @@ impl UDPSocket { // `assume_init()` on the full 128-byte storage would be UB. let mut source_addr: sockaddr_storage = bun_core::ffi::zeroed(); if !this.parse_addr(global_this, 0, 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, )); } let mut group_addr: sockaddr_storage = bun_core::ffi::zeroed(); if !this.parse_addr(global_this, 0, 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, )); } @@ -1050,9 +1027,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(global_this, 0, arguments[2], &mut interface)? @@ -1062,25 +1037,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) } @@ -1110,12 +1072,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, )); } @@ -1140,16 +1099,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) } @@ -1189,12 +1141,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, )); } @@ -1207,15 +1156,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 cf95b89cca37..54d02fe17886 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -3068,6 +3068,176 @@ 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. The + // `` markup lives in the template literals here; the `lead` / + // `prefix` / `which` / `tail` pieces are substituted as positional args, + // so (like user data) they are emitted verbatim and must be plain text. + + /// `.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); + throw!( + this, global, + Expect::get_signature(matcher_name, matcher_params, true), + "\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); + throw!( + this, global, signature, + "\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 { + throw!( + this, global, signature, + "\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, + }; + throw!(this, global, signature, "\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); + throw!( + this, global, signature, + "\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); + throw!( + this, global, signature, + "\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 { // `mock.results` is a user-mutable JSArray, so `value` can be anything // (`fn.mock.results.push(undefined)`); `fast_get` requires an object. diff --git a/src/runtime/test_runner/expect/toEqual.rs b/src/runtime/test_runner/expect/toEqual.rs index 7a54b6d254c3..3954194f82a4 100644 --- a/src/runtime/test_runner/expect/toEqual.rs +++ b/src/runtime/test_runner/expect/toEqual.rs @@ -10,18 +10,39 @@ impl Expect { &self, global: &JSGlobalObject, frame: &CallFrame, + ) -> JsResult { + self.equals_impl(global, frame, "toEqual", JSValue::jest_deep_equals) + } + + #[bun_jsc::host_fn(method)] + pub(crate) 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(); 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 throw!(this, global, signature, "\n\n{}\n", diff_formatter); - } - - let signature: &str = Expect::get_signature("toEqual", "expected", false); + let signature: &str = Expect::get_signature(name, "expected", not); throw!(this, global, signature, "\n\n{}\n", diff_formatter) } } diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs index a5cdfd9ae762..8d417a4054f8 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::throw; use super::Expect; @@ -33,20 +32,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; } @@ -58,50 +44,43 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global, - signature, - "\n\nExpected mock function not to have been called with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), + "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 throw!( - this, - global, - signature, - "\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 throw!(this, global, signature, "\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 03c20604f0ab..b2f33aecb194 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs @@ -1,7 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; -use super::throw; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_been_last_called_with( @@ -16,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; @@ -35,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() { @@ -53,40 +42,32 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global, - signature, - "\n\nExpected last call not to be with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), + "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 throw!( - this, - global, - signature, - "\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, - }; - throw!(this, global, signature, "\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 430840424625..a0d82527de8c 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs @@ -1,7 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::Expect; -use super::throw; pub(crate) fn to_have_been_nth_called_with( this: &Expect, @@ -14,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() { @@ -37,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!( @@ -45,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() { @@ -63,51 +51,34 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global, - signature, - "\n\nExpected call #{} not to be with: {}\nBut it was.", - nth_call_num, - expected_args_js_array.to_fmt(&mut formatter), + "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 throw!( - this, - global, - signature, - "\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, - }; - throw!( - this, + mock::throw_diff( + &this, global, signature, - "\n\nCall #{}:\n{}\n", nth_call_num, diff_format, + 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 235c6c8addcf..a9afc812a96d 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; use super::throw; @@ -18,7 +16,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(); @@ -30,24 +28,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 => {} } } @@ -56,66 +48,52 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global_this, - Expect::get_signature("toHaveBeenLastReturnedWith", "expected", true), - concat!( - "\n\n", - "Expected mock function not to have last returned: {}\n", - "But it did.\n", - ), - expected.to_fmt(&mut formatter), + "toHaveBeenLastReturnedWith", + "expected", + format_args!("Expected mock function not to have last returned"), + expected, + "\nBut it did.\n", ); } if calls_count == 0 { - return throw!( - this, - global_this, - signature, - concat!("\n\n", "The mock function was not called."), - ); + return throw!(this, global_this, signature, "\n\nThe mock function was not called."); } if last_call_threw { - return throw!( - this, + return mock::throw_call_threw( + &this, global_this, signature, - concat!("\n\n", "The last call threw an error: {}\n"), - last_error_value.to_fmt(&mut formatter), + 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 throw!(this, global_this, signature, "\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); - throw!( - this, + mock::throw_expected_received( + &this, global_this, signature, - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - last_return_value.to_fmt(&mut formatter2), + 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 72cb6905b7f4..cdeac1789966 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs @@ -1,7 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; -use super::throw; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_nth_returned_with( @@ -16,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 @@ -39,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 => {} } } @@ -66,71 +62,52 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global, - get_signature("toHaveNthReturnedWith", "n, expected", true), - "\n\nExpected mock function not to have returned on call {}: {}\nBut it did.\n", - n, - expected.to_fmt(&mut formatter), + "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 throw!( - this, - global, - signature, - "\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 throw!( - this, + return mock::throw_call_threw( + &this, global, signature, - "\n\nCall {} threw an error: {}\n", - n, - nth_error_value.to_fmt(&mut formatter), + 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 throw!( - this, + return mock::throw_diff( + &this, global, signature, - "\n\nCall {}:\n{}\n", n, diff_format, + format_args!("Call {}:\n", n), + expected, + nth_return_value, ); } - throw!( - this, + mock::throw_expected_received( + &this, global, signature, - "\n\nCall {}:\nExpected: {}\nReceived: {}", - n, - expected.to_fmt(&mut formatter), - nth_return_value.to_fmt(&mut formatter2), + 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 da5133127e52..29cad9e33005 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::throw; use super::Expect; @@ -33,25 +32,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 => {} } } @@ -60,18 +51,17 @@ 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 throw!( - this, + return mock::throw_not_failure( + &this, global, - not_signature, - "\n\nExpected mock function not to have returned: {}\n", - expected.to_fmt(&mut formatter), + "toHaveReturnedWith", + "expected", + format_args!("Expected mock function not to have returned"), + expected, + "\n", ); } @@ -82,33 +72,22 @@ 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 throw!(this, global, signature, "\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 throw!( - this, + return mock::throw_expected_received( + &this, global, signature, - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - received.to_fmt(&mut formatter2), + 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 { diff --git a/src/runtime/test_runner/expect/toStrictEqual.rs b/src/runtime/test_runner/expect/toStrictEqual.rs deleted file mode 100644 index d36615536adc..000000000000 --- a/src/runtime/test_runner/expect/toStrictEqual.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; - -use super::throw; -use super::DiffFormatter; -use super::Expect; - -impl Expect { - #[bun_jsc::host_fn(method)] - pub(crate) 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(); - - 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 throw!(this, global, signature, "\n\n{}\n", diff_formatter); - } - - let signature = Expect::get_signature("toStrictEqual", "expected", false); - throw!(this, global, signature, "\n\n{}\n", diff_formatter) - } -} diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 599b30669b38..443b57abdcaa 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -576,7 +576,6 @@ pub mod expect { "toMatchObject.rs" => to_match_object, "toMatchSnapshot.rs" => to_match_snapshot, "toSatisfy.rs" => to_satisfy, - "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 ed3cf5371ac7..41ad109556cd 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -528,20 +528,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, @@ -2736,7 +2723,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 41e8b945bc8b..ebc7ff9fd435 100644 --- a/src/runtime/valkey_jsc/js_valkey_functions.rs +++ b/src/runtime/valkey_jsc/js_valkey_functions.rs @@ -101,7 +101,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 @@ -161,15 +161,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 optionally takes +// the `CommandMeta` to send with), 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, @@ -179,84 +182,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, @@ -270,133 +212,20 @@ 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, CommandMeta::default(), $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, CommandMeta::default()); }; -} - -macro_rules! cmd_strings_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { - cmd_strings_varargs!($fn_name, $name, $command, $state, CommandMeta::default()); + ($fn_name:ident, $name:literal, $command:literal, strict, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, false, $state, CommandMeta::default()); }; - ($fn_name:ident, $name:literal, $command:literal, $state:ident, $meta:expr) => { - #[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), - $meta, - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, strict, $state:ident, $meta:expr $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, false, $state, $meta); }; -} - -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, $meta:expr $(, $arg0_name:literal)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -407,11 +236,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 { @@ -429,7 +267,7 @@ macro_rules! cmd_key_value_varargs { frame.this(), $command.as_bytes(), CommandArgs::Args(&args), - CommandMeta::default(), + $meta, concat!("Failed to send ", $command), ) } @@ -1200,27 +1038,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_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_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!(hpttl, b"hpttl", "HPTTL", strict, NotSubscriber); + cmd_varargs!(httl, b"httl", "HTTL", strict, NotSubscriber); #[bun_jsc::host_fn(method)] pub(crate) fn hsetnx( @@ -1311,12 +1167,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", @@ -1325,8 +1181,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", @@ -1335,7 +1191,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( getrange, b"getrange", "GETRANGE", @@ -1344,7 +1200,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value_value2!( + cmd!( setrange, b"setrange", "SETRANGE", @@ -1353,8 +1209,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", @@ -1362,28 +1218,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", @@ -1392,7 +1248,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lrem, b"lrem", "LREM", @@ -1401,7 +1257,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lset, b"lset", "LSET", @@ -1410,7 +1266,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( ltrim, b"ltrim", "LTRIM", @@ -1419,8 +1275,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", @@ -1428,7 +1284,7 @@ impl JSValkeyClient { "milliseconds", NotSubscriber ); - cmd_key_value!( + cmd!( pexpireat, b"pexpireat", "PEXPIREAT", @@ -1436,17 +1292,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", @@ -1454,21 +1310,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", @@ -1477,7 +1363,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zlexcount, b"zlexcount", "ZLEXCOUNT", @@ -1486,47 +1372,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", @@ -1534,7 +1422,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( incrbyfloat, b"incrbyfloat", "INCRBYFLOAT", @@ -1542,7 +1430,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( decrby, b"decrby", "DECRBY", @@ -1550,13 +1438,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", @@ -1565,7 +1453,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( psetex, b"psetex", "PSETEX", @@ -1574,8 +1462,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", @@ -1584,27 +1472,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", @@ -1645,7 +1557,7 @@ impl JSValkeyClient { ) } - cmd_key_value_value2!( + cmd!( substr, b"substr", "SUBSTR", @@ -1654,7 +1566,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value!( + cmd!( hstrlen, b"hstrlen", "HSTRLEN", @@ -1662,10 +1574,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", @@ -1674,7 +1592,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyrank, b"zremrangebyrank", "ZREMRANGEBYRANK", @@ -1683,7 +1601,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyscore, b"zremrangebyscore", "ZREMRANGEBYSCORE", @@ -1692,27 +1610,35 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zrevrank, b"zrevrank", "ZREVRANK", "key", NotSubscriber); - cmd_strings_varargs!( + cmd_varargs!( + zrevrank, + b"zrevrank", + "ZREVRANK", + required "key", + NotSubscriber + ); + cmd_varargs!( psubscribe, b"psubscribe", "PSUBSCRIBE", + strict, DontCare, CommandMeta::default() | CommandMeta::SUBSCRIPTION_REQUEST ); - cmd_strings_varargs!( + cmd_varargs!( punsubscribe, b"punsubscribe", "PUNSUBSCRIBE", + strict, DontCare, CommandMeta::default() | CommandMeta::SUBSCRIPTION_REQUEST ); - 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!(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 2a41b66e2cc4..8d000c2ee79a 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -55,7 +55,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 966f578b8088..339f194d39ae 100644 --- a/src/sourcemap_jsc/CodeCoverage.rs +++ b/src/sourcemap_jsc/CodeCoverage.rs @@ -508,6 +508,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)?; @@ -530,20 +549,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); @@ -576,20 +584,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); } @@ -622,6 +619,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 @@ -636,60 +653,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 { @@ -714,54 +692,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 diff --git a/src/url/Cargo.toml b/src/url/Cargo.toml index 3ecf09213d22..1db1fd3e7e69 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -22,6 +22,7 @@ bitflags.workspace = true bun_alloc.workspace = true bun_core.workspace = true bun_collections.workspace = true +bun_opaque.workspace = true # TODO(b1): bun_io gated — crate does not compile yet; local Write stub in lib.rs # bun_io.workspace = true bun_paths.workspace = true diff --git a/src/url/lib.rs b/src/url/lib.rs index 94860e3f1070..ac64a2ff9415 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -45,11 +45,11 @@ pub mod whatwg { use super::BunString as String; use super::strings; - /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. - /// Construct via `from_string`/`from_utf8`; free via `deinit`. - #[repr(C)] - pub struct URL { - _opaque: [u8; 0], + bun_opaque::opaque_ffi! { + /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind a pointer. + /// Construct via `from_string`/`from_utf8` (or `bun_jsc::UrlJsc::from_js`); + /// free exactly once via `deinit`/`destroy`. + pub struct URL; } // Getters take `*const URL` — the C++ side (BunString.cpp) never mutates the @@ -64,11 +64,16 @@ pub mod whatwg { safe fn URL__fromString(str: &mut String) -> Option>; 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__host(url: &URL) -> String; safe fn URL__hostname(url: &URL) -> String; + safe fn URL__port(url: &URL) -> u32; safe fn URL__deinit(url: &mut URL); safe fn URL__pathname(url: &URL) -> String; safe fn URL__getHref(input: &mut String) -> String; safe fn URL__getFileURLString(input: &mut String) -> String; + safe fn URL__pathFromFileURL(input: &mut String) -> String; safe fn URL__getHrefJoin(base: &mut String, relative: &mut String) -> String; safe fn URL__fragmentIdentifier(url: &URL) -> String; fn URL__originLength(latin1_slice: *const u8, len: usize) -> u32; @@ -114,12 +119,24 @@ pub mod whatwg { } impl URL { - pub(crate) fn from_string(str: &String) -> Option> { - let mut input = *str; + /// Returns an owned C++ heap allocation that the caller must free exactly + /// once via [`deinit`](Self::deinit) / [`destroy`](Self::destroy), or + /// `None` if `str` does not parse. + pub fn from_string(str: String) -> Option> { + let mut input = str; URL__fromString(&mut input) } + /// See [`from_string`](Self::from_string) for ownership. pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(&String::borrow_utf8(input)) + Self::from_string(String::borrow_utf8(input)) + } + /// By-value form of the free [`file_url_from_string`]. + pub fn file_url_from_string(str: String) -> String { + file_url_from_string(&str) + } + pub fn path_from_file_url(str: String) -> String { + let mut input = str; + URL__pathFromFileURL(&mut input) } /// The URL fragment (the part after `#`), excluding the leading '#'. pub fn fragment_identifier(&self) -> String { @@ -131,10 +148,27 @@ pub mod whatwg { pub fn href(&self) -> String { URL__href(self) } + pub fn username(&self) -> String { + URL__username(self) + } + pub fn password(&self) -> String { + URL__password(self) + } + /// Returns the host WITHOUT the port. + /// + /// Note that this does NOT match JS `host`, which includes the port (that + /// form is [`hostname`](Self::hostname) here). + /// + /// ```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 `hostname`, which excludes the port (that - /// port-less form is `bun_jsc::URL::host`). + /// port-less form is [`host`](Self::host) here). /// /// ```text /// URL("http://example.com:8080").hostname() => "example.com:8080" @@ -142,12 +176,28 @@ pub mod whatwg { 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) + } pub fn pathname(&self) -> String { URL__pathname(self) } pub fn deinit(&mut self) { URL__deinit(self) } + /// Raw-pointer form of [`deinit`](Self::deinit) for callers holding the + /// pointer returned by `from_string`/`from_utf8`/`from_js`. Not a `Drop`: + /// the handle is constructed and destroyed across the C++ boundary. + /// + /// # Safety + /// `this` must be a live handle returned by one of the constructors and + /// must not be used again afterwards. + pub unsafe fn destroy(this: *mut Self) { + // SAFETY: caller guarantees `this` is live and uniquely owned. + unsafe { &mut *this }.deinit() + } } } // Re-export the free helpers at crate root so lower-tier callers can write