diff --git a/Cargo.lock b/Cargo.lock index 7b8ae79d6080..90844add059f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1610,7 +1610,6 @@ dependencies = [ "bun_core", "bun_errno", "bun_http_types", - "bun_js_parser", "bun_options_types", "bun_paths", "bun_ptr", @@ -2157,6 +2156,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 17433f88d5d6..7985a4d7d0a2 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1749,16 +1749,6 @@ pub fn clear_to_end() { // - reset // - reset -/// Lowercase lookup wrapper. The table -/// itself lives in `bun_output_tags` (shared with the `pretty_fmt!` proc-macro -/// so there is exactly one copy). -pub mod color_map { - #[inline] - pub fn get(name: &[u8]) -> Option<&'static str> { - bun_output_tags::color_for_bytes(name) - } -} - pub use ansi::{BOLD, DIM, RESET}; pub use bun_output_tags::{ansi, ansi_b}; @@ -2060,82 +2050,9 @@ pub fn pretty_fmt_args( } /// Runtime `` → ANSI rewriter, used for testing the proc-macro and for -/// the rare dynamic case. -/// -/// Colour table lives in `bun_output_tags`; the state machine is kept duplicated -/// vs `bun_core_macros::rewrite` because the two intentionally diverge in the -/// `{` arm (proc-macro rewrites specs `{s}`→`{}`; this side copies braces -/// verbatim) and on unknown tags (proc-macro errors; this side emits `""`). -pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> Vec { - let mut out = Vec::with_capacity(fmt.len() * 4); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let color_name = &fmt[start..i]; - let color_str: &str = 'picker: { - if let Some(lit) = color_map::get(color_name) { - break 'picker lit; - } else if color_name == b"r" { - is_reset = true; - break 'picker ""; - } else { - // Unknown tag: the `pretty_fmt!` proc-macro rejects - // this at its call sites; this runtime path drops the - // tag. - break 'picker ""; - } - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - color_str.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} +/// the rare dynamic case. The implementation lives in `bun_output_tags` so the +/// `bun_clap_macros` proc-macro crate shares the same state machine. +pub use bun_output_tags::pretty_fmt_runtime; #[doc(hidden)] #[inline] diff --git a/src/bun_core/string/mod.rs b/src/bun_core/string/mod.rs index ef9c0306bea3..c08f25c8c043 100644 --- a/src/bun_core/string/mod.rs +++ b/src/bun_core/string/mod.rs @@ -2434,8 +2434,11 @@ pub mod printer { /// `MutableString`, and any other `crate::io::Write` sink. pub use crate::io::Write as PrinterWriter; + // PERF: `ascii_only` is a runtime arg so the large callers collapse to a + // single monomorphization instead of one per (ascii_only × quote_char × …) + // combo; see `write_pre_quoted_string_inner` for the same trade-off. #[inline] - pub(crate) fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { + pub fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { if c <= LAST_ASCII as i32 { c >= FIRST_ASCII as i32 && c != b'\\' as i32 @@ -2452,9 +2455,20 @@ pub mod printer { } } - /// Same algorithm as `bun_js_printer::write_pre_quoted_string`. - /// PERF: (quote_char, ascii_only, json, encoding) are runtime params — - /// profile if it shows up on a hot path. + /// `strings::Encoding` stand-in that derives `ConstParamTy` so it can be + /// used as a const-generic parameter (`const ENCODING: Encoding`). The + /// variant set is identical; convert at the boundary if a + /// `strings::Encoding` is ever needed. + #[derive(Clone, Copy, Debug, PartialEq, Eq, core::marker::ConstParamTy)] + pub enum Encoding { + Ascii, + Utf8, + Latin1, + Utf16, + } + + /// Runtime-encoding adapter: selects the matching monomorphized + /// [`write_pre_quoted_string_inner`] instance. pub fn write_pre_quoted_string( text_in: &[u8], writer: &mut W, @@ -2463,68 +2477,119 @@ pub mod printer { json: bool, encoding: StrEncoding, ) -> crate::CrateResult<()> { - debug_assert!(!json || quote_char == b'"'); - // utf16 view over the same bytes (only used when encoding == Utf16). - // Callers pass 2-byte-aligned even-length input for Utf16; `cast_slice` - // panics (rather than UB) if that contract is violated. - let text16: &[u16] = if encoding == StrEncoding::Utf16 { - crate::cast_slice::(text_in) - } else { - &[] - }; - let n: usize = if encoding == StrEncoding::Utf16 { - text16.len() - } else { - text_in.len() - }; + match encoding { + StrEncoding::Ascii => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf8 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Latin1 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf16 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + } + } + + /// `quote_char` / `ascii_only` / `json` are runtime args: the branches on + /// them are cheap and well-predicted, and collapsing the monomorphizations + /// keeps the hot transpile pages dense. `ENCODING` stays `const` — it + /// changes the code-unit indexing structure of the loop, so a per-encoding + /// copy is genuinely different code. + #[inline(never)] + pub fn write_pre_quoted_string_inner( + text_in: &[u8], + writer: &mut W, + quote_char: u8, + ascii_only: bool, + json: bool, + ) -> crate::CrateResult<()> + where + W: PrinterWriter + ?Sized, + { + debug_assert!( + !(json && quote_char != b'"'), + "for json, quote_char must be '\"'" + ); + + let text = text_in; let mut i: usize = 0; + let n: usize = match ENCODING { + Encoding::Utf16 => text.len() / 2, + _ => text.len(), + }; + + macro_rules! code_unit_at { + ($idx:expr) => { + match ENCODING { + Encoding::Utf16 => { + let lo = text[$idx * 2]; + let hi = text[$idx * 2 + 1]; + u16::from_le_bytes([lo, hi]) as i32 + } + _ => text[$idx] as i32, + } + }; + } while i < n { - let width: u8 = match encoding { - StrEncoding::Latin1 | StrEncoding::Ascii | StrEncoding::Utf16 => 1, - StrEncoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text_in[i]), + let width: u8 = match ENCODING { + Encoding::Latin1 | Encoding::Ascii => 1, + Encoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text[i]), + Encoding::Utf16 => 1, }; let clamped_width = (width as usize).min(n.saturating_sub(i)); - let c: i32 = match encoding { - StrEncoding::Utf8 => { - let mut buf = [0u8; 4]; - buf[..clamped_width].copy_from_slice(&text_in[i..i + clamped_width]); - strings::decode_wtf8_rune_t::(buf, width, 0) + let c: i32 = match ENCODING { + Encoding::Utf8 => { + let bytes: [u8; 4] = match clamped_width { + 1 => [text[i], 0, 0, 0], + 2 => [text[i], text[i + 1], 0, 0], + 3 => [text[i], text[i + 1], text[i + 2], 0], + 4 => [text[i], text[i + 1], text[i + 2], text[i + 3]], + _ => unreachable!(), + }; + strings::decode_wtf8_rune_t::(bytes, width, 0) + } + Encoding::Ascii => { + debug_assert!(text[i] <= 0x7F); + text[i] as i32 } - StrEncoding::Ascii => { - debug_assert!(text_in[i] <= 0x7F); - text_in[i] as i32 + Encoding::Latin1 => text[i] as i32, + Encoding::Utf16 => { + // TODO: if this is a part of a surrogate pair, we could parse the whole codepoint in order + // to emit it as a single \u{result} rather than two paired \uLOW\uHIGH. + // eg: "\u{10334}" will convert to "𐌴" without this. + code_unit_at!(i) } - StrEncoding::Latin1 => text_in[i] as i32, - StrEncoding::Utf16 => text16[i] as i32, }; if can_print_without_escape(c, ascii_only) { - match encoding { - StrEncoding::Ascii | StrEncoding::Utf8 => { - let remain = &text_in[i + clamped_width..]; + match ENCODING { + Encoding::Ascii | Encoding::Utf8 => { + let remain = &text[i + clamped_width..]; if let Some(j) = strings::index_of_needs_escape_for_java_script_string( remain, quote_char, ) { - writer.write_all(&text_in[i..i + clamped_width])?; - i += clamped_width; - writer.write_all(&remain[..j as usize])?; - i += j as usize; + let j = j as usize; + writer.write_all(&text[i..i + clamped_width + j])?; + i += clamped_width + j; } else { - writer.write_all(&text_in[i..])?; + writer.write_all(&text[i..])?; break; } } - StrEncoding::Latin1 | StrEncoding::Utf16 => { - let mut cp = [0u8; 4]; - let cp_len = strings::encode_wtf8_rune(&mut cp, c as u32); - writer.write_all(&cp[..cp_len])?; + Encoding::Latin1 | Encoding::Utf16 => { + let mut codepoint_bytes = [0u8; 4]; + let codepoint_len = + strings::encode_wtf8_rune(&mut codepoint_bytes, c as u32); + writer.write_all(&codepoint_bytes[..codepoint_len])?; i += clamped_width; } } continue; } - match c { 0x07 => { writer.write_all(b"\\x07")?; @@ -2568,11 +2633,16 @@ pub mod printer { } 0x24 => { if quote_char == b'`' { - let next_is_brace = match encoding { - StrEncoding::Utf16 => i + 1 < n && text16[i + 1] == b'{' as u16, - _ => i + 1 < n && text_in[i + 1] == b'{', + let next = if i + clamped_width < n { + Some(code_unit_at!(i + clamped_width)) + } else { + None }; - writer.write_all(if next_is_brace { b"\\$" } else { b"$" })?; + if next == Some(b'{' as i32) { + writer.write_all(b"\\$")?; + } else { + writer.write_all(b"$")?; + } } else { writer.write_all(b"$")?; } @@ -2584,6 +2654,7 @@ pub mod printer { } _ => { i += width as usize; + if c <= 0xFF && !json { let h = hex2_upper(c as u8); writer.write_all(&[b'\\', b'x', h[0], h[1]])?; @@ -2605,9 +2676,14 @@ pub mod printer { bytes: &mut MutableString, ascii_only: bool, ) -> crate::CrateResult<()> { - // PERF: consider pre-growing via an estimated UTF-8 length — profile if it shows up on a hot path. + // ~12.5% slack heuristic: tab-indented JS (three.js) escapes ~9.4% of + // bytes, so `>> 4` (6.25%) would under-shoot and force a 2x doubling + // memcpy. Writer still grows on demand if this under-shoots. + bytes.grow_if_needed(text.len() + (text.len() >> 3) + 8)?; bytes.append_char(b'"')?; - write_pre_quoted_string(text, bytes, b'"', ascii_only, true, StrEncoding::Utf8)?; + write_pre_quoted_string_inner::<_, { Encoding::Utf8 }>( + text, bytes, b'"', ascii_only, true, + )?; bytes.append_char(b'"').expect("unreachable"); Ok(()) } diff --git a/src/bun_output_tags/lib.rs b/src/bun_output_tags/lib.rs index 239866f15050..93846fee2a70 100644 --- a/src/bun_output_tags/lib.rs +++ b/src/bun_output_tags/lib.rs @@ -1,10 +1,13 @@ -//! Single source of truth for Bun's `` → ANSI colour table **and** the +//! Single source of truth for Bun's `` → ANSI colour table, the //! named ANSI escape constants used directly by REPL / diff-printer / multi-run -//! output. Zero-dep `#![no_std]` leaf so both the proc-macro crate and runtime -//! crates can import it without a cycle. +//! output, **and** the runtime `` rewriter ([`pretty_fmt_runtime`]). +//! Zero-dep `#![no_std]` (+`alloc`) leaf so both the proc-macro crates and +//! runtime crates can import it without a cycle. #![no_std] +extern crate alloc; + /// Named ANSI SGR escape sequences. One canonical literal per colour/attribute; /// every other crate aliases this module rather than re-declaring the bytes. /// @@ -100,3 +103,84 @@ pub fn color_for_bytes(name: &[u8]) -> Option<&'static str> { } None } + +/// Runtime `` → ANSI rewriter: expands tags to escape sequences when +/// `is_enabled`, or strips them when not. `\<`/`\>` escapes pass the bracket +/// through, `{…}` spec bodies are copied verbatim, `` and `` emit +/// [`RESET`], and unknown tags are dropped silently. +/// +/// Shared by `bun_core::output` (CLI logging) and `bun_clap_macros` (which runs +/// it at macro-expansion time with `is_enabled = false` to bake tag-stripped +/// help strings into rodata). The `pretty_fmt!` proc-macro keeps its own +/// deliberately divergent variant (`{s}`→`{}` spec rewriting, compile errors on +/// unknown tags). +#[inline] +pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> alloc::vec::Vec { + // `* 2` covers the worst-case expansion: the shortest tags (``, ``, + // 3 bytes) become 4-byte escapes, a ratio under 2. + let mut out = alloc::vec::Vec::with_capacity(fmt.len() * 2); + let mut i = 0usize; + while i < fmt.len() { + match fmt[i] { + b'\\' => { + i += 1; + if i < fmt.len() { + match fmt[i] { + b'<' | b'>' => { + out.push(fmt[i]); + i += 1; + } + _ => { + out.push(b'\\'); + out.push(fmt[i]); + i += 1; + } + } + } + } + b'>' => { + i += 1; + } + b'{' => { + while i < fmt.len() && fmt[i] != b'}' { + out.push(fmt[i]); + i += 1; + } + } + b'<' => { + i += 1; + let mut is_reset = i < fmt.len() && fmt[i] == b'/'; + if is_reset { + i += 1; + } + let start = i; + while i < fmt.len() && fmt[i] != b'>' { + i += 1; + } + let name = &fmt[start..i]; + let seq: &str = if let Some(c) = color_for_bytes(name) { + c + } else if name == b"r" { + is_reset = true; + "" + } else { + // Unknown tag: dropped silently (the `pretty_fmt!` + // proc-macro rejects these at compile time instead). + "" + }; + if is_enabled { + out.extend_from_slice(if is_reset { + RESET.as_bytes() + } else { + seq.as_bytes() + }); + } + } + _ => { + out.push(fmt[i]); + i += 1; + } + } + } + out +} diff --git a/src/clap_macros/lib.rs b/src/clap_macros/lib.rs index 0969a83031f5..0d2451ab89c8 100644 --- a/src/clap_macros/lib.rs +++ b/src/clap_macros/lib.rs @@ -265,91 +265,15 @@ fn byte_str_b(s: &[u8]) -> LitByteStr { LitByteStr::new(s, Span::call_site()) } -/// 1:1 port of `bun_core::output::pretty_fmt_runtime` — rewrites Bun's `` -/// colour markup to ANSI escape sequences when `is_enabled`, or strips it when -/// not. Run here at macro-expansion time with `is_enabled = false` so each param -/// description's tag-stripped form (`Help::msg_plain`) is a `const` byte literal -/// in rodata. The ANSI form is *not* baked in — it is rare (only `bun --help` on -/// a colour TTY) and would otherwise roughly triple the help-string rodata, so -/// `bun_clap::pretty_help_desc` derives it from `Help::msg` on demand instead. -fn pretty_rewrite(fmt: &[u8], is_enabled: bool) -> Vec { - use bun_output_tags::{RESET, color_for_bytes}; - let mut out: Vec = Vec::with_capacity(fmt.len() * 2); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let name = &fmt[start..i]; - let seq: &str = if let Some(c) = color_for_bytes(name) { - c - } else if name == b"r" { - is_reset = true; - "" - } else { - // Unknown tag: `pretty_fmt_runtime` (the path this replaces) - // drops it silently. Match - // the lenient runtime behaviour — a compile error would be - // stricter than what shipped, and param specs don't carry - // unknown tags anyway. - "" - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - seq.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} - fn emit_param(krate: &Path, p: &Param) -> TokenStream2 { let msg = byte_str(&p.id.msg); - // Precompute only the tag-stripped form (the non-TTY help path needs it ready - // without a TTY check); the ANSI form is derived lazily from `msg` by - // `bun_clap::pretty_help_desc`, so it stays out of rodata. - let msg_plain = byte_str_b(&pretty_rewrite(p.id.msg.as_bytes(), false)); + // Precompute only the tag-stripped form as a rodata `const` (non-TTY help + // needs it without a TTY check); the ANSI form is derived lazily by + // `bun_clap::pretty_help_desc` since baking it would ~triple help rodata. + let msg_plain = byte_str_b(&bun_output_tags::pretty_fmt_runtime( + p.id.msg.as_bytes(), + false, + )); let value = byte_str(&p.id.value); let short = match p.names.short { diff --git a/src/collections/StaticHashMap.rs b/src/collections/StaticHashMap.rs index 4769089f1c2f..b4b0a5fbf23d 100644 --- a/src/collections/StaticHashMap.rs +++ b/src/collections/StaticHashMap.rs @@ -281,10 +281,6 @@ impl< Ok(()) } - pub fn put_context(&mut self, key: K, value: V, _ctx: Ctx) -> Result<(), AllocError> { - self.put(key, value) - } - pub fn get_or_put(&mut self, key: K) -> Result, AllocError> { self.ensure_unused_capacity(1)?; Ok(self.get_or_put_assume_capacity(key)) diff --git a/src/collections/array_hash_map.rs b/src/collections/array_hash_map.rs index 8a16ba3a3ca9..2c5af57a372c 100644 --- a/src/collections/array_hash_map.rs +++ b/src/collections/array_hash_map.rs @@ -602,18 +602,6 @@ impl ArrayHashMap { self.drop_index(); } - /// Same as `ensure_total_capacity` but takes an explicit `ctx` for the - /// stored key type. Capacity reservation is purely a Vec operation here, - /// so the context is accepted and ignored. - #[inline] - pub fn ensure_total_capacity_context( - &mut self, - n: usize, - _ctx: Ctx, - ) -> Result<(), AllocError> { - self.ensure_total_capacity(n) - } - /// Insert/replace using an externally-supplied /// hash/eql context instead of the stored `C`. Used when `C = AutoContext` /// can't satisfy `K: Hash` (e.g. `bun_semver::String`, whose hash needs the @@ -1972,15 +1960,6 @@ impl StringHashMap Ok(()) } - /// Insert a pre-boxed key without re-allocating it. Uses `try_reserve` so - /// OOM surfaces as `Err` instead of aborting; callers can roll back side - /// effects on failure. - pub fn put_owned(&mut self, key: Box<[u8], A>, value: V) -> Result<(), AllocError> { - self.inner.try_reserve(1).map_err(|_| AllocError)?; - self.inner.insert(StringHashMapKey::owned(key), value); - Ok(()) - } - /// PERF: std::HashMap cannot skip the grow check, so this is /// just `put` without the `Result`. #[inline] diff --git a/src/collections/array_list.rs b/src/collections/array_list.rs index 510ddce2f969..e3a7e9badfe4 100644 --- a/src/collections/array_list.rs +++ b/src/collections/array_list.rs @@ -79,24 +79,11 @@ impl ArrayListAlignedIn { } } - // Sentinel-terminated owned slices are not a Rust type, so this takes a `Box<[T]>` with - // the sentinel already stripped. - pub fn from_owned_slice_sentinel(/* sentinel: T, */ slice: Slice) -> Self { - Self { - unmanaged: Vec::from(slice), - } - } - /// This method empties `self`. pub fn move_to_unmanaged(&mut self) -> Unmanaged { mem::take(&mut self.unmanaged) } - /// Unlike `move_to_unmanaged`, this method *consumes* `self`. - pub fn into_unmanaged_with_allocator(self) -> (Unmanaged, ()) { - (self.unmanaged, ()) - } - /// The contents of `unmanaged` must have been allocated by the global allocator. /// This function takes ownership of `unmanaged`. pub fn from_unmanaged(unmanaged: Unmanaged) -> Self { @@ -134,31 +121,6 @@ impl ArrayListAlignedIn { self.unmanaged.insert(i, item); } - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at( - &mut self, - index: usize, - value: T, - count: usize, - ) -> Result<&mut [T], AllocError> - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - Ok(&mut self.unmanaged[index..index + count]) - } - - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at_assume_capacity(&mut self, index: usize, value: T, count: usize) -> &mut [T] - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - &mut self.unmanaged[index..index + count] - } - /// Note that this `Clone`s each element of `new_items`. pub fn insert_slice(&mut self, index: usize, new_items: &[T]) -> Result<(), AllocError> where @@ -186,15 +148,6 @@ impl ArrayListAlignedIn { Ok(()) } - /// This method `Drop`s the removed items. - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn replace_range_assume_capacity(&mut self, start: usize, len: usize, new_items: &[T]) - where - T: Clone, - { - let _ = self.replace_range(start, len, new_items); - } - pub fn append(&mut self, item: T) -> Result<(), AllocError> { self.unmanaged.push(item); Ok(()) @@ -229,25 +182,6 @@ impl ArrayListAlignedIn { self.unmanaged.extend_from_slice(new_items); } - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice(&mut self, new_items: &[T]) -> Result<(), AllocError> - where - T: Clone, - { - // Rust `&[T]` is always naturally aligned, so this is identical to `append_slice`; a - // caller that truly has unaligned bytes needs `ptr::read_unaligned` at the call site. - self.unmanaged.extend_from_slice(new_items); - Ok(()) - } - - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice_assume_capacity(&mut self, new_items: &[T]) - where - T: Clone, - { - self.unmanaged.extend_from_slice(new_items); - } - /// Note that this creates *shallow* copies of `value`. #[inline] pub fn append_n_times(&mut self, value: T, n: usize) -> Result<(), AllocError> @@ -340,19 +274,6 @@ impl ArrayListAlignedIn { &items[items.len() - 1] } - pub fn get_last_mut(&mut self) -> &mut T { - let len = self.unmanaged.len(); - &mut self.unmanaged[len - 1] - } - - pub fn get_last_or_null(&self) -> Option<&T> { - if self.is_empty() { - None - } else { - Some(self.get_last()) - } - } - pub fn is_empty(&self) -> bool { self.items().is_empty() } diff --git a/src/collections/bit_set.rs b/src/collections/bit_set.rs index c1b843e51396..1674c321f8f1 100644 --- a/src/collections/bit_set.rs +++ b/src/collections/bit_set.rs @@ -317,23 +317,6 @@ impl IntegerBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(self) -> Self { - let mut result = self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(self, other: Self) -> Self { - let mut result = self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -343,24 +326,6 @@ impl IntegerBitSet { result } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(self, other: Self) -> Self { - let mut result = self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(self, other: Self) -> Self { - let mut result = self; - result.set_intersection(other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -669,23 +634,6 @@ impl ArrayBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(&self) -> Self { - let mut result = *self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -705,24 +653,6 @@ impl ArrayBitSet { false } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(&self, other: &Self) -> Self { - let mut result = *self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_intersection(&other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -823,15 +753,6 @@ impl DynamicBitSetUnmanaged { unsafe { slice::from_raw_parts_mut(self.masks, n) } } - /// Raw pointer to the mask words. Use this (not `masks_slice{,_mut}`) when - /// `self` and another `DynamicBitSetUnmanaged` may point at the same - /// storage and both are accessed in the same operation — forming - /// overlapping `&mut [usize]` / `&[usize]` would be UB. - #[inline(always)] - pub fn masks_ptr(&self) -> *mut usize { - self.masks - } - /// `self.masks[i] = f(self.masks[i], other.masks[i])` for every mask word. /// Centralises the binary set-op loop (`set_union` / `set_intersection` / /// `set_exclude` / `toggle_set` / `copy_into`) behind a single audited @@ -1149,14 +1070,6 @@ impl DynamicBitSetUnmanaged { self.zip_masks_raw(other, |a, b| a & b); } - pub fn set_exclude_two(&mut self, other: &Self, third: &Self) { - debug_assert!(other.bit_length == self.bit_length); - // Two passes is equivalent to the original fused loop: each word is - // independent, so `(a & !b) & !c` per index is associative across passes. - self.zip_masks_raw(other, |a, b| a & !b); - self.zip_masks_raw(third, |a, c| a & !c); - } - pub fn set_exclude(&mut self, other: &Self) { debug_assert!(other.bit_length == self.bit_length); self.zip_masks_raw(other, |a, b| a & !b); diff --git a/src/collections/lib.rs b/src/collections/lib.rs index 57547112bce9..76e481857433 100644 --- a/src/collections/lib.rs +++ b/src/collections/lib.rs @@ -420,13 +420,6 @@ impl SmallList { self.0.insert_many(index as usize, items.iter().cloned()) } #[inline] - pub fn insert_slice_assume_capacity(&mut self, index: u32, items: &[T]) - where - T: Clone, - { - self.0.insert_many(index as usize, items.iter().cloned()) - } - #[inline] pub fn pop(&mut self) -> Option { self.0.pop() } diff --git a/src/collections/multi_array_list.rs b/src/collections/multi_array_list.rs index 883f3d72888b..85f9d835002b 100644 --- a/src/collections/multi_array_list.rs +++ b/src/collections/multi_array_list.rs @@ -536,7 +536,7 @@ impl<'a, F> ColMut<'a, F> { } } -/// Index-based comparison context for `sort` / `sort_span` / `sort_unstable`. +/// Index-based comparison context for `sort` / `sort_unstable`. pub trait SortContext { fn less_than(&self, a_index: usize, b_index: usize) -> bool; } @@ -1208,21 +1208,11 @@ impl MultiArrayList { self.sort_internal::(0, self.len, ctx); } - /// Stable sort of `[a, b)` by index-based context. - pub fn sort_span(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - /// Unstable sort by index-based context. pub fn sort_unstable(&mut self, ctx: &C) { self.sort_internal::(0, self.len, ctx); } - /// Unstable sort of `[a, b)` by index-based context. - pub fn sort_span_unstable(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - pub fn capacity_in_bytes(capacity: usize) -> usize { Reflected::::ELEM_BYTES * capacity } diff --git a/src/collections/pool.rs b/src/collections/pool.rs index 6341d620af60..dfdba6023c14 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -66,20 +66,16 @@ impl Node { self.next = std::ptr::from_mut::>(new_node); } - /// Remove a node from the list. - /// - /// Arguments: - /// node: Pointer to the node to be removed. - /// Returns: - /// node removed - pub fn remove_next(&mut self) -> Option<*mut Node> { - let next_node = if self.next.is_null() { - return None; - } else { - self.next - }; - self.next = Node::next_of(next_node); - Some(next_node) + /// Iterate over each next node, returning the count of all nodes except the starting one. + /// This operation is O(N). + pub fn count_children(&self) -> usize { + let mut count: usize = 0; + let mut it: *const Node = self.next; + while !it.is_null() { + count += 1; + it = Node::next_of(it); + } + count } /// Iterate over the singly-linked list from this node, until the final node is found. @@ -94,18 +90,6 @@ impl Node { it = next; } } - - /// Iterate over each next node, returning the count of all nodes except the starting one. - /// This operation is O(N). - pub fn count_children(&self) -> usize { - let mut count: usize = 0; - let mut it: *const Node = self.next; - while !it.is_null() { - count += 1; - it = Node::next_of(it); - } - count - } } pub struct SinglyLinkedList { diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 194aedb197ec..b5b718ca186e 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -1496,7 +1496,7 @@ mod draft { if let Some(public_hoist_pattern_expr) = out.get(b"public-hoist-pattern") { install.public_hoist_pattern = - match pnpm_matcher_from_expr(&public_hoist_pattern_expr, log, source, bump) { + match PnpmMatcher::from_expr(&public_hoist_pattern_expr, log, source) { Ok(v) => Some(v), Err(FromExprError::OutOfMemory) => return Err(AllocError), Err(_) => { @@ -1508,16 +1508,15 @@ mod draft { } if let Some(hoist_pattern_expr) = out.get(b"hoist-pattern") { - install.hoist_pattern = - match pnpm_matcher_from_expr(&hoist_pattern_expr, log, source, bump) { - Ok(v) => Some(v), - Err(FromExprError::OutOfMemory) => return Err(AllocError), - Err(_) => { - // error.InvalidRegExp, error.UnexpectedExpr - log.reset(); - None - } - }; + install.hoist_pattern = match PnpmMatcher::from_expr(&hoist_pattern_expr, log, source) { + Ok(v) => Some(v), + Err(FromExprError::OutOfMemory) => return Err(AllocError), + Err(_) => { + // error.InvalidRegExp, error.UnexpectedExpr + log.reset(); + None + } + }; } let mut registry_map = install.scoped.take().unwrap_or_default(); @@ -1803,125 +1802,7 @@ mod draft { Ok(()) } - use bun_install_types::NodeLinker::{ - Behavior as PnpmBehavior, CreateMatcherError, FromExprError, Matcher as PnpmMatcherEntry, - PnpmMatcher, create_matcher, - }; - - /// `PnpmMatcher.fromExpr` operating on - /// `bun_ast::Expr` instead of the lower-tier `bun_ast::Expr`. - /// - /// `bun_install_types` (T2) cannot depend on `bun_js_parser` (T4), - /// and the two `ExprData` enums are distinct (closed Rust enums; only the leaf - /// `E::*` payloads are shared). `bun_ini` depends on both, so the T4-typed - /// overload lives here. The matcher construction is delegated to the shared - /// `create_matcher` helper in `bun_install_types::NodeLinker`. - fn pnpm_matcher_from_expr( - expr: &Expr, - log: &mut Log, - source: &Source, - bump: &Arena, - ) -> Result { - let mut buf: Vec = Vec::new(); - - // bun.jsc.initialize(false) is performed lazily inside the regex vtable - // compile hook (tier-6 owns it). - - let mut matchers: Vec = Vec::new(); - let mut has_include = false; - let mut has_exclude = false; - - match &expr.data { - ExprData::EString(s) => { - // SAFETY: arena-backed `EString::slice` mutates only its own - // resolved-data cache; the StoreRef pointee outlives this call. - let s_mut: &mut E::EString = unsafe { &mut *s.as_ptr() }; - let pattern = s_mut.slice(bump); - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory), - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } - ExprData::EArray(patterns) => { - for pattern_expr in patterns.items.slice() { - if let Some(pattern) = pattern_expr.as_string_cloned(bump)? { - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => { - return Err(FromExprError::OutOfMemory); - } - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } else { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - } - _ => { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - - let behavior = if !has_include { - PnpmBehavior::AllMatchersExclude - } else if !has_exclude { - PnpmBehavior::AllMatchersInclude - } else { - PnpmBehavior::HasExcludeAndIncludeMatchers - }; - - Ok(PnpmMatcher { - matchers: matchers.into_boxed_slice(), - behavior, - }) - } + use bun_install_types::NodeLinker::{FromExprError, PnpmMatcher}; fn handle_auth( v: &mut NpmRegistry, diff --git a/src/install/PackageInstall.rs b/src/install/PackageInstall.rs index d21790e80947..3384028992bd 100644 --- a/src/install/PackageInstall.rs +++ b/src/install/PackageInstall.rs @@ -1324,232 +1324,153 @@ impl<'a> PackageInstall<'a> { return res; } - #[cfg(windows)] - type WinSlice<'b> = &'b mut [u16]; #[cfg(not(windows))] - type WinSlice<'b> = (); - #[cfg(windows)] - type WinOffset = usize; - #[cfg(not(windows))] - type WinOffset = (); - - // Two overlapping slices into the same buffer (`head` is the whole - // buffer, `to_copy_into` is its tail) would be two live aliasing - // `&mut [u16]`, which is UB — pass head buffer + tail offset and - // reslice inside. fn copy( destination_dir_: &Dir, walker: &mut Walker, mut progress_: Option<&mut Progress>, - to_copy_into1_offset: WinOffset, - head1: WinSlice<'_>, - to_copy_into2_offset: WinOffset, - head2: WinSlice<'_>, ) -> crate::Result { - #[cfg(not(windows))] let mut real_file_count: u32 = 0; - #[cfg(windows)] - let real_file_count: u32 = 0; - #[cfg(not(windows))] let mut copy_file_state = bun_sys::copy_file::CopyFileState::default(); - #[cfg(not(windows))] - let _ = (to_copy_into1_offset, head1, to_copy_into2_offset, head2); while let Some(entry) = walker.next()? { - #[cfg(windows)] - { - use bun_sys::windows::{self, Win32ErrorExt as _}; - match entry.kind { - EntryKind::Directory | EntryKind::File => {} - _ => continue, - } - - if entry.path.len() > head1.len() - to_copy_into1_offset - || entry.path.len() > head2.len() - to_copy_into2_offset - { - return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); - } - - let dest_len = to_copy_into1_offset + entry.path.len(); - head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); - head1[dest_len] = 0; - let dest = bun_core::WStr::from_buf(head1, dest_len); + if entry.kind != EntryKind::File { + continue; + } + real_file_count += 1; - let src_len = to_copy_into2_offset + entry.path.len(); - head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); - head2[src_len] = 0; - let src = bun_core::WStr::from_buf(head2, src_len); + let in_file = sys::openat(entry.dir, entry.basename, sys::O::RDONLY, 0)?; + let _close_in = sys::CloseOnDrop::new(in_file); - match entry.kind { - EntryKind::Directory => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built - // into head1/head2 above. - if unsafe { - windows::CreateDirectoryExW( - src.as_ptr(), - dest.as_ptr(), - core::ptr::null_mut(), - ) - } == 0 - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir_, - entry.path.as_slice(), - ); - } + bun_output::scoped_log!( + install, + "createFile {} {}\n", + destination_dir_.fd(), + bstr::BStr::new(entry.path.as_bytes()) + ); + // Open O_WRONLY|O_CREAT|O_TRUNC, mode 0o666. + let create = |path: &ZStr| { + sys::openat( + destination_dir_.fd(), + path, + sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, + 0o666, + ) + }; + let outfile = match create(entry.path) { + Ok(f) => f, + Err(_) => 'brk: { + let entry_dirname = bun_paths::resolve_path::dirname::< + bun_paths::platform::Auto, + >(entry.path.as_bytes()); + if !entry_dirname.is_empty() { + let _ = bun_sys::MakePath::make_path::( + destination_dir_, + entry_dirname, + ); } - EntryKind::File => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. - if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } == 0 { - if let Some(entry_dirname) = - bun_paths::Dirname::dirname_u16(entry.path.as_slice()) - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir_, - entry_dirname, - ); - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. - if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } - != 0 - { - continue; - } - } - - if let Some(progress) = progress_.as_deref_mut() { + match create(entry.path) { + Ok(f) => break 'brk f, + Err(err) => { + if let Some(progress) = progress_ { progress.root.end(); progress.refresh(); } - if let Some(err) = windows::Win32Error::get().to_system_errno() { - bun_core::pretty_errorln!( - "{}: copying file {}", - <&'static str>::from(err), - bun_core::fmt::fmt_os_path( - entry.path.as_slice(), - Default::default() - ) - ); - } else { - bun_core::pretty_errorln!( - "error copying file {}", - bun_core::fmt::fmt_os_path( - entry.path.as_slice(), - Default::default() - ) - ); - } - + bun_core::pretty_errorln!( + "{}: copying file {}", + bstr::BStr::new(err.name()), + bun_core::fmt::fmt_os_path( + entry.path.as_bytes(), + Default::default() + ) + ); Global::crash(); } } - _ => unreachable!(), // handled above } - } - #[cfg(not(windows))] + }; + let _close_out = sys::CloseOnDrop::new(outfile); + + #[cfg(unix)] { - if entry.kind != EntryKind::File { + let Ok(stat) = sys::fstat(in_file) else { continue; - } - real_file_count += 1; + }; + // `sys::fchmod` is the safe by-value-fd wrapper (kernel + // validates the fd; no memory-safety preconditions). + // Result intentionally ignored. + let _ = sys::fchmod(outfile, stat.st_mode as bun_sys::Mode); + } - let in_file = sys::openat(entry.dir, entry.basename, sys::O::RDONLY, 0)?; - let _close_in = sys::CloseOnDrop::new(in_file); + if let Err(err) = + bun_sys::copy_file::copy_file_with_state(in_file, outfile, &mut copy_file_state) + { + if let Some(progress) = progress_.as_deref_mut() { + progress.root.end(); + progress.refresh(); + } - bun_output::scoped_log!( - install, - "createFile {} {}\n", - destination_dir_.fd(), - bstr::BStr::new(entry.path.as_bytes()) + bun_core::pretty_errorln!( + "{}: copying file {}", + bstr::BStr::new(err.name()), + bun_core::fmt::fmt_os_path(entry.path.as_bytes(), Default::default()) ); - // Open O_WRONLY|O_CREAT|O_TRUNC, mode 0o666. - let create = |path: &ZStr| { - sys::openat( - destination_dir_.fd(), - path, - sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, - 0o666, - ) - }; - let outfile = match create(entry.path) { - Ok(f) => f, - Err(_) => 'brk: { - let entry_dirname = bun_paths::resolve_path::dirname::< - bun_paths::platform::Auto, - >(entry.path.as_bytes()); - if !entry_dirname.is_empty() { - let _ = bun_sys::MakePath::make_path::( - destination_dir_, - entry_dirname, - ); - } - match create(entry.path) { - Ok(f) => break 'brk f, - Err(err) => { - if let Some(progress) = progress_ { - progress.root.end(); - progress.refresh(); - } + Global::crash(); + } + } - bun_core::pretty_errorln!( - "{}: copying file {}", - bstr::BStr::new(err.name()), - bun_core::fmt::fmt_os_path( - entry.path.as_bytes(), - Default::default() - ) - ); - Global::crash(); - } + Ok(real_file_count) + } + + #[cfg(windows)] + let result = { + use bun_sys::windows::{self, Win32ErrorExt as _}; + let destination_dir_ = &state.subdir; + let mut progress_ = self.progress.as_deref_mut(); + walk_install_dir_windows( + destination_dir_, + state.walker.as_mut().unwrap(), + state.to_copy_buf_off, + &mut state.buf[..], + state.to_copy_buf2_off, + &mut state.buf2[..], + |dest, src, entry_path| { + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. + if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } == 0 { + if let Some(entry_dirname) = bun_paths::Dirname::dirname_u16(entry_path) { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir_, entry_dirname); + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. + if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } != 0 { + return Ok(()); } } - }; - let _close_out = sys::CloseOnDrop::new(outfile); - - #[cfg(unix)] - { - let Ok(stat) = sys::fstat(in_file) else { - continue; - }; - // `sys::fchmod` is the safe by-value-fd wrapper (kernel - // validates the fd; no memory-safety preconditions). - // Result intentionally ignored. - let _ = sys::fchmod(outfile, stat.st_mode as bun_sys::Mode); - } - if let Err(err) = bun_sys::copy_file::copy_file_with_state( - in_file, - outfile, - &mut copy_file_state, - ) { if let Some(progress) = progress_.as_deref_mut() { progress.root.end(); progress.refresh(); } - bun_core::pretty_errorln!( - "{}: copying file {}", - bstr::BStr::new(err.name()), - bun_core::fmt::fmt_os_path(entry.path.as_bytes(), Default::default()) - ); + if let Some(err) = windows::Win32Error::get().to_system_errno() { + bun_core::pretty_errorln!( + "{}: copying file {}", + <&'static str>::from(err), + bun_core::fmt::fmt_os_path(entry_path, Default::default()) + ); + } else { + bun_core::pretty_errorln!( + "error copying file {}", + bun_core::fmt::fmt_os_path(entry_path, Default::default()) + ); + } + Global::crash(); } - } - } - - Ok(real_file_count) - } - - #[cfg(windows)] - let result = copy( - &state.subdir, - state.walker.as_mut().unwrap(), - self.progress.as_deref_mut(), - state.to_copy_buf_off, - &mut state.buf[..], - state.to_copy_buf2_off, - &mut state.buf2[..], - ); + Ok(()) + }, + ) + }; #[cfg(not(windows))] let result = copy( &state.subdir, @@ -1557,10 +1478,6 @@ impl<'a> PackageInstall<'a> { // (`state.walker()` would reborrow `&mut state` and conflict). state.walker.as_mut().unwrap(), self.progress.as_deref_mut(), - (), - (), - (), - (), ); self.file_count = match result { @@ -1795,37 +1712,18 @@ impl<'a> PackageInstall<'a> { } } - #[cfg(windows)] - type WinSlice<'b> = &'b mut [u16]; - #[cfg(not(windows))] - type WinSlice<'b> = (); - #[cfg(windows)] - type WinOffset = usize; - #[cfg(not(windows))] - type WinOffset = (); - #[cfg(windows)] - type Head2Char = u16; - #[cfg(not(windows))] - type Head2Char = u8; - // Two overlapping slices into the same buffer (`head` is the whole // buffer, `to_copy_into` is its tail) would be two live aliasing // `&mut`, which is UB — pass head buffer + tail offset and reslice // inside. + #[cfg(not(windows))] fn copy( destination_dir: &Dir, walker: &mut Walker, - to_copy_into1_offset: WinOffset, - head1: WinSlice<'_>, to_copy_into2_offset: usize, - head2: &mut [Head2Char], + head2: &mut [u8], ) -> crate::Result { - #[cfg(not(windows))] let mut real_file_count: u32 = 0; - #[cfg(windows)] - let real_file_count: u32 = 0; - #[cfg(not(windows))] - let _ = (to_copy_into1_offset, head1); while let Some(entry) = walker.next()? { #[cfg(unix)] { @@ -1860,99 +1758,51 @@ impl<'a> PackageInstall<'a> { _ => {} } } - #[cfg(not(unix))] - { - use bun_sys::windows; - match entry.kind { - EntryKind::Directory | EntryKind::File => {} - _ => continue, - } - - if entry.path.len() > head1.len() - to_copy_into1_offset - || entry.path.len() > head2.len() - to_copy_into2_offset - { - return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); - } - - let dest_len = to_copy_into1_offset + entry.path.len(); - head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); - head1[dest_len] = 0; - let dest = bun_core::WStr::from_buf(head1, dest_len); + } - let src_len = to_copy_into2_offset + entry.path.len(); - head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); - head2[src_len] = 0; - let src = bun_core::WStr::from_buf(head2, src_len); + Ok(real_file_count) + } - match entry.kind { - EntryKind::Directory => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built - // into head1/head2 above. - if unsafe { - windows::CreateDirectoryExW( - src.as_ptr(), - dest.as_ptr(), - core::ptr::null_mut(), - ) - } == 0 - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir, - entry.path.as_slice(), - ); + #[cfg(windows)] + let result = { + let destination_dir = &state.subdir; + walk_install_dir_windows( + destination_dir, + state.walker.as_mut().unwrap(), + state.to_copy_buf_off, + &mut state.buf[..], + state.to_copy_buf2_off, + &mut state.buf2[..], + |dest, src, entry_path| match sys::symlink_w(dest, src, Default::default()) { + Err(err) => { + if let Some(entry_dirname) = bun_paths::Dirname::dirname_u16(entry_path) { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir, entry_dirname); + if sys::symlink_w(dest, src, Default::default()).is_ok() { + return Ok(()); } } - EntryKind::File => match sys::symlink_w(dest, src, Default::default()) { - Err(err) => { - if let Some(entry_dirname) = - bun_paths::Dirname::dirname_u16(entry.path.as_slice()) - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir, - entry_dirname, - ); - if sys::symlink_w(dest, src, Default::default()).is_ok() { - continue; - } - } - if PackageManager::verbose_install() { - bun_core::run_once! {{ - bun_core::warn!( - "CreateHardLinkW failed, falling back to CopyFileW: {} -> {}\n", - bun_core::fmt::fmt_os_path(src.as_slice(), Default::default()), - bun_core::fmt::fmt_os_path(dest.as_slice(), Default::default()), - ); - }} - } + if PackageManager::verbose_install() { + bun_core::run_once! {{ + bun_core::warn!( + "CreateHardLinkW failed, falling back to CopyFileW: {} -> {}\n", + bun_core::fmt::fmt_os_path(src.as_slice(), Default::default()), + bun_core::fmt::fmt_os_path(dest.as_slice(), Default::default()), + ); + }} + } - return Err(err.into()); - } - Ok(_) => {} - }, - _ => unreachable!(), // handled above + Err(err.into()) } - } - } - - Ok(real_file_count) - } - - #[cfg(windows)] - let result = copy( - &state.subdir, - state.walker.as_mut().unwrap(), - state.to_copy_buf_off, - &mut state.buf[..], - state.to_copy_buf2_off, - &mut state.buf2[..], - ); + Ok(_) => Ok(()), + }, + ) + }; #[cfg(not(windows))] let result = copy( &state.subdir, state.walker.as_mut().unwrap(), - (), - (), to_copy_buf2_offset, &mut buf2[..], ); @@ -2557,3 +2407,66 @@ impl<'a> PackageInstall<'a> { } type Walker = walker_skippable::Walker; + +/// Shared Windows directory walk for the copyfile/symlink install backends: +/// builds NUL-terminated wide dest/src paths for each entry, creates +/// directories (`CreateDirectoryExW` with a `make_path` fallback), and calls +/// `per_file(dest, src, entry_path)` for each file. +/// +/// Two overlapping slices into the same buffer (`head` is the whole buffer, +/// `to_copy_into` is its tail) would be two live aliasing `&mut [u16]`, which +/// is UB — pass head buffer + tail offset and reslice inside. +#[cfg(windows)] +fn walk_install_dir_windows( + destination_dir: &Dir, + walker: &mut Walker, + to_copy_into1_offset: usize, + head1: &mut [u16], + to_copy_into2_offset: usize, + head2: &mut [u16], + mut per_file: impl FnMut(&bun_core::WStr, &bun_core::WStr, &[u16]) -> crate::Result<()>, +) -> crate::Result { + use bun_sys::windows; + + while let Some(entry) = walker.next()? { + match entry.kind { + EntryKind::Directory | EntryKind::File => {} + _ => continue, + } + + if entry.path.len() > head1.len() - to_copy_into1_offset + || entry.path.len() > head2.len() - to_copy_into2_offset + { + return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); + } + + let dest_len = to_copy_into1_offset + entry.path.len(); + head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); + head1[dest_len] = 0; + let dest = bun_core::WStr::from_buf(head1, dest_len); + + let src_len = to_copy_into2_offset + entry.path.len(); + head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); + head2[src_len] = 0; + let src = bun_core::WStr::from_buf(head2, src_len); + + match entry.kind { + EntryKind::Directory => { + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built + // into head1/head2 above. + if unsafe { + windows::CreateDirectoryExW(src.as_ptr(), dest.as_ptr(), core::ptr::null_mut()) + } == 0 + { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir, entry.path.as_slice()); + } + } + EntryKind::File => per_file(dest, src, entry.path.as_slice())?, + _ => unreachable!(), // handled above + } + } + + // Windows installs don't track a real file count. + Ok(0) +} diff --git a/src/install/PackageInstaller.rs b/src/install/PackageInstaller.rs index ccc1b663f85f..a01d6e203ca4 100644 --- a/src/install/PackageInstaller.rs +++ b/src/install/PackageInstaller.rs @@ -367,6 +367,16 @@ pub(crate) fn alias_is_safe_install_target(alias: &[u8]) -> bool { component_count == 1 || (component_count == 2 && alias[0] == b'@') } +/// Where to record a newly trusted dependency once its lifecycle scripts are +/// enqueued. +#[derive(Clone, Copy)] +struct TrustedDepRecord { + /// Add the alias to `trusted_deps_to_add_to_package_json`. + package_json: bool, + /// Add the alias to the lockfile's `trusted_dependencies`. + lockfile: bool, +} + impl<'a> PackageInstaller<'a> { // ────────────────────────────────────────────────────────────────────── // BACKREF accessors @@ -1840,74 +1850,20 @@ impl<'a> PackageInstaller<'a> { if resolution.tag != resolution::Tag::Root && (resolution.tag == resolution::Tag::Workspace || is_trusted) { - let mut folder_path = - AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); - // `defer folder_path.deinit()` — AbsPath impls Drop. - folder_path - .append(alias.slice(string_buf!())) - .unwrap_or_oom(); - - 'enqueue_lifecycle_scripts: { - if self - .manager() - .postinstall_optimizer - .should_ignore_lifecycle_scripts( - &postinstall_optimizer::PkgInfo { - name_hash: pkg_name_hash, - version: if resolution.tag == resolution::Tag::Npm { - Some(resolution.npm().version) - } else { - None - }, - version_buf: string_buf!(), - }, - self.lockfile().packages.items_resolutions() - [package_id as usize] - .get(self.lockfile().buffers.resolutions.as_slice()), - self.lockfile().packages.items_meta(), - self.manager().options.cpu, - self.manager().options.os, - Some(self.current_tree_id), - ) - { - if PackageManager::verbose_install() { - bun_core::pretty_errorln!( - "[Lifecycle Scripts] ignoring {} lifecycle scripts", - bstr::BStr::new(pkg_name.slice(string_buf!())), - ); - } - break 'enqueue_lifecycle_scripts; - } - - if self.enqueue_lifecycle_scripts( - alias.slice(string_buf!()), - log_level, - &mut folder_path, - package_id, - dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), - resolution, - ) { - if is_trusted_through_update_request { - self.manager_mut() - .trusted_deps_to_add_to_package_json - .push(Box::<[u8]>::from(alias.slice(string_buf!()))); - - if self.lockfile().trusted_dependencies.is_none() { - self.lockfile_mut().trusted_dependencies = - Some(Default::default()); - } - self.lockfile_mut() - .trusted_dependencies - .as_mut() - .unwrap() - .put( - truncated_dep_name_hash, - Box::<[u8]>::from(alias.slice(string_buf!())), - ) - .unwrap_or_oom(); - } - } - } + self.enqueue_lifecycle_scripts_for_trusted( + log_level, + package_id, + pkg_name, + pkg_name_hash, + alias, + truncated_dep_name_hash, + dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), + resolution, + TrustedDepRecord { + package_json: is_trusted_through_update_request, + lockfile: is_trusted_through_update_request, + }, + ); } match resolution.tag { @@ -2152,73 +2108,20 @@ impl<'a> PackageInstaller<'a> { }; if resolution.tag != resolution::Tag::Root && is_trusted { - let mut folder_path = - AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); - folder_path - .append(alias.slice(string_buf!())) - .unwrap_or_oom(); - - 'enqueue_lifecycle_scripts: { - if self - .manager() - .postinstall_optimizer - .should_ignore_lifecycle_scripts( - &postinstall_optimizer::PkgInfo { - name_hash: pkg_name_hash, - version: if resolution.tag == resolution::Tag::Npm { - Some(resolution.npm().version) - } else { - None - }, - version_buf: string_buf!(), - }, - self.lockfile().packages.items_resolutions()[package_id as usize] - .get(self.lockfile().buffers.resolutions.as_slice()), - self.lockfile().packages.items_meta(), - self.manager().options.cpu, - self.manager().options.os, - Some(self.current_tree_id), - ) - { - if PackageManager::verbose_install() { - bun_core::pretty_errorln!( - "[Lifecycle Scripts] ignoring {} lifecycle scripts", - bstr::BStr::new(pkg_name.slice(string_buf!())), - ); - } - break 'enqueue_lifecycle_scripts; - } - - if self.enqueue_lifecycle_scripts( - alias.slice(string_buf!()), - log_level, - &mut folder_path, - package_id, - dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), - resolution, - ) { - if is_trusted_through_update_request { - self.manager_mut() - .trusted_deps_to_add_to_package_json - .push(Box::<[u8]>::from(alias.slice(string_buf!()))); - } - - if add_to_lockfile { - if self.lockfile().trusted_dependencies.is_none() { - self.lockfile_mut().trusted_dependencies = Some(Default::default()); - } - self.lockfile_mut() - .trusted_dependencies - .as_mut() - .unwrap() - .put( - truncated_dep_name_hash, - Box::<[u8]>::from(alias.slice(string_buf!())), - ) - .unwrap_or_oom(); - } - } - } + self.enqueue_lifecycle_scripts_for_trusted( + log_level, + package_id, + pkg_name, + pkg_name_hash, + alias, + truncated_dep_name_hash, + dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), + resolution, + TrustedDepRecord { + package_json: is_trusted_through_update_request, + lockfile: add_to_lockfile, + }, + ); } // `destination_dir` is `LazyPackageDestinationDir::NodeModulesPath` @@ -2248,6 +2151,98 @@ impl<'a> PackageInstaller<'a> { ); } + /// Enqueue lifecycle scripts for a trusted (or workspace) dependency, then + /// record it in `trusted_deps_to_add_to_package_json` and/or the lockfile's + /// `trusted_dependencies` as requested. + fn enqueue_lifecycle_scripts_for_trusted( + &mut self, + log_level: Options::LogLevel, + package_id: PackageID, + pkg_name: String, + pkg_name_hash: PackageNameHash, + alias: String, + truncated_dep_name_hash: TruncatedPackageNameHash, + optional: bool, + resolution: &Resolution, + record: TrustedDepRecord, + ) { + // SAFETY: `buffers.string_bytes` is append-only and never freed + // for the lifetime of this `PackageInstaller`. + let string_buf_ptr = + bun_ptr::RawSlice::new(self.lockfile().buffers.string_bytes.as_slice()); + macro_rules! string_buf { + () => { + string_buf_ptr.slice() + }; + } + + let mut folder_path = AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); + // `defer folder_path.deinit()` — AbsPath impls Drop. + folder_path + .append(alias.slice(string_buf!())) + .unwrap_or_oom(); + + if self + .manager() + .postinstall_optimizer + .should_ignore_lifecycle_scripts( + &postinstall_optimizer::PkgInfo { + name_hash: pkg_name_hash, + version: if resolution.tag == resolution::Tag::Npm { + Some(resolution.npm().version) + } else { + None + }, + version_buf: string_buf!(), + }, + self.lockfile().packages.items_resolutions()[package_id as usize] + .get(self.lockfile().buffers.resolutions.as_slice()), + self.lockfile().packages.items_meta(), + self.manager().options.cpu, + self.manager().options.os, + Some(self.current_tree_id), + ) + { + if PackageManager::verbose_install() { + bun_core::pretty_errorln!( + "[Lifecycle Scripts] ignoring {} lifecycle scripts", + bstr::BStr::new(pkg_name.slice(string_buf!())), + ); + } + return; + } + + if self.enqueue_lifecycle_scripts( + alias.slice(string_buf!()), + log_level, + &mut folder_path, + package_id, + optional, + resolution, + ) { + if record.package_json { + self.manager_mut() + .trusted_deps_to_add_to_package_json + .push(Box::<[u8]>::from(alias.slice(string_buf!()))); + } + + if record.lockfile { + if self.lockfile().trusted_dependencies.is_none() { + self.lockfile_mut().trusted_dependencies = Some(Default::default()); + } + self.lockfile_mut() + .trusted_dependencies + .as_mut() + .unwrap() + .put( + truncated_dep_name_hash, + Box::<[u8]>::from(alias.slice(string_buf!())), + ) + .unwrap_or_oom(); + } + } + } + /// returns true if scripts are enqueued fn enqueue_lifecycle_scripts( &mut self, diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 929dd885bc4b..4dc657df97b5 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -1412,6 +1412,129 @@ pub fn get() -> *mut PackageManager { // init // ────────────────────────────────────────────────────────────────────────── +/// Placement-writes one field of the `PackageManager` singleton through the +/// raw pointer `$p` — see the PERF NOTE in [`init`] for why the struct must +/// not be built by value. +macro_rules! wr { + ($p:ident, $field:ident, $val:expr) => { + core::ptr::addr_of_mut!((*$p).$field).write($val) + }; +} + +/// Writes the `PackageManager` fields that take identical default values in +/// both init paths ([`init`] and [`init_with_runtime_once`]). Each caller +/// writes the remaining (divergent) fields itself; together they must fully +/// initialize the singleton. +/// +/// Uses per-field placement writes — see the PERF NOTE in [`init`] for why +/// the struct must not be built by value. +/// +/// # Safety +/// `p` must point to the allocated (possibly uninitialized) singleton from +/// `allocate_package_manager()`, with no other references to it live. +unsafe fn write_shared_default_fields(p: *mut PackageManager) { + // SAFETY: caller guarantees `p` is valid for per-field placement writes. + unsafe { + // The two large pools: in-place init that only zeros the 256 B + // occupancy bitset and leaves `[MaybeUninit; N]` untouched — no + // stack temporary, no memcpy. + PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( + (*p).preallocated_network_tasks + )); + PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( + (*p).preallocated_resolve_tasks + )); + + wr!(p, cache_directory_, None); + wr!(p, cache_directory_path, ZBox::from_bytes(b"")); + wr!( + p, + active_lifecycle_scripts, + crate::lifecycle_script_runner::List { + root: core::ptr::null_mut(), + // `lifecycle_script_runner::List`'s heap comparator never + // dereferences its context arg, so it is modeled as a ZST + // (`StartedAtCtx`) instead of threading a back-pointer. + context: crate::lifecycle_script_runner::StartedAtCtx, + } + ); + wr!(p, network_task_fifo, NetworkQueue::init()); + wr!(p, patch_task_fifo, PatchTaskFifo::init()); + wr!(p, ast_arena, bun_alloc::Arena::new()); + wr!(p, resolve_tasks, ResolveTaskQueue::default()); + // `Lockfile` contains `HashMap`/`Vec`/`NonNull` fields, so a + // zero-bit pattern is UB; allocate the real (empty) lockfile here directly. + // `Lockfile::default()` ≡ `Lockfile::init_empty()`. + wr!(p, lockfile, Box::new(Lockfile::default())); + wr!(p, timestamp_for_manifest_cache_control, 0); + wr!(p, extracted_count, 0); + wr!(p, default_features, Features::default()); + wr!(p, summary, Default::default()); + wr!(p, progress, Progress::default()); + wr!(p, downloads_node, None); + wr!(p, scripts_node, None); + wr!(p, progress_name_buf, [0; 768]); + wr!(p, progress_name_buf_dynamic, Vec::new()); + wr!(p, track_installed_bin, TrackInstalledBin::None); + wr!(p, root_progress_node, core::ptr::null_mut()); + wr!(p, to_update, false); + wr!(p, update_requests, Box::default()); + wr!(p, root_package_id, RootPackageId::default()); + wr!(p, task_batch, thread_pool::Batch::default()); + wr!(p, task_queue, TaskDependencyQueue::default()); + wr!(p, manifests, PackageManifestMap::default()); + wr!(p, folders, Default::default()); + wr!(p, git_repositories, RepositoryMap::default()); + wr!(p, network_dedupe_map, Default::default()); + wr!( + p, + async_network_task_queue, + AsyncNetworkTaskQueue::default() + ); + wr!(p, network_tarball_batch, thread_pool::Batch::default()); + wr!(p, network_resolve_batch, thread_pool::Batch::default()); + wr!(p, patch_apply_batch, thread_pool::Batch::default()); + wr!(p, patch_calc_hash_batch, thread_pool::Batch::default()); + wr!(p, patch_task_queue, PatchTaskQueue::default()); + wr!(p, pending_pre_calc_hashes, AtomicU32::new(0)); + wr!(p, pending_tasks, AtomicU32::new(0)); + wr!(p, total_tasks, 0); + wr!( + p, + lifecycle_script_time_log, + LifecycleScriptTimeLog::default() + ); + wr!(p, pending_lifecycle_script_tasks, AtomicU32::new(0)); + wr!(p, finished_installing, AtomicBool::new(false)); + wr!(p, total_scripts, 0); + wr!(p, root_lifecycle_scripts, None); + wr!(p, node_gyp_tempdir_name, Box::default()); + wr!(p, preinstall_state, Vec::new()); + wr!(p, postinstall_optimizer, Default::default()); + wr!(p, global_link_dir, None); + wr!(p, global_dir, None); + wr!(p, global_link_dir_path, Box::default()); + wr!(p, on_wake, WakeHandler::default()); + wr!( + p, + ci_mode, + LazyBool::new(PackageManager::compute_is_continuous_integration) + ); + wr!( + p, + peer_dependencies, + LinearFifo::>::init() + ); + wr!(p, known_npm_aliases, NpmAliasMap::default()); + wr!(p, trusted_deps_to_add_to_package_json, Vec::new()); + wr!(p, any_failed_to_install, false); + wr!(p, updating_packages, StringArrayHashMap::default()); + wr!(p, patched_dependencies_to_remove, ArrayHashMap::default()); + wr!(p, last_reported_slow_lifecycle_script_at, 0); + wr!(p, cached_tick_for_slow_lifecycle_script_logging, 0); + } +} + /// Returns `&'static mut PackageManager` — the process-singleton (held in /// `holder::RAW_PTR`) is leaked for the process lifetime and `init()` is called /// exactly once on the single CLI dispatch thread. Every @@ -1924,129 +2047,45 @@ pub fn init( // directly to the heap and keeps the frame under 16 KB. unsafe { let p = manager_ptr; - macro_rules! wr { - ($field:ident, $val:expr) => { - core::ptr::addr_of_mut!((*p).$field).write($val) - }; - } - // The two large pools: in-place init that only zeros the 256 B - // occupancy bitset and leaves `[MaybeUninit; N]` untouched — no - // stack temporary, no memcpy. - PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_network_tasks - )); - PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_resolve_tasks - )); + write_shared_default_fields(p); - wr!(cache_directory_, None); - wr!(cache_directory_path, ZBox::from_bytes(b"")); - wr!(options, options); - wr!( - active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - // `lifecycle_script_runner::List`'s heap comparator never - // dereferences its context arg, so it is modeled as a ZST - // (`StartedAtCtx`) instead of threading a back-pointer. - context: crate::lifecycle_script_runner::StartedAtCtx, - } - ); - wr!(network_task_fifo, NetworkQueue::init()); - wr!(patch_task_fifo, PatchTaskFifo::init()); - wr!(log, ctx.log); - wr!(root_dir, entries_option); - wr!(ast_arena, bun_alloc::Arena::new()); + wr!(p, options, options); + wr!(p, log, ctx.log); + wr!(p, root_dir, entries_option); // reborrow `&mut *env` so the local stays usable for // the post-construction `BUN_MANIFEST_CACHE` / `options.load` // reads. `BackRef` stores a raw pointer — // ending the reborrow here does not alias the later uses. - wr!(env, Some(bun_ptr::BackRef::new_mut(&mut *env))); - wr!(cpu_count, cpu_count); + wr!(p, env, Some(bun_ptr::BackRef::new_mut(&mut *env))); + wr!(p, cpu_count, cpu_count); wr!( + p, thread_pool, ThreadPool::init(thread_pool::Config { max_threads: cpu_count, ..Default::default() }) ); - wr!(resolve_tasks, ResolveTaskQueue::default()); - // `Lockfile` contains `HashMap`/`Vec`/`NonNull` fields, so a - // zero-bit pattern is UB; allocate the real (empty) lockfile here directly. - // `Lockfile::default()` ≡ `Lockfile::init_empty()`. - wr!(lockfile, Box::new(Lockfile::default())); - wr!(root_package_json_file, root_package_json_file); + wr!(p, root_package_json_file, root_package_json_file); // .progress - wr!(event_loop, AnyEventLoop::init()); + wr!(p, event_loop, AnyEventLoop::init()); wr!( + p, original_package_json_path, ZBox::from_vec_with_nul(original_package_json_path_buf) ); - wr!(workspace_package_json_cache, workspace_package_json_cache); - wr!(workspace_name_hash, workspace_name_hash); - wr!(subcommand, subcommand); wr!( - root_package_json_name_at_time_of_init, - root_package_json_name_at_time_of_init - ); - - // remaining defaults: - wr!(timestamp_for_manifest_cache_control, 0); - wr!(extracted_count, 0); - wr!(default_features, Features::default()); - wr!(summary, Default::default()); - wr!(progress, Progress::default()); - wr!(downloads_node, None); - wr!(scripts_node, None); - wr!(progress_name_buf, [0; 768]); - wr!(progress_name_buf_dynamic, Vec::new()); - wr!(track_installed_bin, TrackInstalledBin::None); - wr!(root_progress_node, core::ptr::null_mut()); - wr!(to_update, false); - wr!(update_requests, Box::default()); - wr!(root_package_id, RootPackageId::default()); - wr!(task_batch, thread_pool::Batch::default()); - wr!(task_queue, TaskDependencyQueue::default()); - wr!(manifests, PackageManifestMap::default()); - wr!(folders, Default::default()); - wr!(git_repositories, RepositoryMap::default()); - wr!(network_dedupe_map, Default::default()); - wr!(async_network_task_queue, AsyncNetworkTaskQueue::default()); - wr!(network_tarball_batch, thread_pool::Batch::default()); - wr!(network_resolve_batch, thread_pool::Batch::default()); - wr!(patch_apply_batch, thread_pool::Batch::default()); - wr!(patch_calc_hash_batch, thread_pool::Batch::default()); - wr!(patch_task_queue, PatchTaskQueue::default()); - wr!(pending_pre_calc_hashes, AtomicU32::new(0)); - wr!(pending_tasks, AtomicU32::new(0)); - wr!(total_tasks, 0); - wr!(lifecycle_script_time_log, LifecycleScriptTimeLog::default()); - wr!(pending_lifecycle_script_tasks, AtomicU32::new(0)); - wr!(finished_installing, AtomicBool::new(false)); - wr!(total_scripts, 0); - wr!(root_lifecycle_scripts, None); - wr!(node_gyp_tempdir_name, Box::default()); - wr!(preinstall_state, Vec::new()); - wr!(postinstall_optimizer, Default::default()); - wr!(global_link_dir, None); - wr!(global_dir, None); - wr!(global_link_dir_path, Box::default()); - wr!(on_wake, WakeHandler::default()); - wr!( - ci_mode, - LazyBool::new(PackageManager::compute_is_continuous_integration) + p, + workspace_package_json_cache, + workspace_package_json_cache ); + wr!(p, workspace_name_hash, workspace_name_hash); + wr!(p, subcommand, subcommand); wr!( - peer_dependencies, - LinearFifo::>::init() + p, + root_package_json_name_at_time_of_init, + root_package_json_name_at_time_of_init ); - wr!(known_npm_aliases, NpmAliasMap::default()); - wr!(trusted_deps_to_add_to_package_json, Vec::new()); - wr!(any_failed_to_install, false); - wr!(updating_packages, StringArrayHashMap::default()); - wr!(patched_dependencies_to_remove, ArrayHashMap::default()); - wr!(last_reported_slow_lifecycle_script_at, 0); - wr!(cached_tick_for_slow_lifecycle_script_logging, 0); } holder::INITIALIZED.store(true, core::sync::atomic::Ordering::Release); // The per-field placement above fully initialized the singleton; the @@ -2358,23 +2397,10 @@ pub(crate) fn init_with_runtime_once( // directly to the heap singleton. unsafe { let p = manager_ptr; - macro_rules! wr { - ($field:ident, $val:expr) => { - core::ptr::addr_of_mut!((*p).$field).write($val) - }; - } - // The two large pools: in-place init that only zeros the 256 B - // occupancy bitset and leaves `[MaybeUninit; N]` untouched. - PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_network_tasks - )); - PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_resolve_tasks - )); + write_shared_default_fields(p); - wr!(cache_directory_, None); - wr!(cache_directory_path, ZBox::from_bytes(b"")); wr!( + p, options, Options { max_concurrent_lifecycle_scripts: cli @@ -2383,114 +2409,46 @@ pub(crate) fn init_with_runtime_once( ..Default::default() } ); - wr!( - active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - context: crate::lifecycle_script_runner::StartedAtCtx, - } - ); - wr!(network_task_fifo, NetworkQueue::init()); - wr!(log, std::ptr::from_mut(log)); - wr!(root_dir, root_dir); - wr!(ast_arena, bun_alloc::Arena::new()); + wr!(p, log, std::ptr::from_mut(log)); + wr!(p, root_dir, root_dir); // reborrow `&mut *env` so the local stays usable for // the post-construction `BUN_MANIFEST_CACHE` / `options.load` // reads. `BackRef` stores a raw pointer — // ending the reborrow here does not alias the later uses. - wr!(env, Some(bun_ptr::BackRef::new_mut(&mut *env))); - wr!(cpu_count, cpu_count); + wr!(p, env, Some(bun_ptr::BackRef::new_mut(&mut *env))); + wr!(p, cpu_count, cpu_count); wr!( + p, thread_pool, ThreadPool::init(thread_pool::Config { max_threads: cpu_count, ..Default::default() }) ); - // `Lockfile` holds `HashMap`/`Vec`/`NonNull` (zero-bit pattern is - // UB), so allocate the real empty lockfile here directly instead of a zeroed placeholder. - wr!(lockfile, Box::new(Lockfile::default())); // `.root_package_json_file` is never read in the runtime // path. Use the explicit invalid-fd sentinel rather than `mem::zeroed()` — // on posix `Fd(0)` is stdin, not the invalid marker. wr!( + p, root_package_json_file, bun_sys::File::from_fd(Fd::invalid()) ); // erased *mut () set by tier-6; `js_current()` resolves the per-thread JS // event loop via `bun_io::__bun_get_vm_ctx` (link-time, definer in bun_runtime). - wr!(event_loop, AnyEventLoop::js_current()); + wr!(p, event_loop, AnyEventLoop::js_current()); wr!( + p, original_package_json_path, ZBox::from_vec_with_nul(original_package_json_path) ); - wr!(subcommand, Subcommand::Install); - - // remaining defaults: - wr!(resolve_tasks, ResolveTaskQueue::default()); - wr!(timestamp_for_manifest_cache_control, 0); - wr!(extracted_count, 0); - wr!(default_features, Features::default()); - wr!(summary, Default::default()); - wr!(progress, Progress::default()); - wr!(downloads_node, None); - wr!(scripts_node, None); - wr!(progress_name_buf, [0; 768]); - wr!(progress_name_buf_dynamic, Vec::new()); - wr!(track_installed_bin, TrackInstalledBin::None); - wr!(root_progress_node, core::ptr::null_mut()); - wr!(to_update, false); - wr!(update_requests, Box::default()); - wr!(root_package_json_name_at_time_of_init, Box::default()); - wr!(root_package_id, RootPackageId::default()); - wr!(task_batch, thread_pool::Batch::default()); - wr!(task_queue, TaskDependencyQueue::default()); - wr!(manifests, PackageManifestMap::default()); - wr!(folders, Default::default()); - wr!(git_repositories, RepositoryMap::default()); - wr!(network_dedupe_map, Default::default()); - wr!(async_network_task_queue, AsyncNetworkTaskQueue::default()); - wr!(network_tarball_batch, thread_pool::Batch::default()); - wr!(network_resolve_batch, thread_pool::Batch::default()); - wr!(patch_apply_batch, thread_pool::Batch::default()); - wr!(patch_calc_hash_batch, thread_pool::Batch::default()); - wr!(patch_task_fifo, PatchTaskFifo::init()); - wr!(patch_task_queue, PatchTaskQueue::default()); - wr!(pending_pre_calc_hashes, AtomicU32::new(0)); - wr!(pending_tasks, AtomicU32::new(0)); - wr!(total_tasks, 0); - wr!(lifecycle_script_time_log, LifecycleScriptTimeLog::default()); - wr!(pending_lifecycle_script_tasks, AtomicU32::new(0)); - wr!(finished_installing, AtomicBool::new(false)); - wr!(total_scripts, 0); - wr!(root_lifecycle_scripts, None); - wr!(node_gyp_tempdir_name, Box::default()); - wr!(preinstall_state, Vec::new()); - wr!(postinstall_optimizer, Default::default()); - wr!(global_link_dir, None); - wr!(global_dir, None); - wr!(global_link_dir_path, Box::default()); - wr!(on_wake, WakeHandler::default()); - wr!( - ci_mode, - LazyBool::new(PackageManager::compute_is_continuous_integration) - ); - wr!( - peer_dependencies, - LinearFifo::>::init() - ); - wr!(known_npm_aliases, NpmAliasMap::default()); - wr!(trusted_deps_to_add_to_package_json, Vec::new()); - wr!(any_failed_to_install, false); - wr!(workspace_name_hash, None); + wr!(p, subcommand, Subcommand::Install); + wr!(p, root_package_json_name_at_time_of_init, Box::default()); + wr!(p, workspace_name_hash, None); wr!( + p, workspace_package_json_cache, WorkspacePackageJSONCache::default() ); - wr!(updating_packages, StringArrayHashMap::default()); - wr!(patched_dependencies_to_remove, ArrayHashMap::default()); - wr!(last_reported_slow_lifecycle_script_at, 0); - wr!(cached_tick_for_slow_lifecycle_script_logging, 0); } holder::INITIALIZED.store(true, core::sync::atomic::Ordering::Release); // SAFETY: per-field placement above fully initialized the PackageManager; diff --git a/src/install/PackageManager/PackageJSONEditor.rs b/src/install/PackageManager/PackageJSONEditor.rs index 9ed87945fee7..29a664d7ed7c 100644 --- a/src/install/PackageManager/PackageJSONEditor.rs +++ b/src/install/PackageManager/PackageJSONEditor.rs @@ -39,6 +39,46 @@ fn arena_dup<'a>(arena: &'a bun_alloc::Arena, bytes: &[u8]) -> &'a [u8] { arena.alloc_slice_copy(bytes) } +/// Builds the replacement version string for an updated npm dependency, +/// preserving the original pin style (`1.2.3` / `~1.2.3` / `^1.2.3`) and, for +/// aliases, the `npm:@scope/pkg@` prefix from `dep_literal`. +fn replacement_version_literal( + version_fmt: impl std::fmt::Display, + original_version_literal: &[u8], + is_alias: bool, + dep_literal: &[u8], + exact_versions: bool, +) -> Vec { + let mut v = Vec::new(); + if is_alias { + // negative because the real package might have a scope + // e.g. "dep": "npm:@foo/bar@1.2.3" + if let Some(at_index) = strings::last_index_of_char(dep_literal, b'@') { + write!(&mut v, "{}@", bstr::BStr::new(&dep_literal[0..at_index])) + .expect("infallible: in-memory write"); + } + } + let pin_prefix = if exact_versions { + "" + } else { + let version_literal = if is_alias { + match strings::last_index_of_char(original_version_literal, b'@') { + Some(at_index) => &original_version_literal[at_index + 1..], + None => original_version_literal, + } + } else { + original_version_literal + }; + match semver::Version::which_version_is_pinned(version_literal) { + semver::PinnedVersion::Patch => "", + semver::PinnedVersion::Minor => "~", + semver::PinnedVersion::Major => "^", + } + }; + write!(&mut v, "{}{}", pin_prefix, version_fmt).expect("infallible: in-memory write"); + v +} + /// Shallow-copy a `G::Property` for the JSON-editing path. Only `key`/`value` /// (both `Option`, `Copy`) are populated by the JSON parser; the rest /// (`ts_decorators`, `class_static_block`, …) are always default for parsed @@ -435,82 +475,14 @@ pub(crate) fn edit_update_no_args( } } - let new_version: Vec = 'new_version: { - // `resolution.tag == Npm` checked above. - let version_fmt = resolution.npm().version.fmt(string_buf); - if options.exact_versions { - let mut v = Vec::new(); - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write"); - break 'new_version v; - } - - let version_literal: &[u8] = 'version_literal: { - if !is_alias { - break 'version_literal &entry - .value - .original_version_literal; - } - if let Some(at_index) = strings::last_index_of_char( - &entry.value.original_version_literal, - b'@', - ) { - break 'version_literal &entry - .value - .original_version_literal[at_index + 1..]; - } - &entry.value.original_version_literal - }; - - let pinned_version = - semver::Version::which_version_is_pinned( - version_literal, - ); - let mut v = Vec::new(); - match pinned_version { - semver::PinnedVersion::Patch => { - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Minor => { - write!(&mut v, "~{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Major => { - write!(&mut v, "^{}", version_fmt) - .expect("infallible: in-memory write") - } - } - v - }; - - if is_alias { - let dep_literal = - workspace_dep.version.literal.slice(string_buf); - - // negative because the real package might have a scope - // e.g. "dep": "npm:@foo/bar@1.2.3" - if let Some(at_index) = - strings::last_index_of_char(dep_literal, b'@') - { - let mut v = Vec::new(); - write!( - &mut v, - "{}@{}", - bstr::BStr::new(&dep_literal[0..at_index]), - bstr::BStr::new(&new_version) - ) - .unwrap(); - dep.value = Some(Expr::allocate( - arena, - E::EString::init(arena_str(arena, &v)), - bun_ast::Loc::EMPTY, - )); - break 'updated; - } - - // fallthrough and replace entire version. - } + // `resolution.tag == Npm` checked above. + let new_version = replacement_version_literal( + resolution.npm().version.fmt(string_buf), + &entry.value.original_version_literal, + is_alias, + workspace_dep.version.literal.slice(string_buf), + options.exact_versions, + ); dep.value = Some(Expr::allocate( arena, @@ -1132,73 +1104,16 @@ pub(crate) fn edit( if let Some(entry) = manager.updating_packages.fetch_swap_remove(request.name) { - let new_version: Vec = 'new_version: { - let version_fmt = resolutions[request.package_id as usize] + let new_version = replacement_version_literal( + resolutions[request.package_id as usize] .npm() .version - .fmt(manager.lockfile.buffers.string_bytes.as_slice()); - if options.exact_versions { - let mut v = Vec::new(); - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write"); - break 'new_version v; - } - - let version_literal: &[u8] = 'version_literal: { - if !entry.value.is_alias { - break 'version_literal &entry - .value - .original_version_literal; - } - if let Some(at_index) = strings::last_index_of_char( - &entry.value.original_version_literal, - b'@', - ) { - break 'version_literal &entry - .value - .original_version_literal[at_index + 1..]; - } - - &entry.value.original_version_literal - }; - - let pinned_version = - semver::Version::which_version_is_pinned(version_literal); - let mut v = Vec::new(); - match pinned_version { - semver::PinnedVersion::Patch => { - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Minor => { - write!(&mut v, "~{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Major => { - write!(&mut v, "^{}", version_fmt) - .expect("infallible: in-memory write") - } - } - v - }; - - if entry.value.is_alias { - let dep_literal = &entry.value.original_version_literal; - - if let Some(at_index) = - strings::last_index_of_char(dep_literal, b'@') - { - let mut v = Vec::new(); - write!( - &mut v, - "{}@{}", - bstr::BStr::new(&dep_literal[0..at_index]), - bstr::BStr::new(&new_version) - ) - .unwrap(); - break 'npm arena_str(arena, &v); - } - } + .fmt(manager.lockfile.buffers.string_bytes.as_slice()), + &entry.value.original_version_literal, + entry.value.is_alias, + &entry.value.original_version_literal, + options.exact_versions, + ); break 'npm arena_str(arena, &new_version); } diff --git a/src/install/PackageManager/PackageManagerEnqueue.rs b/src/install/PackageManager/PackageManagerEnqueue.rs index 8316c27b7c1a..0f9f5ff5fe49 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.rs +++ b/src/install/PackageManager/PackageManagerEnqueue.rs @@ -645,6 +645,36 @@ pub unsafe fn enqueue_patch_task_pre(this: &mut PackageManager, task: *mut Patch let _ = this.pending_pre_calc_hashes.fetch_add(1, Ordering::Relaxed); } +/// Returns the task-callback list for `task_id`, creating and initializing it +/// if this is the first callback registered for the task. +fn task_callback_list<'a>( + this: &'a mut PackageManager, + task_id: Task::Id, +) -> crate::Result<&'a mut TaskCallbackList> { + let entry = this.task_queue.get_or_put_context(task_id, ())?; + if !entry.found_existing { + *entry.value_ptr = TaskCallbackList::default(); + } + Ok(entry.value_ptr) +} + +/// Registers dependency `id` as a callback for `task_id`, tagging it as a +/// root or transitive dependency. +fn push_dependency_task_callback( + this: &mut PackageManager, + task_id: Task::Id, + id: DependencyID, + is_root: bool, +) -> crate::Result<()> { + let ctx = if is_root { + TaskCallbackContext::RootDependency(id) + } else { + TaskCallbackContext::Dependency(id) + }; + task_callback_list(this, task_id)?.push(ctx); + Ok(()) +} + /// Q: "What do we do with a dependency in a package.json?" /// A: "We enqueue it!" pub fn enqueue_dependency_with_main_and_success_fn( @@ -1179,18 +1209,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( return Ok(()); } - let manifest_entry_parse = - this.task_queue.get_or_put_context(task_id, ())?; - if !manifest_entry_parse.found_existing { - *manifest_entry_parse.value_ptr = TaskCallbackList::default(); - } - - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - manifest_entry_parse.value_ptr.push(ctx); + push_dependency_task_callback(this, task_id, id, is_root)?; } return Ok(()); } @@ -1215,11 +1234,6 @@ pub fn enqueue_dependency_with_main_and_success_fn( let alias = this.lockfile.str_detached(&dependency.name); let url = this.lockfile.str_detached(&dep.repo); let clone_id = Task::Id::for_git_clone(url); - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; if cfg!(debug_assertions) { bun_output::scoped_log!( @@ -1246,15 +1260,11 @@ pub fn enqueue_dependency_with_main_and_success_fn( let needs_ctx = this.lockfile.buffers.resolutions[id as usize] == invalid_package_id; - let entry = this - .task_queue - .get_or_put_context(checkout_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } if needs_ctx { - entry.value_ptr.push(ctx); + push_dependency_task_callback(this, checkout_id, id, is_root) + .expect("unreachable"); + } else { + task_callback_list(this, checkout_id).expect("unreachable"); } if dependency.behavior.is_peer() { @@ -1280,14 +1290,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); this.task_batch.push(ThreadPool::Batch::from(task)); } else { - let entry = this - .task_queue - .get_or_put_context(clone_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); + push_dependency_task_callback(this, clone_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -1332,24 +1335,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); } - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - // reshaped for borrowck — `entry` mutably borrows - // `this.task_queue`; scope it tightly so the calls below can - // reborrow `*this`. - { - let entry = this - .task_queue - .get_or_put_context(task_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); - } + push_dependency_task_callback(this, task_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -1532,22 +1518,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); } - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - // reshaped for borrowck — scope `entry` tightly. - { - let entry = this - .task_queue - .get_or_put_context(task_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); - } + push_dependency_task_callback(this, task_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -2188,6 +2159,41 @@ fn get_or_put_resolved_package_with_find_result( // `guard` drops here → success_fn(this, dependency_id, package.meta.id) } +/// Scans the root package's dependencies for a workspace entry matching +/// `name_hash`; on a match, records the resolution via `success_fn` and +/// returns the already-resolved workspace package. +fn resolve_root_workspace_package( + this: &mut PackageManager, + name_hash: PackageNameHash, + dependency_id: DependencyID, + success_fn: SuccessFn, +) -> Option { + let root_package = this.lockfile.root_package()?; + let root_dependencies = root_package + .dependencies + .get(this.lockfile.buffers.dependencies.as_slice()); + let root_resolutions = root_package + .resolutions + .get(this.lockfile.buffers.resolutions.as_slice()); + + debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); + for (root_dep, &workspace_package_id) in root_dependencies.iter().zip(root_resolutions) { + if workspace_package_id != invalid_package_id + && root_dep.version.tag == dependency::version::Tag::Workspace + && root_dep.name_hash == name_hash + { + // make sure verifyResolutions sees this resolution as a valid package id + success_fn(this, dependency_id, workspace_package_id); + return Some(ResolvedPackageResult { + package: *this.lockfile.packages.get(workspace_package_id as usize), + is_first_time: false, + task: None, + }); + } + } + None +} + fn get_or_put_resolved_package( this: &mut PackageManager, name_hash: PackageNameHash, @@ -2314,54 +2320,27 @@ fn get_or_put_resolved_package( match version.tag { dependency::version::Tag::Npm | dependency::version::Tag::DistTag => { - 'resolve_from_workspace: { - if version.tag == dependency::version::Tag::Npm { - let workspace_path = if this.lockfile.workspace_paths.count() > 0 { - this.lockfile.workspace_paths.get(&name_hash) - } else { - None - }; - let workspace_version = this.lockfile.workspace_versions.get(&name_hash); - let buf = this.lockfile.buffers.string_bytes.as_slice(); - let npm_group = &version.npm().version; - if this.options.link_workspace_packages - && ((workspace_version.is_some() - && npm_group.satisfies(*workspace_version.unwrap(), buf, buf)) - // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 - // if the workspace doesn't have a version, it can still be used if - // dependency version is wildcard - || (workspace_path.is_some() && npm_group.is_star())) + if version.tag == dependency::version::Tag::Npm { + let workspace_path = if this.lockfile.workspace_paths.count() > 0 { + this.lockfile.workspace_paths.get(&name_hash) + } else { + None + }; + let workspace_version = this.lockfile.workspace_versions.get(&name_hash); + let buf = this.lockfile.buffers.string_bytes.as_slice(); + let npm_group = &version.npm().version; + if this.options.link_workspace_packages + && ((workspace_version.is_some() + && npm_group.satisfies(*workspace_version.unwrap(), buf, buf)) + // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 + // if the workspace doesn't have a version, it can still be used if + // dependency version is wildcard + || (workspace_path.is_some() && npm_group.is_star())) + { + if let Some(resolved) = + resolve_root_workspace_package(this, name_hash, dependency_id, success_fn) { - let Some(root_package) = this.lockfile.root_package() else { - break 'resolve_from_workspace; - }; - let root_dependencies = root_package - .dependencies - .get(this.lockfile.buffers.dependencies.as_slice()); - let root_resolutions = root_package - .resolutions - .get(this.lockfile.buffers.resolutions.as_slice()); - - debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); - for (root_dep, &workspace_package_id) in - root_dependencies.iter().zip(root_resolutions) - { - if workspace_package_id != invalid_package_id - && root_dep.version.tag == dependency::version::Tag::Workspace - && root_dep.name_hash == name_hash - { - // make sure verifyResolutions sees this resolution as a valid package id - success_fn(this, dependency_id, workspace_package_id); - return Ok(Some(ResolvedPackageResult { - package: *this - .lockfile - .packages - .get(workspace_package_id as usize), - is_first_time: false, - task: None, - })); - } - } + return Ok(Some(resolved)); } } } @@ -2475,46 +2454,21 @@ fn get_or_put_resolved_package( let find_result = match find_result_opt { Some(r) => r, None => { - 'resolve_workspace_from_dist_tag: { - // choose a workspace for a dist_tag only if a version was not found - if version.tag == dependency::version::Tag::DistTag { - let workspace_path = if this.lockfile.workspace_paths.count() > 0 { - this.lockfile.workspace_paths.get(&name_hash) - } else { - None - }; - if workspace_path.is_some() { - let Some(root_package) = this.lockfile.root_package() else { - break 'resolve_workspace_from_dist_tag; - }; - let root_dependencies = root_package - .dependencies - .get(this.lockfile.buffers.dependencies.as_slice()); - let root_resolutions = root_package - .resolutions - .get(this.lockfile.buffers.resolutions.as_slice()); - - debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); - for (root_dep, &workspace_package_id) in - root_dependencies.iter().zip(root_resolutions) - { - if workspace_package_id != invalid_package_id - && root_dep.version.tag - == dependency::version::Tag::Workspace - && root_dep.name_hash == name_hash - { - // make sure verifyResolutions sees this resolution as a valid package id - success_fn(this, dependency_id, workspace_package_id); - return Ok(Some(ResolvedPackageResult { - package: *this - .lockfile - .packages - .get(workspace_package_id as usize), - is_first_time: false, - task: None, - })); - } - } + // choose a workspace for a dist_tag only if a version was not found + if version.tag == dependency::version::Tag::DistTag { + let workspace_path = if this.lockfile.workspace_paths.count() > 0 { + this.lockfile.workspace_paths.get(&name_hash) + } else { + None + }; + if workspace_path.is_some() { + if let Some(resolved) = resolve_root_workspace_package( + this, + name_hash, + dependency_id, + success_fn, + ) { + return Ok(Some(resolved)); } } } diff --git a/src/install/PackageManager/WorkspacePackageJSONCache.rs b/src/install/PackageManager/WorkspacePackageJSONCache.rs index 087e52430d57..248d74d461e1 100644 --- a/src/install/PackageManager/WorkspacePackageJSONCache.rs +++ b/src/install/PackageManager/WorkspacePackageJSONCache.rs @@ -2,6 +2,7 @@ use crate::Error; use bun_collections::StringHashMap; +use bun_core::{Global, Output}; // `Expr` here is the JSON parser's AST node (`bun_ast::Expr`, re- // exported via `crate::bun_json`). It is intentionally NOT `bun_ast::Expr` // — that lives in a higher-tier crate and is a distinct type. Consumers of @@ -206,6 +207,30 @@ impl WorkspacePackageJSONCache { GetResult::Entry(entry.value_ptr) } + /// `get_with_path`, except read/parse failures are fatal: pending log + /// messages and the error are printed to stderr, then the process exits. + pub fn get_with_path_or_exit( + &mut self, + log: &mut Log, + abs_package_json_path: &[u8], + opts: GetJSONOptions, + ) -> &mut MapEntry { + let (err, fmt) = match self.get_with_path(log, abs_package_json_path, opts) { + GetResult::Entry(entry) => return entry, + GetResult::ReadErr(err) => (err, "failed to read '{}'"), + GetResult::ParseErr(err) => (err, "failed to parse '{}'"), + }; + if log.errors > 0 { + let _ = log.print(std::ptr::from_mut(Output::error_writer())); + } + Output::err( + err, + fmt, + format_args!("{}", bstr::BStr::new(abs_package_json_path)), + ); + Global::crash(); + } + /// source path is used as the key, needs to be absolute pub fn get_with_source( &mut self, diff --git a/src/install/PackageManager/install_with_manager.rs b/src/install/PackageManager/install_with_manager.rs index 058a17fd9517..b26eb90970f7 100644 --- a/src/install/PackageManager/install_with_manager.rs +++ b/src/install/PackageManager/install_with_manager.rs @@ -8,7 +8,6 @@ use bun_core::{ZStr, strings}; use bun_glob as glob; use bun_semver::String as SemverString; -use crate::GetJsonResult as WorkspacePackageJsonCacheResult; use crate::Subcommand; use crate::dependency::{DependencyExt as _, Tag as DependencyVersionTag}; use crate::lockfile::{self, Lockfile}; @@ -140,72 +139,12 @@ pub fn install_with_manager( let mut lockfile = Lockfile::default(); let mut maybe_root = lockfile::Package::default(); - // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). - let root_package_json_entry = match manager - .workspace_package_json_cache - .get_with_path( - manager.log_mut(), - root_package_json_path.as_bytes(), - Default::default(), - ) { - WorkspacePackageJsonCacheResult::Entry(entry) => entry, - WorkspacePackageJsonCacheResult::ReadErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to read '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - WorkspacePackageJsonCacheResult::ParseErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to parse '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - }; - - // `Source` is not `Copy`, so - // clone it (cheap — `Source` is a few `Box<[u8]>` handles) so the - // `&mut *mgr` reborrow below doesn't conflict with the cache borrow. - let source_copy = root_package_json_entry.source.clone(); - - let mut resolver: () = (); - // `parse` needs `manager`, `manager.log` and a fresh - // stack `lockfile` simultaneously. Route through raw ptrs so - // borrowck doesn't see overlapping `&mut PackageManager` / - // `&mut Lockfile`. - { - // `log_mut()` reads the BACKREF `self.log: *mut Log` and - // returns the disjoint CLI `Log` allocation (lifetime - // decoupled from `&self`), so call it safely through - // `manager` *before* establishing the raw-ptr split — no - // borrow on `*manager` survives into the `&mut *mgr` below. - let log = manager.log_mut(); - let mgr: *mut PackageManager = manager; - maybe_root.parse( - &mut lockfile, - // SAFETY: `mgr` is the sole provenance root for `*manager`; `log` is a - // disjoint backref and `lockfile` is a stack local, so this `&mut` is unique. - unsafe { &mut *mgr }, - log, - &source_copy, - &mut resolver, - Features::main(), - )?; - } + parse_root_package( + manager, + root_package_json_path, + &mut lockfile, + &mut maybe_root, + )?; let mut mapping = vec![invalid_package_id; maybe_root.dependencies.len as usize] .into_boxed_slice(); // @memset already done via vec! init @@ -1484,6 +1423,49 @@ fn record_updating_package_versions(manager: &mut PackageManager) { } } +/// Load the root package.json from the workspace cache (exiting on read/parse +/// errors) and parse it as the root `Package` into `lockfile`. `lockfile` must +/// be storage disjoint from `*manager` (a stack local, or the heap allocation +/// behind `manager.lockfile`'s `Box`). +fn parse_root_package( + manager: &mut PackageManager, + root_package_json_path: &ZStr, + lockfile: &mut Lockfile, + root: &mut lockfile::Package, +) -> crate::Result<()> { + // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). + let root_package_json_entry = manager.workspace_package_json_cache.get_with_path_or_exit( + manager.log_mut(), + root_package_json_path.as_bytes(), + Default::default(), + ); + + // `Source` is not `Copy`, so clone it (cheap — `Source` is a few + // `Box<[u8]>` handles) so the `&mut *mgr` reborrow below doesn't conflict + // with the cache borrow. + let source_copy = root_package_json_entry.source.clone(); + + let mut resolver: () = (); + // `log_mut()` reads the BACKREF `self.log: *mut Log` and returns the + // disjoint CLI `Log` allocation (lifetime decoupled from `&self`), so call + // it safely through `manager` *before* establishing the raw-ptr split — no + // borrow on `*manager` survives into the `&mut *mgr` below. + let log = manager.log_mut(); + let mgr: *mut PackageManager = manager; + root.parse( + lockfile, + // SAFETY: `mgr` is the sole provenance root for `*manager`; `log` is a + // disjoint backref and `lockfile` is caller-guaranteed disjoint + // storage, so this `&mut` is unique. + unsafe { &mut *mgr }, + log, + &source_copy, + &mut resolver, + Features::main(), + )?; + Ok(()) +} + #[cold] #[inline(never)] fn create_new_lockfile_and_enqueue( @@ -1521,63 +1503,21 @@ fn create_new_lockfile_and_enqueue( Global::crash(); } - // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). - let root_package_json_entry = match manager.workspace_package_json_cache.get_with_path( - manager.log_mut(), - root_package_json_path.as_bytes(), - Default::default(), - ) { - WorkspacePackageJsonCacheResult::Entry(entry) => entry, - WorkspacePackageJsonCacheResult::ReadErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to read '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - WorkspacePackageJsonCacheResult::ParseErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to parse '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - }; - - let source_copy = root_package_json_entry.source.clone(); - - let mut resolver: () = (); { - // `log_mut()` reads the BACKREF `self.log` and returns the disjoint - // CLI `Log` allocation (lifetime decoupled from `&self`); call it - // safely *before* the raw-ptr split. - let log = manager.log_mut(); let mgr: *mut PackageManager = manager; - // SAFETY: `mgr` is the sole provenance root; `parse` reborrows the - // disjoint `lockfile` field through it. No other live `&mut` to - // `*mgr` exists across the call. - root.parse( - // SAFETY: disjoint field projection through the sole provenance root `mgr`. - unsafe { &mut (*mgr).lockfile }, - // SAFETY: `parse` touches only `PackageManager` fields disjoint from - // `lockfile` through this borrow; `mgr` is the sole provenance root. + // SAFETY: `mgr` is the sole provenance root; `manager.lockfile` is an + // owned `Box`, so this projects its heap allocation, which is disjoint + // storage from the `PackageManager` struct itself. + let lockfile: *mut Lockfile = unsafe { &raw mut *(*mgr).lockfile }; + parse_root_package( + // SAFETY: `parse_root_package` touches only `PackageManager` fields + // disjoint from `lockfile` through this borrow; `mgr` is the sole + // provenance root. unsafe { &mut *mgr }, - log, - &source_copy, - &mut resolver, - Features::main(), + root_package_json_path, + // SAFETY: points to the Box-owned heap allocation, disjoint from `*mgr`. + unsafe { &mut *lockfile }, + &mut root, )?; } diff --git a/src/install/PackageManager/patchPackage.rs b/src/install/PackageManager/patchPackage.rs index 04dfd2bc0100..8cecded7436b 100644 --- a/src/install/PackageManager/patchPackage.rs +++ b/src/install/PackageManager/patchPackage.rs @@ -147,101 +147,7 @@ pub fn do_patch_commit( let (cache_dir, cache_dir_subpath, changes_dir, pkg): (Fd, &ZStr, Vec, Package) = match arg_kind { PatchArgKind::Path => 'result: { - let package_json_path = - resolve_path::join_z::(&[argument, b"package.json"]); - let package_json_source: bun_ast::Source = - match bun_ast::to_source(package_json_path, Default::default()) { - Ok(s) => s, - Err(e) => { - Output::err( - e, - "failed to read {f}", - (bun_fmt::quote(package_json_path.as_bytes()),), - ); - Global::crash(); - } - }; - - initialize_store(); - let log = manager.log_mut(); - let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { - Ok(p) => p, - Err(err) => { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); - bun_core::pretty_errorln!( - "{} parsing package.json in \"{}\"", - err.name(), - bstr::BStr::new(package_json_source.path.pretty_dir()), - ); - Global::crash(); - } - }; - let json = parsed.root; - - let version: &[u8] = 'version: { - if let Some(v) = json.get(b"version") { - if let bun_ast::ExprData::EString(s) = &v.data { - let s = s.data.slice(); - break 'version s; - } - } - bun_core::pretty_error!( - "error: invalid package.json, missing or invalid property \"version\": {}\n", - bstr::BStr::new(package_json_source.path.text()), - ); - Global::crash(); - }; - - let mut resolver: () = (); - let mut package = Package::default(); - let log = manager.log_mut(); - package.parse_with_json::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - json, - &mut resolver, - Features::FOLDER, - )?; - - let actual_package = match lockfile.package_index.get(&package.name_hash) { - None => { - bun_core::pretty_error!( - "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", - ); - Global::crash(); - } - Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), - Some(PackageIndexEntry::Ids(ids)) => 'brk: { - for &id in ids.as_slice() { - let pkg = *lockfile.packages.get(id as usize); - let total = resolution_buf.len(); - let mut cursor: &mut [u8] = &mut resolution_buf[..]; - write!( - &mut cursor, - "{}", - pkg.resolution - .fmt(lockfile.buffers.string_bytes.as_slice(), PathSep::Posix) - ) - .expect("unreachable"); - let written = total - cursor.len(); - let resolution_label = &resolution_buf[..written]; - if resolution_label == version { - break 'brk pkg; - } - } - bun_core::pretty_error!( - "error: could not find package with name: {}\n", - bstr::BStr::new( - package.name.slice(lockfile.buffers.string_bytes.as_slice()) - ), - ); - Global::crash(); - } - }; - - let name = lockfile.str(&package.name).to_vec(); + let (name, actual_package) = load_path_package(manager, &mut lockfile, argument)?; let resolution_clone = actual_package.resolution; let cache_result = compute_cache_dir_and_subpath( manager, @@ -724,7 +630,6 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> { let arg_kind: PatchArgKind = PatchArgKind::from_arg(argument); let mut folder_path_buf = PathBuffer::uninit(); - let mut resolution_buf = [0u8; 1024]; #[cfg(windows)] let mut win_normalizer = PathBuffer::uninit(); @@ -758,109 +663,18 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> { let (cache_dir, cache_dir_subpath, module_folder, pkg_name): (Fd, &[u8], Vec, Vec) = match arg_kind { PatchArgKind::Path => 'brk: { - let package_json_path = - resolve_path::join_z::(&[argument, b"package.json"]); - let package_json_source: bun_ast::Source = - match bun_ast::to_source(package_json_path, Default::default()) { - Ok(s) => s, - Err(e) => { - Output::err( - e, - "failed to read {f}", - (bun_fmt::quote(package_json_path.as_bytes()),), - ); - Global::crash(); - } - }; - - initialize_store(); - let log = manager.log_mut(); - let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { - Ok(p) => p, - Err(err) => { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); - bun_core::pretty_errorln!( - "{} parsing package.json in \"{}\"", - err.name(), - bstr::BStr::new(package_json_source.path.pretty_dir()), - ); - Global::crash(); - } - }; - let json = parsed.root; - - let version: &[u8] = 'version: { - if let Some(v) = json.get(b"version") { - if let bun_ast::ExprData::EString(s) = &v.data { - let s = s.data.slice(); - break 'version s; - } - } - bun_core::pretty_error!( - "error: invalid package.json, missing or invalid property \"version\": {}\n", - bstr::BStr::new(package_json_source.path.text()), - ); - Global::crash(); - }; - - let mut resolver: () = (); - let mut package = Package::default(); - let log = manager.log_mut(); - // borrowck — `parse_with_json` needs `&mut Lockfile` and + // borrowck — `load_path_package` needs `&mut Lockfile` and // `&mut PackageManager` simultaneously, but the lockfile here is // `manager.lockfile`. Temporarily move the Box out so the two - // borrows are disjoint; `parse_with_json` never reads `pm.lockfile` - // (it takes the lockfile as its own parameter). Restore before - // propagating any error so `manager` is never left half-torn. + // borrows are disjoint. Restore before propagating any error so + // `manager` is never left half-torn. let mut lockfile: Box = core::mem::take(&mut manager.lockfile); - let parse_result = package.parse_with_json::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - json, - &mut resolver, - Features::FOLDER, - ); + let result = load_path_package(manager, &mut lockfile, argument); manager.lockfile = lockfile; - parse_result?; + let (name, actual_package) = result?; let lockfile: &Lockfile = &manager.lockfile; let strbuf = lockfile.buffers.string_bytes.as_slice(); - let actual_package = match lockfile.package_index.get(&package.name_hash) { - None => { - bun_core::pretty_error!( - "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", - ); - Global::crash(); - } - Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), - Some(PackageIndexEntry::Ids(ids)) => 'id: { - for &id in ids.as_slice() { - let pkg = *lockfile.packages.get(id as usize); - let total = resolution_buf.len(); - let mut cursor: &mut [u8] = &mut resolution_buf[..]; - write!( - &mut cursor, - "{}", - pkg.resolution.fmt(strbuf, PathSep::Posix) - ) - .expect("unreachable"); - let written = total - cursor.len(); - let resolution_label = &resolution_buf[..written]; - if resolution_label == version { - break 'id pkg; - } - } - bun_core::pretty_error!( - "error: could not find package with name: {}\n", - bstr::BStr::new(package.name.slice(strbuf)), - ); - Global::crash(); - } - }; - - let name = lockfile.str(&package.name).to_vec(); let existing_patchfile_hash: Option = 'existing_patchfile_hash: { let mut name_and_version = Vec::new(); write!( @@ -1240,6 +1054,112 @@ fn node_modules_folder_for_dependency_id( } } +/// Shared `PatchArgKind::Path` handling for `bun patch` and `bun patch --commit`: +/// parse `/package.json`, register it against the lockfile, and find +/// the matching package in the lockfile's package index. Returns the package +/// name and the lockfile's entry for the package. +/// +/// `parse_with_json` never reads `manager.lockfile` (it takes the lockfile as +/// its own parameter), so callers whose lockfile lives in `manager.lockfile` +/// may temporarily move the Box out to make the two `&mut` borrows disjoint. +fn load_path_package( + manager: &mut PackageManager, + lockfile: &mut Lockfile, + argument: &[u8], +) -> crate::Result<(Vec, Package)> { + let package_json_path = resolve_path::join_z::(&[argument, b"package.json"]); + let package_json_source: bun_ast::Source = + match bun_ast::to_source(package_json_path, Default::default()) { + Ok(s) => s, + Err(e) => { + Output::err( + e, + "failed to read {f}", + (bun_fmt::quote(package_json_path.as_bytes()),), + ); + Global::crash(); + } + }; + + initialize_store(); + let log = manager.log_mut(); + let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { + Ok(p) => p, + Err(err) => { + let _ = log.print(std::ptr::from_mut(Output::error_writer())); + bun_core::pretty_errorln!( + "{} parsing package.json in \"{}\"", + err.name(), + bstr::BStr::new(package_json_source.path.pretty_dir()), + ); + Global::crash(); + } + }; + let json = parsed.root; + + let version: &[u8] = 'version: { + if let Some(v) = json.get(b"version") { + if let bun_ast::ExprData::EString(s) = &v.data { + break 'version s.data.slice(); + } + } + bun_core::pretty_error!( + "error: invalid package.json, missing or invalid property \"version\": {}\n", + bstr::BStr::new(package_json_source.path.text()), + ); + Global::crash(); + }; + + let mut resolver: () = (); + let mut package = Package::default(); + package.parse_with_json::<()>( + lockfile, + manager, + log, + &package_json_source, + json, + &mut resolver, + Features::FOLDER, + )?; + + let strbuf = lockfile.buffers.string_bytes.as_slice(); + let actual_package = match lockfile.package_index.get(&package.name_hash) { + None => { + bun_core::pretty_error!( + "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", + ); + Global::crash(); + } + Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), + Some(PackageIndexEntry::Ids(ids)) => 'brk: { + let mut resolution_buf = [0u8; 1024]; + for &id in ids.as_slice() { + let pkg = *lockfile.packages.get(id as usize); + let total = resolution_buf.len(); + let mut cursor: &mut [u8] = &mut resolution_buf[..]; + write!( + &mut cursor, + "{}", + pkg.resolution.fmt(strbuf, PathSep::Posix) + ) + .expect("unreachable"); + let written = total - cursor.len(); + if &resolution_buf[..written] == version { + break 'brk pkg; + } + } + bun_core::pretty_error!( + "error: could not find package with name: {}\n", + bstr::BStr::new(package.name.slice(strbuf)), + ); + Global::crash(); + } + }; + + let name = lockfile.str(&package.name).to_vec(); + Ok((name, actual_package)) +} + type IdPair = (DependencyID, PackageID); fn pkg_info_for_name_and_version( diff --git a/src/install/PackageManager/runTasks.rs b/src/install/PackageManager/runTasks.rs index 90c984a207b9..a43d6d4bdca0 100644 --- a/src/install/PackageManager/runTasks.rs +++ b/src/install/PackageManager/runTasks.rs @@ -448,16 +448,7 @@ pub fn run_tasks( ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, name) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, name); } continue; @@ -500,16 +491,7 @@ pub fn run_tasks( response.status_code, ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, name) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, name); continue; } @@ -706,45 +688,11 @@ pub fn run_tasks( .map(crate::Error::from) .unwrap_or(crate::Error::TarballFailedToDownload); - // The download will not be retried for this task_id, so - // drop the dedupe state before dispatching the error. - // Otherwise a later `enqueuePackageForDownload` for the - // same package sees `found_existing`, never schedules a - // network task, and waits forever for a callback that - // will not arrive. `Store.Installer.onPackageDownloadError` - // drains `task_queue` itself but does not touch - // `network_dedupe_map`, so this must run on the callback - // path too. Capture `is_required` first — - // `isNetworkTaskRequired` reads the map and returns `true` - // when the entry is gone, which would upgrade optional-dep - // warnings to errors on the void-callback fallback below. - let is_required = manager.is_network_task_required(task.task_id); - let _ = manager.network_dedupe_map.remove(&task.task_id); - - if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { - if C::IS_STORE_INSTALLER { - C::on_package_download_error_store( - extract_ctx, - task.task_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } else { - let package_id = manager.lockfile.buffers.resolutions - [extract.dependency_id as usize]; - C::on_package_download_error_pkg( - extract_ctx, - package_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } + let Some(is_required) = + dispatch_tarball_error::(manager, extract_ctx, task, extract, err) + else { continue; - } + }; if is_required { bun_ast::add_error_pretty!( @@ -771,16 +719,7 @@ pub fn run_tasks( .fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto,), ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, extract.name.slice()) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, extract.name.slice()); if let Some(removed) = manager.task_queue.remove(&task.task_id) { drop(removed); @@ -792,50 +731,21 @@ pub fn run_tasks( let response = &metadata.response; if response.status_code > 399 { - // Non-retryable HTTP error: drop dedupe state so a later - // enqueue for this task_id schedules a fresh network task - // instead of waiting on this failed one. Runs before the - // callback branch so `Store.Installer` (which `continue`s - // from the callback) is covered too. Capture - // `is_required` first — `isNetworkTaskRequired` reads the - // map and returns `true` when the entry is gone. - let is_required = manager.is_network_task_required(task.task_id); - let _ = manager.network_dedupe_map.remove(&task.task_id); - - if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { - let err = match response.status_code { - 400 => crate::Error::TarballHTTP400, - 401 => crate::Error::TarballHTTP401, - 402 => crate::Error::TarballHTTP402, - 403 => crate::Error::TarballHTTP403, - 404 => crate::Error::TarballHTTP404, - 405..=499 => crate::Error::TarballHTTP4xx, - _ => crate::Error::TarballHTTP5xx, - }; + let err = match response.status_code { + 400 => crate::Error::TarballHTTP400, + 401 => crate::Error::TarballHTTP401, + 402 => crate::Error::TarballHTTP402, + 403 => crate::Error::TarballHTTP403, + 404 => crate::Error::TarballHTTP404, + 405..=499 => crate::Error::TarballHTTP4xx, + _ => crate::Error::TarballHTTP5xx, + }; - if C::IS_STORE_INSTALLER { - C::on_package_download_error_store( - extract_ctx, - task.task_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } else { - let package_id = manager.lockfile.buffers.resolutions - [extract.dependency_id as usize]; - C::on_package_download_error_pkg( - extract_ctx, - package_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } + let Some(is_required) = + dispatch_tarball_error::(manager, extract_ctx, task, extract, err) + else { continue; - } + }; if is_required { bun_ast::add_error_pretty!( @@ -856,16 +766,7 @@ pub fn run_tasks( response.status_code, ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, extract.name.slice()) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, extract.name.slice()); if let Some(removed) = manager.task_queue.remove(&task.task_id) { drop(removed); @@ -1564,6 +1465,68 @@ pub fn run_tasks( Ok(()) } +/// Non-retryable tarball download failure. Drops the dedupe state before +/// dispatching the error — otherwise a later `enqueuePackageForDownload` for +/// the same package sees `found_existing`, never schedules a network task, +/// and waits forever for a callback that will not arrive. +/// `Store.Installer.onPackageDownloadError` drains `task_queue` itself but +/// does not touch `network_dedupe_map`, so this must run on the callback path +/// too. Returns `None` when the `on_package_download_error_*` callback +/// consumed the error, otherwise `Some(is_required)` for the caller's +/// log-and-mark fallback. `is_required` is captured before the dedupe entry +/// is removed — `isNetworkTaskRequired` reads the map and returns `true` when +/// the entry is gone, which would upgrade optional-dep warnings to errors. +fn dispatch_tarball_error( + manager: &mut PackageManager, + extract_ctx: &mut C::Ctx, + task: &NetworkTask, + extract: &ExtractTarball, + err: crate::Error, +) -> Option { + let is_required = manager.is_network_task_required(task.task_id); + let _ = manager.network_dedupe_map.remove(&task.task_id); + + if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { + if C::IS_STORE_INSTALLER { + C::on_package_download_error_store( + extract_ctx, + task.task_id, + extract.name.slice(), + &extract.resolution, + err, + &task.url_buf, + ); + } else { + let package_id = manager.lockfile.buffers.resolutions[extract.dependency_id as usize]; + C::on_package_download_error_pkg( + extract_ctx, + package_id, + extract.name.slice(), + &extract.resolution, + err, + &task.url_buf, + ); + } + return None; + } + + Some(is_required) +} + +fn mark_update_request_failed(manager: &mut PackageManager, name: &[u8]) { + if manager.subcommand == Subcommand::Remove { + return; + } + for request in manager.update_requests.iter_mut() { + if strings::eql(request.name, name) { + request.failed = true; + manager.options.do_.remove(Do::SAVE_LOCKFILE); + manager.options.do_.remove(Do::SAVE_YARN_LOCK); + manager.options.do_.remove(Do::INSTALL_PACKAGES); + } + } +} + #[inline] pub fn pending_task_count(manager: &PackageManager) -> u32 { manager.pending_tasks.load(Ordering::Acquire) diff --git a/src/install/PackageManager/updatePackageJSONAndInstall.rs b/src/install/PackageManager/updatePackageJSONAndInstall.rs index c033b9f19c4e..69cecbaca0db 100644 --- a/src/install/PackageManager/updatePackageJSONAndInstall.rs +++ b/src/install/PackageManager/updatePackageJSONAndInstall.rs @@ -1,10 +1,9 @@ use crate::lockfile::package::PackageColumns as _; +use bstr::BStr; use bun_collections::VecExt; use core::fmt; use std::borrow::Cow; -use bstr::BStr; - use crate::Error; use crate::ShellCompletions; use crate::bun_fs::FileSystem; @@ -618,25 +617,22 @@ fn update_package_json_and_install_with_manager_with_updates( let (source, path): (&[u8], &ZStr) = if matches!(manager.options.patch_features, PatchFeatures::Commit { .. }) { 'source_and_path: { - let root_package_json_entry = match manager - .workspace_package_json_cache - .get_with_path( + let root_package_json_entry = + match manager.workspace_package_json_cache.get_with_path( manager.log_mut(), root_package_json_path.as_bytes(), GetJSONOptions::default(), - ) - .unwrap() - { - Ok(e) => e, - Err(err) => { - Output::err( - err, - "failed to read/parse package.json at '{s}'", - (BStr::new(root_package_json_path.as_bytes()),), - ); - Global::exit(1); - } - }; + ) { + GetResult::Entry(entry) => entry, + GetResult::ReadErr(err) | GetResult::ParseErr(err) => { + Output::err( + err, + "failed to read/parse package.json at '{s}'", + (BStr::new(root_package_json_path.as_bytes()),), + ); + Global::exit(1); + } + }; break 'source_and_path ( &root_package_json_entry.source.contents, diff --git a/src/install/hosted_git_info.rs b/src/install/hosted_git_info.rs index bb14f8fb174c..d4aa843def83 100644 --- a/src/install/hosted_git_info.rs +++ b/src/install/hosted_git_info.rs @@ -1070,6 +1070,45 @@ pub mod formatters { } } + /// Shared writer for every URL template: the variants differ only in the + /// literal prefix, the separator after the host, whether auth/user + /// segments are present, and the suffix. + /// + /// Produces `{prefix}{auth}@{host}{host_sep}{user}/{project}{suffix}#{committish}`, + /// omitting the `@`, `/`, and `#` separators alongside their absent segments. + fn format_template( + prefix: &str, + auth: Option<&[u8]>, + host: &[u8], + host_sep: &str, + user: Option<&[u8]>, + project: &[u8], + suffix: &str, + committish: Option<&[u8]>, + ) -> Result, AllocError> { + let auth_str: &[u8] = auth.unwrap_or(b""); + let auth_sep = if auth_str.is_empty() { "" } else { "@" }; + let (user_str, user_sep): (&[u8], &str) = match user { + Some(u) => (u, "/"), + None => (b"", ""), + }; + let cmsh: &[u8] = committish.unwrap_or(b""); + let cmsh_sep = if cmsh.is_empty() { "" } else { "#" }; + + let mut v = Vec::new(); + write!( + &mut v, + "{prefix}{}{auth_sep}{}{host_sep}{}{user_sep}{}{suffix}{cmsh_sep}{}", + BStr::new(auth_str), + BStr::new(host), + BStr::new(user_str), + BStr::new(project), + BStr::new(cmsh), + ) + .map_err(|_| AllocError)?; + Ok(v) + } + /// Mirrors hosts.js's sshtemplate pub mod ssh { use super::*; @@ -1088,21 +1127,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git@{}:{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git@", + None, + self_.domain(), + ":", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1111,20 +1145,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git@{}:{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git@", + None, + self_.domain(), + ":", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1146,21 +1176,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+ssh://git@{}/{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+ssh://git@", + None, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1169,20 +1194,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+ssh://git@{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+ssh://git@", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1206,26 +1227,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let auth_str: &[u8] = auth.unwrap_or(b""); - let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" }; - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+https://{}{}{}/{}/{}.git{}{}", - BStr::new(auth_str), - BStr::new(auth_sep), - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+https://", + auth, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1235,20 +1246,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+https://{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+https://", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn sourcehut( @@ -1259,22 +1266,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "https://{}/{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "https://", + None, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1296,22 +1297,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "{}{}/{}{}{}", - BStr::new(self_.shortcut()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "", + None, + self_.shortcut(), + "", + user, + project, + "", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1320,20 +1315,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "{}{}{}{}", - BStr::new(self_.shortcut()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "", + None, + self_.shortcut(), + "", + None, + project, + "", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1343,6 +1334,42 @@ pub mod formatters { pub type Type = fn(url: &JscUrl) -> Result, HostedGitInfoError>; + /// Percent-decode the parts into a single owned buffer and build the + /// `ExtractResult` over it. + fn build_result( + user: Option<&[u8]>, + project: &[u8], + committish: Option<&[u8]>, + ) -> Result { + let mut sb = StringBuilder::default(); + if let Some(u) = user { + sb.count(u); + } + sb.count(project); + if let Some(c) = committish { + sb.count(c); + } + + sb.allocate()?; + + let user_slice = match user { + Some(u) => Some(HostedGitInfo::decode_and_append(&mut sb, u)?), + None => None, + }; + let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; + let committish_slice = match committish { + Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), + None => None, + }; + + Ok(ExtractResult { + user: user_slice, + project: project_slice, + committish: committish_slice, + _owned_buffer: Some(sb.move_to_slice()), + }) + } + pub(crate) fn github(url: &JscUrl) -> Result, HostedGitInfoError> { let pathname_owned = url.pathname().to_owned_slice(); let pathname = strings::trim_prefix(&pathname_owned, b"/"); @@ -1387,54 +1414,54 @@ pub mod formatters { committish_part }; - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - sb.allocate()?; - - let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?; - let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; - let committish_slice = match committish { - Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), - None => None, - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + Ok(Some(build_result(Some(user_part), project, committish)?)) } - pub(crate) fn bitbucket(url: &JscUrl) -> Result, HostedGitInfoError> { + /// Shared tail for hosts whose URL shape is `/user/project[.git][/aux]` + /// with the committish in the fragment: reject `aux == reject_aux`, + /// trim `.git`, require a non-empty project (and user, unless + /// `user_optional`, in which case a lone segment is the project), then + /// build the result. With `error_as_none`, allocation or decode + /// failure maps to "not a hosted git URL" rather than an error. + fn user_project_committish( + url: &JscUrl, + reject_aux: &[u8], + user_optional: bool, + error_as_none: bool, + ) -> Result, HostedGitInfoError> { let pathname_owned = url.pathname().to_owned_slice(); let pathname = strings::trim_prefix(&pathname_owned, b"/"); let mut iter = pathname.split(|&b| b == b'/'); - let Some(user_part) = iter.next() else { + let Some(mut user_part) = iter.next() else { return Ok(None); }; - let Some(project_part) = iter.next() else { + let mut project_part = iter.next(); + + if iter.next() == Some(reject_aux) { return Ok(None); - }; - let aux = iter.next(); + } - if let Some(a) = aux { - if a == b"get" { - return Ok(None); - } + if user_optional && project_part.is_none_or(<[u8]>::is_empty) { + project_part = Some(user_part); + user_part = b""; } + let Some(project_part) = project_part else { + return Ok(None); + }; let project = strings::trim_suffix(project_part, b".git"); - - if user_part.is_empty() || project.is_empty() { + if project.is_empty() { return Ok(None); } + let user: Option<&[u8]> = if user_part.is_empty() { + if !user_optional { + return Ok(None); + } + None + } else { + Some(user_part) + }; let fragment_str = OwnedString::new(url.fragment_identifier()); let fragment_utf8 = fragment_str.to_utf8(); @@ -1445,28 +1472,17 @@ pub mod formatters { None }; - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); + match build_result(user, project, committish) { + Ok(result) => Ok(Some(result)), + Err(_) if error_as_none => Ok(None), + Err(err) => Err(err), } + } - sb.allocate()?; - - let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?; - let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; - let committish_slice = match committish { - Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), - None => None, - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + pub(crate) fn bitbucket(url: &JscUrl) -> Result, HostedGitInfoError> { + user_project_committish( + url, b"get", /* user_optional */ false, /* error_as_none */ false, + ) } pub(crate) fn gitlab(url: &JscUrl) -> Result, HostedGitInfoError> { @@ -1524,176 +1540,15 @@ pub mod formatters { } pub(crate) fn gist(url: &JscUrl) -> Result, HostedGitInfoError> { - let pathname_owned = url.pathname().to_owned_slice(); - let pathname = strings::trim_prefix(&pathname_owned, b"/"); - - let mut iter = pathname.split(|&b| b == b'/'); - let Some(mut user_part) = iter.next() else { - return Ok(None); - }; - let mut project_part = iter.next(); - let aux = iter.next(); - - if let Some(a) = aux { - if a == b"raw" { - return Ok(None); - } - } - - if project_part.is_none() || project_part.unwrap().is_empty() { - project_part = Some(user_part); - user_part = b""; - } - - let project = strings::trim_suffix(project_part.unwrap(), b".git"); - let user: Option<&[u8]> = if !user_part.is_empty() { - Some(user_part) - } else { - None - }; - - if project.is_empty() { - return Ok(None); - } - - let fragment_str = OwnedString::new(url.fragment_identifier()); - let fragment_utf8 = fragment_str.to_utf8(); - let fragment = fragment_utf8.slice(); - let committish: Option<&[u8]> = if !fragment.is_empty() { - Some(fragment) - } else { - None - }; - - let mut sb = StringBuilder::default(); - if let Some(u) = user { - sb.count(u); - } - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - let Ok(()) = sb.allocate() else { - return Ok(None); - }; - - let user_slice = match user { - Some(u) => { - let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, u) else { - return Ok(None); - }; - Some(r) - } - None => None, - }; - let Ok(project_slice) = HostedGitInfo::decode_and_append(&mut sb, project) else { - return Ok(None); - }; - let committish_slice = match committish { - Some(c) => { - let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, c) else { - return Ok(None); - }; - Some(r) - } - None => None, - }; - - Ok(Some(ExtractResult { - user: user_slice, - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + user_project_committish( + url, b"raw", /* user_optional */ true, /* error_as_none */ true, + ) } pub(crate) fn sourcehut(url: &JscUrl) -> Result, HostedGitInfoError> { - let pathname_owned = url.pathname().to_owned_slice(); - let pathname = strings::trim_prefix(&pathname_owned, b"/"); - - let mut iter = pathname.split(|&b| b == b'/'); - let Some(user_part) = iter.next() else { - return Ok(None); - }; - let Some(project_part) = iter.next() else { - return Ok(None); - }; - let aux = iter.next(); - - if let Some(a) = aux { - if a == b"archive" { - return Ok(None); - } - } - - let project = strings::trim_suffix(project_part, b".git"); - - if user_part.is_empty() || project.is_empty() { - return Ok(None); - } - - let fragment_str = OwnedString::new(url.fragment_identifier()); - let fragment_utf8 = fragment_str.to_utf8(); - let fragment = fragment_utf8.slice(); - let committish: Option<&[u8]> = if !fragment.is_empty() { - Some(fragment) - } else { - None - }; - - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - let Ok(()) = sb.allocate() else { - return Ok(None); - }; - - // Inline percent-decode rather than `decode_and_append`: this path - // returns None instead of erroring on decode failure. - let user_slice = 'blk: { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, user_part) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - break 'blk start..start + decoded_len; - }; - let project_slice = 'blk: { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, project) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - break 'blk start..start + decoded_len; - }; - let committish_slice = if let Some(c) = committish { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, c) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - Some(start..start + decoded_len) - } else { - None - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + user_project_committish( + url, b"archive", /* user_optional */ false, /* error_as_none */ true, + ) } } @@ -1721,26 +1576,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let auth_str: &[u8] = auth.unwrap_or(b""); - let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" }; - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git://{}{}{}/{}/{}.git{}{}", - BStr::new(auth_str), - BStr::new(auth_sep), - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git://", + auth, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1750,20 +1595,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git://{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git://", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } } diff --git a/src/install/isolated_install.rs b/src/install/isolated_install.rs index 1b3242e965c0..d56490630706 100644 --- a/src/install/isolated_install.rs +++ b/src/install/isolated_install.rs @@ -2404,6 +2404,29 @@ pub(crate) fn install_isolated_packages( let dep = &lockfile_ro.buffers.dependencies[dep_id as usize]; + // Shared failure path for the enqueue-for-download arms + // below; the caller `continue`s after invoking it. + let fail_enqueue = |installer: &mut store::Installer, err, what| { + Output::err( + err, + "failed to enqueue {} for download: {}@{}", + ( + what, + BStr::new(pkg_name.slice(string_buf)), + pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), + ), + ); + Output::flush(); + if installer.manager().options.enable.fail_early() { + Global::exit(1); + } + // .monotonic is okay because an error means the task isn't + // running on another thread. + entry_steps[entry_id.get() as usize] + .store(installer::Step::Done as u32, Ordering::Relaxed); + installer.on_task_complete(entry_id, installer::CompleteState::Fail); + }; + match pkg_res_tag { ResolutionTag::Npm => { match installer.manager_mut().enqueue_package_for_download( @@ -2421,24 +2444,7 @@ pub(crate) fn install_isolated_packages( } Err(err) => { // error.InvalidURL - Output::err( - err, - "failed to enqueue package for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "package"); continue; } } @@ -2470,24 +2476,7 @@ pub(crate) fn install_isolated_packages( bun_core::out_of_memory() } Err(err) => { - Output::err( - err, - "failed to enqueue github package for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "github package"); continue; } } @@ -2514,24 +2503,7 @@ pub(crate) fn install_isolated_packages( bun_core::out_of_memory() } Err(err) => { - Output::err( - err, - "failed to enqueue tarball for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "tarball"); continue; } } diff --git a/src/install/npm.rs b/src/install/npm.rs index 2f27885f2a1f..07c5bdc7d84b 100644 --- a/src/install/npm.rs +++ b/src/install/npm.rs @@ -671,6 +671,38 @@ pub(crate) fn negatable_from_json_value(value: &JSON::E::JsonV this.combine() } +/// Resets and refills `bundled_deps_set` / `bundle_all_deps` from a version's +/// `bundleDependencies` (or legacy `bundledDependencies`) field. +fn extract_bundled_deps( + version_obj: Option<&JSON::E::ObjectJSON>, + bundled_deps_set: &mut StringSet, + bundle_all_deps: &mut bool, +) -> Result<(), AllocError> { + bundled_deps_set.map.clear_retaining_capacity(); + *bundle_all_deps = false; + let Some(bundled_deps_value) = version_obj + .and_then(|o| o.get(b"bundleDependencies")) + .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) + else { + return Ok(()); + }; + match bundled_deps_value { + JSON::E::JsonValue::Boolean(boolean) => { + *bundle_all_deps = *boolean; + } + JSON::E::JsonValue::Array(arr) => { + for bundled_dep in arr.get().items() { + let Some(s) = bundled_dep.as_str() else { + continue; + }; + bundled_deps_set.insert(s)?; + } + } + _ => {} + } + Ok(()) +} + // ────────────────────────────────────────────────────────────────────────── #[repr(C)] @@ -2023,7 +2055,7 @@ impl PackageManifest { let mut optional_peer_dep_names: Vec = Vec::new(); let mut bundled_deps_set = StringSet::init(); - let mut bundle_all_deps: bool; + let mut bundle_all_deps = false; let mut bundled_deps_count: usize = 0; @@ -2159,27 +2191,7 @@ impl PackageManifest { } } - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_value) = version_obj - .and_then(|o| o.get(b"bundleDependencies")) - .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) - { - match bundled_deps_value { - JSON::E::JsonValue::Boolean(boolean) => { - bundle_all_deps = *boolean; - } - JSON::E::JsonValue::Array(arr) => { - for bundled_dep in arr.get().items() { - let Some(s) = bundled_dep.as_str() else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps(version_obj, &mut bundled_deps_set, &mut bundle_all_deps)?; for pair in &DEPENDENCY_GROUPS { if let Some(obj) = version_obj @@ -2366,27 +2378,7 @@ impl PackageManifest { let version_obj = prop.value.as_object(); - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_value) = version_obj - .and_then(|o| o.get(b"bundleDependencies")) - .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) - { - match bundled_deps_value { - JSON::E::JsonValue::Boolean(boolean) => { - bundle_all_deps = *boolean; - } - JSON::E::JsonValue::Array(arr) => { - for bundled_dep in arr.get().items() { - let Some(s) = bundled_dep.as_str() else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps(version_obj, &mut bundled_deps_set, &mut bundle_all_deps)?; let mut package_version: PackageVersion = empty_version; diff --git a/src/install/yarn.rs b/src/install/yarn.rs index c9c8f7f0dcc9..8eba160b8bf9 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -1806,169 +1806,24 @@ pub(crate) fn migrate_yarn_lockfile<'a>( let deps_off = u32::try_from(this.buffers.dependencies.len()).expect("int cast"); let resolutions_off = u32::try_from(this.buffers.resolutions.len()).expect("int cast"); - if let Some(deps) = &entry.dependencies { - for (dep_name_key, dep_version_ref) in deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::PROD, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } - - if let Some(optional_deps) = &entry.optional_dependencies { - for (dep_name_key, dep_version_ref) in optional_deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::OPTIONAL, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } - - if let Some(peer_deps) = &entry.peer_dependencies { - for (dep_name_key, dep_version_ref) in peer_deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::PEER, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } + let dep_groups = [ + (entry.dependencies.as_ref(), dependency::Behavior::PROD), + ( + entry.optional_dependencies.as_ref(), + dependency::Behavior::OPTIONAL, + ), + (entry.peer_dependencies.as_ref(), dependency::Behavior::PEER), + (entry.dev_dependencies.as_ref(), dependency::Behavior::DEV), + ]; - if let Some(dev_deps) = &entry.dev_dependencies { - for (dep_name_key, dep_version_ref) in dev_deps.iter() { + for (deps, behavior) in dep_groups { + let Some(deps) = deps else { continue }; + for (dep_name_key, dep_version_ref) in deps.iter() { let dep_name: &[u8] = dep_name_key.as_ref(); let dep_version_literal: &[u8] = *dep_version_ref; let name_hash = string_hash(dep_name); let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - let dep_version_string = sbuf!().append(dep_version_literal)?; let sliced_string = SlicedString::init( dep_version_string.slice(this.buffers.string_bytes.as_slice()), @@ -1991,7 +1846,7 @@ pub(crate) fn migrate_yarn_lockfile<'a>( name: dep_name_string, name_hash, version: parsed_version, - behavior: dependency::Behavior::DEV, + behavior, }); let mut dep_spec = Vec::new(); diff --git a/src/io/PipeWriter.rs b/src/io/PipeWriter.rs index e9a80c6cb7cd..ac3edafa1311 100644 --- a/src/io/PipeWriter.rs +++ b/src/io/PipeWriter.rs @@ -2385,44 +2385,56 @@ impl WindowsStreamingWriter { } } + /// Blocking write for the `Source::SyncFile` path. Encodes via + /// `write_or_fallback`, then loops `sys::write` until drained. + #[inline] + fn write_sync_file( + &mut self, + buffer_u8: Option<&[u8]>, + buffer_u16: Option<&[u16]>, + kind: WriteKind, + ) -> WriteResult { + let fd = Fd::from_uv(match &self.source { + Some(Source::SyncFile(f)) => f.file, + _ => unreachable!(), + }); + let result = (|| { + let remain = match self.outgoing.write_or_fallback(buffer_u8, buffer_u16, kind) { + Ok(r) => r, + Err(_) => return WriteResult::Err(sys::Error::oom()), + }; + let initial_len = remain.len(); + let mut remain = remain; + + while remain.len() > 0 { + match sys::write(fd, remain) { + sys::Result::Err(err) => return WriteResult::Err(err), + sys::Result::Ok(wrote) => { + remain = &remain[wrote..]; + if wrote == 0 { + break; + } + } + } + } + + let wrote = initial_len - remain.len(); + if wrote == 0 { + return WriteResult::Done(wrote); + } + WriteResult::Wrote(wrote) + })(); + self.outgoing.reset(); + result + } + fn write_internal_u8(&mut self, buffer: &[u8], kind: WriteKind) -> WriteResult { if self.is_done { return WriteResult::Done(0); } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = match self.outgoing.write_or_fallback(Some(buffer), None, kind) { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(Some(buffer), None, kind); } let had_buffered_data = self.outgoing.is_not_empty(); @@ -2447,42 +2459,7 @@ impl WindowsStreamingWriter { } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = - match self - .outgoing - .write_or_fallback(None, Some(buffer), WriteKind::Utf16) - { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(None, Some(buffer), WriteKind::Utf16); } let had_buffered_data = self.outgoing.is_not_empty(); diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs index 8f36b301a9a7..dd6f9c744b03 100644 --- a/src/io/posix_event_loop.rs +++ b/src/io/posix_event_loop.rs @@ -2,7 +2,6 @@ use core::ffi::c_int; use core::ffi::c_void; use core::fmt; -#[cfg(unix)] use core::ptr; #[cfg(not(windows))] @@ -289,6 +288,108 @@ pub enum AllocatorType { Mini, } +/// Pure flag-accessor methods whose bodies are identical for the POSIX and +/// Windows `FilePoll` types (distinct structs — see `windows_event_loop`). +/// Expanded inside each platform's `impl FilePoll`; `Flags` and `Fd` resolve +/// at the expansion site. Platform-divergent methods (the keep-alive / +/// activate family) stay in their own modules — their flag semantics differ. +macro_rules! impl_file_poll_flag_methods { + () => { + #[inline] + pub fn is_active(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn is_watching(&self) -> bool { + !self.flags.contains(Flags::NeedsRearm) + && (self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollProcess)) + } + + pub fn is_registered(&self) -> bool { + self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollProcess) + || self.flags.contains(Flags::PollMachport) + || self.flags.contains(Flags::PollMemoryPressure) + } + + pub fn clear_event(&mut self, flag: Flags) { + self.flags.remove(flag); + } + + pub fn is_readable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Readable); + self.flags.remove(Flags::Readable); + readable + } + + pub fn is_hup(&mut self) -> bool { + let readable = self.flags.contains(Flags::Hup); + self.flags.remove(Flags::Hup); + readable + } + + pub fn is_eof(&mut self) -> bool { + let readable = self.flags.contains(Flags::Eof); + self.flags.remove(Flags::Eof); + readable + } + + pub fn is_writable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Writable); + self.flags.remove(Flags::Writable); + readable + } + + #[inline] + pub fn can_unref(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn file_descriptor(&self) -> Fd { + self.fd + } + }; +} +// Only `windows_event_loop` needs the path-based re-export; this module +// invokes the macros textually. +#[cfg(windows)] +pub(crate) use impl_file_poll_flag_methods; + +/// `PollSlot` impl shared verbatim by both platform `FilePoll` types; both +/// have the `next_to_free` / `flags` fields the trait contract requires. +macro_rules! impl_poll_slot { + ($t:ty) => { + impl $crate::posix_event_loop::PollSlot for $t { + #[inline] + unsafe fn next_to_free(p: *mut Self) -> *mut Self { + // SAFETY: caller upholds the trait-level contract (`p` is a live hive + // slot; raw-pointer field op only). + unsafe { (*p).next_to_free } + } + #[inline] + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { (*p).next_to_free = next } + } + #[inline] + unsafe fn ignore_updates(p: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { + (*p).flags + .insert($crate::posix_event_loop::Flags::IgnoreUpdates) + }; + } + } + }; +} +#[cfg(windows)] +pub(crate) use impl_poll_slot; + // `FilePoll`/`Store` here are POSIX-specific (kqueue/epoll registration, // generation_number, allocator_type). On Windows the variants live in // `windows_event_loop`; the shared `EventLoopCtxVTable` above names @@ -381,33 +482,7 @@ impl FilePoll { self.on_update(0); } - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } + impl_file_poll_flag_methods!(); // Note: not `impl Drop` — FilePoll is pool-allocated (HiveArray) and explicitly // put back via `Store::put`; Drop would be wrong here. @@ -448,14 +523,6 @@ impl FilePoll { self.deinit_possibly_defer(vm, false); } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - || self.flags.contains(Flags::PollMemoryPressure) - } - pub fn on_update(&mut self, size_or_offset: i64) { if self.flags.contains(Flags::OneShot) && !self.flags.contains(Flags::NeedsRearm) { self.flags.insert(Flags::NeedsRearm); @@ -471,19 +538,6 @@ impl FilePoll { unsafe { __bun_run_file_poll(self, size_or_offset) }; } - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } - /// This decrements the active counter if it was previously incremented /// "active" controls whether or not the event loop should potentially idle pub fn disable_keeping_process_alive(&mut self, event_loop_ctx: EventLoopCtx) { @@ -598,11 +652,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - /// Prevent a poll from keeping the process alive. pub fn unref(&mut self, event_loop_ctx: EventLoopCtx) { syslog!("unref"); @@ -626,11 +675,6 @@ impl FilePoll { self.deactivate(event_loop_ctx.loop_mut()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn register(&mut self, loop_: &mut Loop, flag: Flags, one_shot: bool) -> sys::Result<()> { self.register_with_fd( loop_, @@ -1401,25 +1445,49 @@ impl fmt::Display for FlagsFormatter { // `bun_alloc::heap_breakdown` is a no-op outside macOS Instruments // heap-breakdown builds, so the 128-slot hive is unconditional here (same // choice as `RuntimeTranspilerStore`'s TranspilerJob hive). -#[cfg(not(windows))] const HIVE_SIZE: usize = 128; + +/// Raw-pointer field accessors implemented by the platform `FilePoll` types so +/// the deferred-free [`PollStore`] can link slots intrusively. +/// +/// Every method shares one contract: `p` is a live, fully-initialized hive +/// slot, and the body performs raw-pointer field ops only — materializing a +/// `&mut Self` would alias the `&mut PollStore` borrow that covers the inline +/// hive buffer (Stacked Borrows UB). +pub trait PollSlot: Sized { + /// # Safety + /// See the trait-level contract. + unsafe fn next_to_free(p: *mut Self) -> *mut Self; + /// # Safety + /// See the trait-level contract. + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self); + /// Insert `Flags::IgnoreUpdates`. + /// # Safety + /// See the trait-level contract. + unsafe fn ignore_updates(p: *mut Self); +} + #[cfg(not(windows))] -type FilePollHive = bun_collections::hive_array::Fallback; +pub type Store = PollStore; + +#[cfg(not(windows))] +impl_poll_slot!(FilePoll); /// We defer freeing FilePoll until the end of the next event loop iteration /// This ensures that we don't free a FilePoll before the next callback is called -#[cfg(not(windows))] -pub struct Store { - hive: FilePollHive, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, +pub struct PollStore { + hive: bun_collections::hive_array::Fallback, + pending_free_head: *mut P, + pending_free_tail: *mut P, } -#[cfg(not(windows))] -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHive::init(), +impl PollStore

{ + pub fn init() -> Self { + // `hive.put` recycles slots without honoring drop glue on the deferred + // path's assumptions; the store requires plain-old-data slots. + const { assert!(!core::mem::needs_drop::

()) }; + PollStore { + hive: bun_collections::hive_array::Fallback::init(), pending_free_head: ptr::null_mut(), pending_free_tail: ptr::null_mut(), } @@ -1427,7 +1495,7 @@ impl Store { /// Claim a hive slot and move `value` into it. Infallible (heap fallback). #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { + pub fn get_init(&mut self, value: P) -> ptr::NonNull

{ self.hive.get_init(value) } @@ -1436,13 +1504,14 @@ impl Store { while !next.is_null() { let current = next; // SAFETY: intrusive list; nodes were allocated by this hive. Walk via - // raw-pointer reads/writes only — materializing a `&mut FilePoll` + // raw-pointer reads/writes only — materializing a `&mut P` // here would alias the `&mut self.hive` borrow taken by `put()` // below (the slot may live inside the inline hive array). unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. + next = P::next_to_free(current); + P::set_next_to_free(current, ptr::null_mut()); + // `P` has no drop glue (asserted in `init`); `put` is a no-op + // drop + recycle. self.hive.put(current); } } @@ -1451,29 +1520,29 @@ impl Store { } /// `poll` is a live, fully-initialized slot in `self.hive`. It may point - /// *inside* `self.hive`'s inline `[FilePoll; 128]` buffer, so accepting it - /// as `&mut FilePoll` while `&mut self` is live would retag overlapping + /// *inside* `self.hive`'s inline `[P; 128]` buffer, so accepting it + /// as `&mut P` while `&mut self` is live would retag overlapping /// storage under Stacked Borrows (UB). Take it as a raw pointer and /// touch fields only through raw pointer ops — same /// rationale as `process_deferred_frees` above. - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { + pub fn put(&mut self, poll: ptr::NonNull

, vm: EventLoopCtx, ever_registered: bool) { let poll = poll.as_ptr(); if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no + // SAFETY: `poll` is a fully-initialized hive slot; `P` has no // drop glue, so `put` is a no-op drop + recycle. unsafe { self.hive.put(poll) }; return; } // SAFETY: `poll` is a live hive slot (see fn-level comment); raw read of a POD field. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); + debug_assert!(unsafe { P::next_to_free(poll) }.is_null()); if !self.pending_free_tail.is_null() { debug_assert!(!self.pending_free_head.is_null()); // SAFETY: tail is non-null and points into the hive. unsafe { - debug_assert!((*self.pending_free_tail).next_to_free.is_null()); - (*self.pending_free_tail).next_to_free = poll; + debug_assert!(P::next_to_free(self.pending_free_tail).is_null()); + P::set_next_to_free(self.pending_free_tail, poll); } } @@ -1483,7 +1552,7 @@ impl Store { } // SAFETY: see fn-level comment — raw-pointer field access only. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; + unsafe { P::ignore_updates(poll) }; self.pending_free_tail = poll; let callback: OpaqueCallback = Self::process_deferred_frees_thunk; @@ -1493,16 +1562,16 @@ impl Store { ); vm.set_after_event_loop_callback( Some(callback), - core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), + core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), ); } // Safe fn item: module-private thunk, only coerced to the C-ABI // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. + // the store. Body wraps its raw-ptr op explicitly. extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: ctx was set to `self as *mut Store` in `put` above. - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; + // SAFETY: ctx was set to `self as *mut Self` in `put` above. + let this = unsafe { bun_ptr::callback_ctx::(ctx) }; this.process_deferred_frees(); } } diff --git a/src/io/windows_event_loop.rs b/src/io/windows_event_loop.rs index 09b2416eb120..b1237902e4f0 100644 --- a/src/io/windows_event_loop.rs +++ b/src/io/windows_event_loop.rs @@ -1,4 +1,3 @@ -use core::ffi::c_void; use core::ptr; use bun_sys::Fd; @@ -40,31 +39,13 @@ pub struct FilePoll { } impl FilePoll { - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } + posix::impl_file_poll_flag_methods!(); #[inline] pub fn is_keeping_process_alive(&self) -> bool { !self.flags.contains(Flags::Closed) && self.is_active() } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - } - /// Make calling ref() on this poll into a no-op. pub fn disable_keeping_process_alive(&mut self, vm: EventLoopCtx) { if self.flags.contains(Flags::Closed) { @@ -102,11 +83,6 @@ impl FilePoll { self.deinit_with_vm(js_vm_ctx()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn deinit_force_unregister(&mut self) { self.deinit() } @@ -150,34 +126,6 @@ impl FilePoll { vm.file_polls_mut().put(this, vm, was_ever_registered); } - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } - pub fn deinit_with_vm(&mut self, vm: EventLoopCtx) { // `loop_mut()` — crate-private nonnull-asref accessor (single deref in // `EventLoopCtx`); the uws loop is a disjoint allocation from `self`. @@ -233,11 +181,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - pub fn on_ended(&mut self, event_loop_ctx: EventLoopCtx) { self.flags.remove(Flags::KeepsEventLoopAlive); self.flags.insert(Flags::Closed); @@ -267,104 +210,9 @@ impl FilePoll { } } -type FilePollHiveArray = bun_collections::hive_array::Fallback; - -pub struct Store { - hive: FilePollHiveArray, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, -} - -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHiveArray::init(), - pending_free_head: ptr::null_mut(), - pending_free_tail: ptr::null_mut(), - } - } - - #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { - self.hive.get_init(value) - } - - pub fn process_deferred_frees(&mut self) { - let mut next = self.pending_free_head; - while !next.is_null() { - let current = next; - // SAFETY: intrusive deferred-free list; nodes are valid HiveArray slots - // until put(). Walk via raw-pointer reads/writes only — materializing a - // `&mut FilePoll` here would alias the `&mut self.hive` borrow taken by - // `put()` below (the slot may live inside the inline hive buffer). - unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. - self.hive.put(current); - } - } - self.pending_free_head = ptr::null_mut(); - self.pending_free_tail = ptr::null_mut(); - } - - /// `poll` is a live, fully-initialized slot in `self.hive`. Touched only - /// through raw pointer ops to avoid forming a `&mut FilePoll` that would - /// alias `&mut self` (the hive buffer is inline storage). - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { - let poll = poll.as_ptr(); - if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no - // drop glue, so `put` is a no-op drop + recycle. - unsafe { self.hive.put(poll) }; - return; - } - - // SAFETY: `poll` is a valid HiveArray slot pointer. It may live inside - // `self.hive.buffer`, so we access it via raw pointer only (no `&mut FilePoll` - // materialized) to avoid aliasing `&mut self`. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); - - let tail = self.pending_free_tail; - if !tail.is_null() { - debug_assert!(!self.pending_free_head.is_null()); - // SAFETY: `tail` is a valid slot in the intrusive deferred-free list; - // raw-ptr access avoids a second `&mut FilePoll` overlapping `poll`/`self`. - debug_assert!(unsafe { (*tail).next_to_free }.is_null()); - unsafe { (*tail).next_to_free = poll }; - } - - if self.pending_free_head.is_null() { - self.pending_free_head = poll; - debug_assert!(self.pending_free_tail.is_null()); - } - - // SAFETY: see above — short-lived field borrow through raw `poll`, no overlap held. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; - self.pending_free_tail = poll; +pub type Store = posix::PollStore; - let callback: OpaqueCallback = Self::process_deferred_frees_thunk; - debug_assert!( - vm.after_event_loop_callback().is_none() - || vm.after_event_loop_callback().map(|f| f as usize) == Some(callback as usize) - ); - vm.set_after_event_loop_callback( - Some(callback), - core::ptr::NonNull::new(core::ptr::from_mut::(self).cast::()), - ); - } - - // Safe fn item: module-private thunk, only coerced to the C-ABI - // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. - extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: `ctx` was set to `self as *mut Store` in `put` above. The thunk fires - // from the event loop's after-tick hook with no other `&mut Store` borrow live, - // so this is the unique accessor (safe-single-owner). - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; - this.process_deferred_frees(); - } -} +posix::impl_poll_slot!(FilePoll); pub struct Waker { // `BackRef`: `WindowsLoop::get()` hands out the shared diff --git a/src/paths/lib.rs b/src/paths/lib.rs index 91f89e1e3353..40136edb54ff 100644 --- a/src/paths/lib.rs +++ b/src/paths/lib.rs @@ -690,12 +690,6 @@ pub mod fs { self.base } - /// The dir component, or `"."` when it is empty. - #[inline] - pub fn dir_or_dot(&self) -> &'a [u8] { - if self.dir.is_empty() { b"." } else { self.dir } - } - /// Formats [`Self::non_unique_name_string_base`] as a valid JS /// identifier. #[inline] @@ -931,23 +925,6 @@ pub mod fs { } } - /// Same const-concat caveat as `init_with_namespace_virtual`: - /// callers pass the precomputed `concatcp!` result as `pretty`. - #[inline] - pub const fn init_for_kit_built_in( - namespace: &'static [u8], - pretty: &'static [u8], - text: &'static [u8], - ) -> Path<'static> { - Path { - pretty, - is_symlink: true, - text, - namespace, - is_disabled: false, - } - } - /// Debug-only check that `pretty` /// contains no backslashes (Windows). No-op on POSIX. #[inline] diff --git a/src/paths/resolve_path.rs b/src/paths/resolve_path.rs index b7c2eb32ac9b..829b1a52acad 100644 --- a/src/paths/resolve_path.rs +++ b/src/paths/resolve_path.rs @@ -48,9 +48,6 @@ pub fn z<'a>(input: &[u8], output: &'a mut PathBuffer) -> &'a ZStr { } type IsSeparatorFunc = fn(char: u8) -> bool; -// Rust cannot express "fn(T) -> bool" as a value, so the generic-`T` -// callers dispatch via Platform methods instead of fn pointers. -type LastSeparatorFunction = fn(slice: &[u8]) -> Option; #[inline(always)] fn is_dotdot_with_type(slice: &[T]) -> bool { @@ -1240,14 +1237,6 @@ impl Platform { } } - pub const fn get_last_separator_func(self) -> LastSeparatorFunction { - match self { - Platform::Loose => last_index_of_separator_loose, - Platform::Nt | Platform::Windows => last_index_of_separator_windows, - Platform::Posix => last_index_of_separator_posix, - } - } - #[inline(always)] pub fn is_separator(self, char: u8) -> bool { self.is_separator_t::(char) diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index b371d53bb519..635cebb2e8b0 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -35,7 +35,3 @@ bun_resolver.workspace = true bun_sys.workspace = true bun_url.workspace = true bun_wyhash.workspace = true - -[dev-dependencies] -# Test harness (Test::make / Test::make_routes) needs the AST stores. -bun_js_parser.workspace = true diff --git a/src/router/lib.rs b/src/router/lib.rs index 797365348028..94f50ffaffb7 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -1967,278 +1967,13 @@ pub mod pattern { pub use pattern::Pattern; // ────────────────────────────────────────────────────────────────────────── -// Tests + test helpers +// Tests // ────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; - struct MockRequestContextType { - controlled: bool, - url: URLPath, - match_file_path_buf: [u8; 1024], - - handle_request_called: bool, - redirect_called: bool, - matched_route: Option>, - has_called_done: bool, - } - - impl Default for MockRequestContextType { - fn default() -> Self { - Self { - controlled: false, - url: URLPath::default(), - match_file_path_buf: [0; 1024], - handle_request_called: false, - redirect_called: false, - matched_route: None, - has_called_done: false, - } - } - } - - impl MockRequestContextType { - fn handle_request(&mut self) -> crate::Result<()> { - self.handle_request_called = true; - Ok(()) - } - - fn handle_redirect(&mut self, _: &[u8]) -> crate::Result<()> { - self.redirect_called = true; - Ok(()) - } - } - - struct JavaScriptHandler; - impl JavaScriptHandler { - fn enqueue( - _: &mut MockRequestContextType, - _: &mut MockServer, - _: &mut route_param::List<'_>, - ) -> crate::Result<()> { - Ok(()) - } - } - - pub struct MockServer { - watchloop_handle: Option, - watcher: MockWatcher, - } - - impl Default for MockServer { - fn default() -> Self { - Self { - watchloop_handle: None, - watcher: MockWatcher::default(), - } - } - } - - #[derive(Default)] - pub struct MockWatcher { - watchloop_handle: Option, - } - impl MockWatcher { - pub fn start(&mut self) -> crate::Result<()> { - Ok(()) - } - } - - fn make_test(cwd_path: &[u8], data: &[(&str, &str)]) -> crate::Result<()> { - Output::init_test(); - debug_assert!(cwd_path.len() > 1 && cwd_path != b"/" && !cwd_path.ends_with(b"bun")); - let bun_tests_dir = bun_sys::Dir::cwd() - .make_open_path(b"bun-test-scratch", bun_sys::OpenDirOptions::default())?; - let _ = bun_tests_dir.delete_tree(cwd_path); - - let cwd = bun_tests_dir.make_open_path(cwd_path, bun_sys::OpenDirOptions::default())?; - bun_sys::fchdir(cwd.fd())?; - - for (name, value) in data { - let name_b = name.as_bytes(); - // NOTE: paths without a '/' have no parent dir to create; - // rposition on '/' finds the parent (test fixture paths are - // always forward-slash). - if let Some(slash) = name_b.iter().rposition(|&c| c == b'/') { - if slash > 0 { - cwd.make_path(&name_b[..slash])?; - } - } - let file = bun_sys::File::create(cwd.fd(), name_b, true)?; - file.write_all(value.as_bytes())?; - let _ = file.close(); - } - Ok(()) - } - - /// Newtype so the orphan rule lets us `impl ResolverLike` for a - /// foreign-crate type. - struct TestResolver<'a>(bun_resolver::Resolver<'a>); - - impl<'a> ResolverLike for TestResolver<'a> { - fn fs(&self) -> &'static FileSystem { - // SAFETY: process-static singleton (see `FileSystem::instance`). - unsafe { &*self.0.fs() } - } - fn fs_impl(&self) -> *mut Fs::Implementation { - // SAFETY: `&fs.fs` — the `Implementation` field of the singleton. - unsafe { core::ptr::from_mut(&mut (*self.0.fs()).fs) } - } - fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { - self.0.read_dir_info_ignore_error(path) - } - } - - pub struct Test; - - impl Test { - pub fn make_routes( - test_name: &'static str, - data: &[(&str, &str)], - ) -> crate::Result { - Output::init_test(); - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.init(null); - let _ = bun_resolver::fs::FileSystem::init(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - // var pages_parts = [_]string{ top_level_dir, "pages" }; - // const pages_dir = try Fs.FileSystem.instance.absAlloc(default_allocator, &pages_parts); - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))?; - - // const router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - // NOTE: `errdefer logger.print(Output.errorWriter())` — Rust has - // no errdefer; the test harness panics on error anyway, but the guard - // still flushes diagnostics on early-return for parity. - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - // const opts = Options.BundleOptions{ .target = .browser, ... }; - // NOTE: the resolver-side `BundleOptions` subset omits - // `loaders`/`define`/`log`/`routes`/`entry_points`/`out_extensions`/ - // `transform_options` — none are read by `Resolver::init1` or the - // dir-info walk, so `Default` + `target` is the faithful projection. - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - // var resolver = Resolver.init1(default_allocator, &logger, &FileSystem.instance, opts); - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| crate::Error::Sys(bun_errno::SystemErrno::ENOENT))?; - - // return RouteLoader.loadAll(..., opts.routes, &logger, Resolver, &resolver, root_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - let routes = RouteLoader::load_all( - router.config.clone(), - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &mut resolver, - &root_dir, - top_level_dir, - ); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(routes) - } - - pub fn make( - test_name: &'static str, - data: &[(&str, &str)], - ) -> crate::Result> { - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.initWithForce(null, true); - let _ = bun_resolver::fs::FileSystem::init_with_force::(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))?; - - // var router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let mut router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| crate::Error::Sys(bun_errno::SystemErrno::ENOENT))?; - - // try router.loadRoutes(&logger, root_dir, Resolver, &resolver, top_level_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - router.load_routes( - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &root_dir, - &mut resolver, - top_level_dir, - )?; - let entry_points = router.get_entry_points(); - - assert_eq!(data.len(), entry_points.len()); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(router) - } - } - #[test] fn pattern_match() { type Entry = Param<'static>; diff --git a/src/runtime/api/HashObject.rs b/src/runtime/api/HashObject.rs index 1f91c1ec0b29..9765924ec02c 100644 --- a/src/runtime/api/HashObject.rs +++ b/src/runtime/api/HashObject.rs @@ -260,20 +260,7 @@ fn hash_wrap(global: &JSGlobalObject, frame: &CallFrame) -> Js input = blob.shared_view(); } else { match arg.js_type_loose() { - jsc::JSType::ArrayBuffer - | jsc::JSType::Int8Array - | jsc::JSType::Uint8Array - | jsc::JSType::Uint8ClampedArray - | jsc::JSType::Int16Array - | jsc::JSType::Uint16Array - | jsc::JSType::Int32Array - | jsc::JSType::Uint32Array - | jsc::JSType::Float16Array - | jsc::JSType::Float32Array - | jsc::JSType::Float64Array - | jsc::JSType::BigInt64Array - | jsc::JSType::BigUint64Array - | jsc::JSType::DataView => { + t if t.is_array_buffer_like() => { array_buffer = match arg.as_array_buffer(global) { Some(ab) => ab, None => { diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index 1b5da97def4c..ed2945934982 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -1,10 +1,9 @@ -use bun_ast::{E, Expr, expr::Data as ExprData}; +use bun_ast::ToJSError; use bun_collections::HashMap; -use bun_collections::VecExt; use bun_core::StackCheck; -use bun_core::{OwnedString, String as BunString, ZigString}; +use bun_core::{OwnedString, String as BunString}; use bun_js_parser::lexer; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, StringJsc, wtf}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::json5; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { @@ -76,7 +75,7 @@ pub fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { } }; - expr_to_js(root, global) + expr_to_js(&root, global) }, ) } @@ -427,55 +426,15 @@ impl Stringifier { } } -fn estring_to_js(str: &E::EString, global: &JSGlobalObject) -> JsResult { - // NOTE: the JSON5 parser never builds ropes, so the simple slice → JS - // path is sufficient. - if str.is_utf16 { - let zig = ZigString::init_utf16(str.slice16()); - let bun_s = BunString::init(zig); - bun_s.to_js(global) - } else { - jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) - } -} - -fn expr_to_js(expr: Expr, global: &JSGlobalObject) -> JsResult { - expr_to_js_with_check(expr, global, StackCheck::init()) -} - -fn expr_to_js_with_check( - expr: Expr, - global: &JSGlobalObject, - stack_check: StackCheck, -) -> JsResult { - if !stack_check.is_safe_to_recurse() { - return Err(global.throw_stack_overflow()); - } - match expr.data { - ExprData::ENull(_) => Ok(JSValue::NULL), - ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), - ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), - ExprData::EString(str) => estring_to_js(str.get(), global), - ExprData::EArray(arr) => { - JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { - expr_to_js_with_check(*item, global, stack_check) - }) - } - ExprData::EObject(obj) => { - let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); - for prop in obj.properties.slice() { - let key_expr = prop.key.expect("infallible: prop has key"); - let value = expr_to_js_with_check( - prop.value.expect("infallible: prop has value"), - global, - stack_check, - )?; - let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; - let key_str = OwnedString::new(key_js.to_bun_string(global)?); - js_obj.put_may_be_index(global, &key_str, value)?; - } - Ok(js_obj) - } - _ => Ok(JSValue::UNDEFINED), - } +fn expr_to_js(expr: &bun_ast::Expr, global: &JSGlobalObject) -> JsResult { + bun_js_parser_jsc::expr_to_js(expr, global).map_err(|err| match err { + ToJSError::OutOfMemory => JsError::OutOfMemory, + ToJSError::JSTerminated => JsError::Terminated, + // The exception (e.g. stack overflow) is already pending on the global. + ToJSError::JSError => JsError::Thrown, + // Unreachable: the JSON5 parser only emits literal nodes. + ToJSError::CannotConvertArgumentTypeToJS + | ToJSError::CannotConvertIdentifierToJS + | ToJSError::MacroError => global.throw(format_args!("Cannot convert JSON5 value to JS")), + }) } diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index 22855563fbd5..1302f263abab 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, @@ -137,21 +166,8 @@ pub(crate) fn set_max_markdown_block_bytes_for_testing( pub fn render_to_ansi(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, theme_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let mut theme = md::AnsiTheme { colors: true, @@ -208,21 +224,8 @@ pub(crate) fn render_to_html( ) -> JsResult { let [input_value, opts_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let options = parse_options(global_this, opts_value)?; @@ -311,21 +314,8 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, callbacks_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -344,9 +334,8 @@ pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsR })?; // Run parser with the JS callback renderer - if let Err(err) = md::render_with_renderer(input, options, js_renderer.renderer()) { - return Err(parser_err_to_js(global_this, err, input.len())); - } + md::render_with_renderer(input, options, js_renderer.renderer()) + .map_err(|err| parser_err_to_js(global_this, err, input.len()))?; // Return accumulated result let result = js_renderer.get_result(); @@ -407,21 +396,8 @@ fn render_ast( ) -> JsResult { let [input_value, components_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -444,9 +420,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/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 4e2646dc4e57..87e9501c8224 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -449,6 +449,67 @@ pub struct DevServer { bun_event_loop::impl_timer_owner!(DevServer; from_timer_ptr => memory_visualizer_timer); +/// Exhaustiveness check: destructures a `&DevServer` without `..`, so adding, +/// removing, or renaming a field fails to compile at every invocation +/// (`Drop`, `memory_cost_detailed`), forcing the per-field logic there to be +/// reviewed. All bindings are `_` so nothing is moved or borrowed past the +/// statement. +macro_rules! destructure_dev_server_fields { + ($e:expr) => { + let crate::bake::dev_server::DevServer { + magic: _, + root: _, + inspector_server_id: _, + configuration_hash_key: _, + vm: _, + server: _, + router: _, + route_bundles: _, + graph_safety_lock: _, + client_graph: _, + server_graph: _, + barrel_files_with_deferrals: _, + barrel_needed_exports: _, + incremental_result: _, + route_lookup: _, + html_router: _, + assets: _, + source_maps: _, + bundling_failures: _, + frontend_only: _, + has_tailwind_plugin_hack: _, + server_fetch_function_callback: _, + server_register_update_callback: _, + bun_watcher: _, + directory_watchers: _, + watcher_atomics: _, + testing_batch_events: _, + generation: _, + bundles_since_last_error: _, + framework: _, + bundler_framework_views: _, + bundler_options: _, + server_transpiler: _, + client_transpiler: _, + ssr_transpiler: _, + log: _, + plugin_state: _, + current_bundle: _, + next_bundle: _, + deferred_request_pool: _, + active_websocket_connections: _, + dump_dir: _, + emit_incremental_visualizer_events: _, + emit_memory_visualizer_events: _, + memory_visualizer_timer: _, + has_pre_crash_handler: _, + assume_perfect_incremental_bundling: _, + broadcast_console_log_from_browser_to_server: _, + } = $e; + }; +} +pub(crate) use destructure_dev_server_fields; + pub(super) const INTERNAL_PREFIX: &str = "/_bun"; /// Assets which are routed to the `Assets` storage. pub(super) const ASSET_PREFIX: &str = const_format::concatcp!(INTERNAL_PREFIX, "/asset"); @@ -1070,62 +1131,11 @@ impl Drop for DevServer { // practice, so a plain fetch_add is fine. DEV_SERVER_DEINIT_COUNT_FOR_TESTING.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - // Exhaustiveness check: destructuring without `..` fails to compile when a field is added, - // removed, or renamed, forcing this Drop to be reviewed. All bindings - // are `_` so nothing is moved; cleanup not done explicitly below - // happens via the implicit field drops after this body returns. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = &*self; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a field is added, removed, or renamed, forcing this + // Drop to be reviewed. Cleanup not done explicitly below happens via + // the implicit field drops after this body returns. + destructure_dev_server_fields!(&*self); // WebSockets should be deinitialized before other parts. // `websocket.close()` synchronously dispatches `HmrSocket.onClose`, diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index 005a9750f06b..41a48197d624 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -24,11 +24,11 @@ use bun_paths::{self as paths, PathBuffer}; pub(crate) use crate::api::js_bundler::Plugin; use crate::api::js_bundler::js_bundler::PluginJscExt as _; -// Note: parent `mod.rs` already declares `dev_server` / `framework_router` -// as sibling modules of this file; pull them in instead of re-declaring (which -// would duplicate the module tree and fail on `framework_router` having no +// Note: parent `mod.rs` already declares `framework_router` as a sibling +// module of this file; pull it in instead of re-declaring (which would +// duplicate the module tree and fail on `framework_router` having no // matching filename). -use super::{dev_server, framework_router}; +use super::framework_router; // Note: `pub use dev_server as DevServer` / `framework_router as // FrameworkRouter` are already provided by the parent `mod.rs` (lines 349/369); @@ -1142,38 +1142,6 @@ impl Framework { ) } - pub fn init_transpiler<'a>( - &mut self, - arena: &'a Arena, - log: &mut bun_ast::Log, - mode: Mode, - renderer: Graph, - out: &mut core::mem::MaybeUninit>, - bundler_options: &BuildConfigSubset, - ) -> crate::Result<()> { - let source_map: bun_bundler::options::SourceMapOption = match mode { - // Source maps must always be external, as DevServer special cases - // the linking and part of the generation of these. It also relies - // on source maps always being enabled. - Mode::Development => bun_bundler::options::SourceMapOption::External, - // TODO: follow user configuration - _ => bun_bundler::options::SourceMapOption::None, - }; - - self.init_transpiler_with_options( - arena, - log, - mode, - renderer, - out, - bundler_options, - source_map, - None, - None, - None, - ) - } - pub fn init_transpiler_with_options<'a>( &mut self, arena: &'a Arena, @@ -1187,149 +1155,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 30c0265a71e1..82cee7802b6a 100644 --- a/src/runtime/bake/dev_server/memory_cost.rs +++ b/src/runtime/bake/dev_server/memory_cost.rs @@ -33,62 +33,10 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { let mut source_maps: usize = 0; let mut assets: usize = 0; - // Exhaustiveness check: - // destructuring without `..` fails to compile when a DevServer field is - // added, removed, or renamed, forcing the accounting below to be updated. - // All bindings are `_` so nothing is moved or borrowed past this block. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = dev; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a DevServer field is added, removed, or renamed, forcing + // the accounting below to be updated. + crate::bake::dev_server_body::destructure_dev_server_fields!(dev); // does not contain pointers // .assume_perfect_incremental_bundling diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index e2f7649aa7fa..a045aa0f6694 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -206,12 +206,11 @@ impl Framework { ) } - /// Sets up a per-graph - /// `Transpiler` in place. The full body lives in - /// `bake_body::Framework::init_transpiler_with_options`; this keystone - /// version operates on the keystone `BuildConfigSubset` (which omits - /// `conditions`/`env`/`define`/`drop` until the schema types are - /// const-constructible — those paths default). + /// Sets up a per-graph `Transpiler` in place via `init_transpiler_impl`, + /// with the DevServer defaults: source maps follow `mode` and the three + /// minify overrides always default to `mode != Development` regardless of + /// `BuildConfigSubset`. User-supplied source-map/minify flags are only + /// honored by `init_transpiler_with_options` (bake_body). /// Returns the arena slot for the `bake_types::Framework` projection; caller must `drop_in_place` it. pub fn init_transpiler<'a>( &mut self, @@ -222,79 +221,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. @@ -304,65 +231,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 @@ -481,6 +366,181 @@ impl Framework { } } +/// Caller-specific inputs to `init_transpiler_impl`: the two `Framework` +/// representations contribute their feature flags and `bake_types::Framework` +/// projection here. A `None` minify override defaults to +/// `mode != Development`. +pub(crate) struct InitTranspilerOptions { + pub source_map: bun_bundler::options::SourceMapOption, + pub minify_whitespace: Option, + pub minify_syntax: Option, + pub minify_identifiers: Option, + pub has_react_fast_refresh: bool, + pub has_server_components: bool, + pub framework_view: bun_bundler::bake_types::Framework, +} + +/// Shared body of `Framework::init_transpiler` (keystone, DevServer) and +/// `bake_body::Framework::init_transpiler_with_options` (production): wires +/// the per-graph transpiler options (target/conditions/minify/source +/// map/define/drop) that are identical between the two `Framework` +/// representations, which only contribute the `InitTranspilerOptions` here. +/// +/// Returns the arena slot for the projection; the caller must `drop_in_place` +/// it — interior `Box<[u8]>` are not dropped by bumpalo. (The production path +/// deliberately leaks it: bounded per-session, revisit when +/// `bake_types::BuiltInModule` is reshaped to `&'a [u8]`.) +pub(crate) fn init_transpiler_impl<'a>( + arena: &'a bun_alloc::Arena, + log: &mut bun_ast::Log, + mode: Mode, + renderer: Graph, + out: &mut core::mem::MaybeUninit>, + bundler_options: &BuildConfigSubset, + opts: InitTranspilerOptions, +) -> 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 { @@ -561,56 +621,24 @@ impl From for Framework { } } } -impl From for BuildConfigSubset { - fn from(src: bake_body::BuildConfigSubset) -> Self { - // `BuildConfigSubset` mirrors the field-set - // `Framework::init_transpiler` reads (everything except `loader` / - // `source_map`, which only `init_transpiler_with_options` honours). - Self { - ignore_dce_annotations: src.ignore_dce_annotations, - conditions: src.conditions, - drop: src.drop, - env: src.env, - env_prefix: src.env_prefix, - define: src.define, - minify_syntax: src.minify_syntax, - minify_identifiers: src.minify_identifiers, - minify_whitespace: src.minify_whitespace, - } - } -} impl From for SplitBundlerOptions { fn from(src: bake_body::SplitBundlerOptions) -> Self { Self { // `bake_body::Plugin` and keystone `jsc::Plugin` both alias // `crate::api::js_bundler::Plugin` — same nominal type, no cast. plugin: src.plugin, - client: src.client.into(), - server: src.server.into(), - ssr: src.ssr.into(), + client: src.client, + server: src.server, + ssr: src.ssr, } } } /// `bake.SplitBundlerOptions.BuildConfigSubset`. Full body (with `from_js`) -/// lives in `bake_body.rs`; this keystone mirror carries every field that -/// `Framework::init_transpiler` reads so DevServer's -/// per-graph transpilers see bunfig `[serve.static]` define/env/conditions. -#[derive(Default)] -pub struct BuildConfigSubset { - pub ignore_dce_annotations: Option, - pub conditions: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub drop: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub env: bun_options_types::schema::api::DotEnvBehavior, - pub env_prefix: Option<&'static [u8]>, - pub define: bun_options_types::schema::api::StringMap, - pub minify_syntax: Option, - pub minify_identifiers: Option, - pub minify_whitespace: Option, - // `loader`/`source_map` intentionally omitted — only - // `init_transpiler_with_options` (bake_body) honours those, and DevServer - // never calls that path. -} +/// lives in `bake_body.rs`; DevServer's `init_transpiler` reads everything +/// except `loader`/`source_map`, which only `init_transpiler_with_options` +/// honours. +pub use bake_body::BuildConfigSubset; /// `bake.HmrRuntime` — embedded HMR runtime code + precomputed line count. /// Canonical definition; `bake_body::HmrRuntime` re-exports this diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index facc9291a5d6..e11241aa7e65 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -35,7 +35,6 @@ use bun_resolver as resolver; use crate::cli::command::{Context, HotReload}; use bun_options_types::context::MacroOptions; -use bun_options_types::offline_mode::OfflineMode; use bun_bundler::options::OutputKind; @@ -135,35 +134,7 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { vm.argv.clone_from(&ctx.passthrough); vm.arena = NonNull::new(&raw mut arena); // vm.allocator = arena.arena() — dropped per §Allocators - // `BundleOptions.install` is `Option>`, so no - // lifetime-extension cast is needed. - let install_ptr = ctx.install.as_deref().map(NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - b.resolver.opts.prefer_offline_install = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Offline; - // Note: `bun_resolver::options::BundleOptions` has no - // `prefer_latest_install` field; compute the value once - // and assign only to `b.options` (which does carry it). The resolver - // never reads it. - let prefer_latest = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Latest; - b.options.global_cache = b.resolver.opts.global_cache; - b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; - b.options.prefer_latest_install = prefer_latest; - // SAFETY: `b.env` is the Transpiler-owned `*mut Loader`; store it - // as `NonNull` (not `&Loader`) because `configure_defines()` below - // reborrows the same allocation as `&mut Loader` via `run_env_loader()`, - // which would alias a live `&Loader` here. The Loader outlives the - // resolver (process-lifetime singleton or VM-owned). - b.resolver.env_loader = NonNull::new(b.env); + crate::cli::run_command::wire_install_options(b, ctx); b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; b.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; diff --git a/src/runtime/cli/bunx_command.rs b/src/runtime/cli/bunx_command.rs index 9be94531906b..28fd208e6652 100644 --- a/src/runtime/cli/bunx_command.rs +++ b/src/runtime/cli/bunx_command.rs @@ -213,6 +213,19 @@ pub(crate) enum GetBinNameError { NeedToInstall, } +/// Inputs shared by both post-install cached-bin probes; invariant across the +/// initial-bin-name and package-json-bin-name attempts. +struct CachedBinProbe<'a> { + bunx_cache_dir: &'a [u8], + ignore_cwd: &'a [u8], + top_level_dir: &'a [u8], + #[cfg(unix)] + uid: libc::uid_t, + #[cfg(not(unix))] + uid: u32, + dirname_store: &'static bun_resolver::fs::DirnameStore, +} + impl BunxCommand { /// Adds `create-` to the string, but also handles scoped packages correctly. /// Always clones the string in the process. @@ -584,6 +597,77 @@ impl BunxCommand { true } + /// Post-install cache probe: builds + /// `/node_modules/.bin/`, resolves it + /// via `bun_which::which`, and execs it via `Run::run_binary` (noreturn) + /// once it passes the `is_trusted_cached_binary` TOCTOU check. Returns + /// `Ok(())` only on a miss: the binary is absent or untrusted. + fn try_run_cached_bin( + ctx: &mut ContextData, + path_buf: &mut PathBuffer, + absolute_in_cache_dir_buf: &mut PathBuffer, + probe: &CachedBinProbe, + bin_name: &[u8], + env_loader: &mut bun_dotenv::Loader<'static>, + passthrough: &[Box<[u8]>], + ) -> crate::Result<()> { + let buf_total = absolute_in_cache_dir_buf.len(); + let absolute_in_cache_dir: &[u8] = { + let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; + write!( + cursor, + "{cache}{sep}node_modules{sep}.bin{sep}{bin}{exe}", + cache = BStr::new(probe.bunx_cache_dir), + sep = bun_paths::SEP as char, + bin = BStr::new(bin_name), + exe = EXE_SUFFIX, + ) + .expect("unreachable"); + let written = buf_total - cursor.len(); + // SAFETY: `written` bytes initialized above + unsafe { core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) } + }; + + // Similar to "npx": try the bin in the global cache. Do not try $PATH + // because we already checked it above if we should. + if let Some(destination) = bun_which::which( + path_buf, + probe.bunx_cache_dir, + if !probe.ignore_cwd.is_empty() { + b"".as_slice() + } else { + probe.top_level_dir + }, + absolute_in_cache_dir, + ) { + let out: &[u8] = destination.as_bytes(); + // The install we just ran should have created this symlink as the + // current user, but the cache lives in a world-writable temp dir; an + // attacker can race the install and plant a uid-mismatched entry. + // Bail out to the generic error rather than execute it. + if Self::is_trusted_cached_binary(destination, probe.uid) { + let stored = probe.dirname_store.append_slice(out)?; + Run::run_binary( + ctx, + stored, + destination, + probe.top_level_dir, + env_loader, + passthrough, + None, + )?; + // run_binary is noreturn + } else { + bun_output::scoped_log!( + bunx, + "refusing untrusted cached binary: {}", + BStr::new(out) + ); + } + } + Ok(()) + } + fn exit_with_usage() -> ! { crate::cli::command::tag_print_help(Command::Tag::BunxCommand, false); Global::exit(1); @@ -1369,61 +1453,23 @@ impl BunxCommand { _ => {} } - absolute_in_cache_dir = { - let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; - write!( - cursor, - "{cache}{sep}node_modules{sep}.bin{sep}{bin}{exe}", - cache = BStr::new(bunx_cache_dir), - sep = bun_paths::SEP as char, - bin = BStr::new(initial_bin_name), - exe = EXE_SUFFIX, - ) - .expect("unreachable"); - let written = buf_total - cursor.len(); - // SAFETY: `written` bytes initialized above - unsafe { core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) } + let cached_bin_probe = CachedBinProbe { + bunx_cache_dir, + ignore_cwd: &ignore_cwd, + top_level_dir, + uid, + dirname_store: fs.dirname_store, }; - // Similar to "npx": - // - // 1. Try the bin in the global cache - // Do not try $PATH because we already checked it above if we should - if let Some(destination) = bun_which::which( + Self::try_run_cached_bin( + ctx, &mut path_buf, - bunx_cache_dir, - if !ignore_cwd.is_empty() { - b"".as_slice() - } else { - top_level_dir - }, - absolute_in_cache_dir, - ) { - let out: &[u8] = destination.as_bytes(); - // The install we just ran should have created this symlink as the - // current user, but the cache lives in a world-writable temp dir; an - // attacker can race the install and plant a uid-mismatched entry. - // Bail out to the generic error rather than execute it. - if Self::is_trusted_cached_binary(destination, uid) { - let stored = fs.dirname_store.append_slice(out)?; - Run::run_binary( - ctx, - stored, - destination, - top_level_dir, - env_loader, - passthrough, - None, - )?; - // run_binary is noreturn - } else { - bun_output::scoped_log!( - bunx, - "refusing untrusted cached binary: {}", - BStr::new(out) - ); - } - } + &mut absolute_in_cache_dir_buf, + &cached_bin_probe, + initial_bin_name, + env_loader, + passthrough, + )?; // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use // BUT: Skip this if --package was used, as the user explicitly specified the binary name @@ -1435,55 +1481,15 @@ impl BunxCommand { false, ) { if !strings::eql_long(&package_name_for_bin, initial_bin_name, true) { - absolute_in_cache_dir = { - let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; - write!( - cursor, - "{}/node_modules/.bin/{}{}", - BStr::new(bunx_cache_dir), - BStr::new(&package_name_for_bin), - EXE_SUFFIX, - ) - .expect("unreachable"); - let written = buf_total - cursor.len(); - // SAFETY: `written` bytes initialized above - unsafe { - core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) - } - }; - - if let Some(destination) = bun_which::which( + Self::try_run_cached_bin( + ctx, &mut path_buf, - bunx_cache_dir, - if !ignore_cwd.is_empty() { - b"".as_slice() - } else { - top_level_dir - }, - absolute_in_cache_dir, - ) { - let out: &[u8] = destination.as_bytes(); - // Same TOCTOU hardening as the post-install probe above. - if Self::is_trusted_cached_binary(destination, uid) { - let stored = fs.dirname_store.append_slice(out)?; - Run::run_binary( - ctx, - stored, - destination, - top_level_dir, - env_loader, - passthrough, - None, - )?; - // run_binary is noreturn - } else { - bun_output::scoped_log!( - bunx, - "refusing untrusted cached binary: {}", - BStr::new(out) - ); - } - } + &mut absolute_in_cache_dir_buf, + &cached_bin_probe, + &package_name_for_bin, + env_loader, + passthrough, + )?; } } } diff --git a/src/runtime/cli/create_command.rs b/src/runtime/cli/create_command.rs index 572840ca716b..420668e69aa0 100644 --- a/src/runtime/cli/create_command.rs +++ b/src/runtime/cli/create_command.rs @@ -1690,64 +1690,40 @@ impl CreateCommand { } if !bun_paths::is_absolute(positional) { - 'outer: { - if let Some(home_dir) = env_loader.map.get(b"BUN_CREATE_DIR") { - let parts = [home_dir, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; - } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) - .unwrap_or(false) - { - example_tag = ExampleTag::LocalFolder; - break 'brk &home_dir_buf[..len]; - } - } - } - - 'outer: { - let parts = [filesystem.top_level_dir, BUN_CREATE_DIR, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; + // Returns the path length if `parts` joins to an existing template directory. + let probe_template_dir = |buf: &mut PathBuffer, parts: &[&[u8]]| -> Option { + let len = filesystem.abs_buf(parts, buf).len(); + buf[len] = 0; + // SAFETY: buf[len] == 0 written above + let outdir_path = bun_core::ZStr::from_buf(&buf[..], len); + if bun_paths::resolve_path::has_any_illegal_chars(outdir_path.as_bytes()) { + return None; } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) + bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path) .unwrap_or(false) - { + .then_some(len) + }; + + // Empty parts are skipped by the path join, so the two-part + // BUN_CREATE_DIR candidate pads with `b""`. + let candidates: [Option<[&[u8]; 3]>; 3] = [ + env_loader + .map + .get(b"BUN_CREATE_DIR") + .map(|home_dir| [home_dir, positional, b"".as_slice()]), + Some([filesystem.top_level_dir, BUN_CREATE_DIR, positional]), + env_loader + .map + .get(b"HOME") + .map(|home_dir| [home_dir, BUN_CREATE_DIR, positional]), + ]; + for parts in candidates.into_iter().flatten() { + if let Some(len) = probe_template_dir(&mut *home_dir_buf, &parts) { example_tag = ExampleTag::LocalFolder; break 'brk &home_dir_buf[..len]; } } - 'outer: { - if let Some(home_dir) = env_loader.map.get(b"HOME") { - let parts = [home_dir, BUN_CREATE_DIR, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; - } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) - .unwrap_or(false) - { - example_tag = ExampleTag::LocalFolder; - break 'brk &home_dir_buf[..len]; - } - } - } - if bun_paths::is_absolute(positional) { example_tag = ExampleTag::LocalFolder; break 'brk positional; diff --git a/src/runtime/cli/filter_run.rs b/src/runtime/cli/filter_run.rs index 6746d8769c87..210bdf77ed70 100644 --- a/src/runtime/cli/filter_run.rs +++ b/src/runtime/cli/filter_run.rs @@ -1,6 +1,6 @@ use core::ffi::{c_char, c_void}; use std::io::Write as _; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; use std::time::Instant; #[cfg(unix)] @@ -9,6 +9,9 @@ use crate::api::bun::process::{self as spawn, Process, Rusage, SpawnOptions, Sta use crate::cli::Command; use crate::cli::filter_arg as FilterArg; use crate::cli::run_command::RunCommand; +use crate::cli::run_processes_shared::{ + AbortHandler, SHOULD_ABORT, aggregate_exit_code, buffered_stdio, watch_or_reap, +}; use bun_collections::StringHashMap; use bun_core::{Global, Output}; use bun_core::{ZStr, strings}; @@ -182,16 +185,7 @@ impl<'a> ProcessHandle<'a> { ) }); - match process.watch_or_reap() { - Ok(_) => {} - Err(err) => { - if !process.has_exited() { - // SAFETY: all-zero is a valid Rusage (POD C struct) - let rusage = bun_core::ffi::zeroed::(); - process.on_exit(Status::Err(err), &rusage); - } - } - } + watch_or_reap(process); Ok(()) } @@ -587,87 +581,11 @@ impl<'a> State<'a> { if self.aborted { let _ = self.redraw(true); } - for handle in self.handles.iter() { - if let Some(proc) = &handle.process { - match &proc.status { - Status::Exited(exited) => { - if exited.code != 0 { - return exited.code; - } - } - Status::Signaled(signal) => { - return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); - } - _ => return 1, - } - } - } - 0 - } -} - -struct AbortHandler; - -static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); -// Atomic because it is set from a signal handler. - -impl AbortHandler { - #[cfg(unix)] - extern "C" fn posix_signal_handler( - sig: i32, - info: *const bun_sys::posix::siginfo_t, - _: *const c_void, - ) { - let _ = sig; - let _ = info; - SHOULD_ABORT.store(true, Ordering::SeqCst); - } - - #[cfg(windows)] - extern "system" fn windows_ctrl_handler( - dw_ctrl_type: bun_sys::windows::DWORD, - ) -> bun_sys::windows::BOOL { - if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { - SHOULD_ABORT.store(true, Ordering::SeqCst); - return bun_sys::windows::TRUE; - } - bun_sys::windows::FALSE - } - - pub(crate) fn install() { - #[cfg(unix)] - { - // SAFETY: libc::sigaction is #[repr(C)] POD; all-zero is a valid value (fields overwritten below). - let mut act: libc::sigaction = bun_core::ffi::zeroed(); - act.sa_sigaction = Self::posix_signal_handler as *const () as usize; - act.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND; - // SAFETY: sa_mask is a valid out-pointer; act is on the stack. - unsafe { - libc::sigemptyset(&raw mut act.sa_mask); - libc::sigaction(libc::SIGINT, &raw const act, core::ptr::null_mut()); - } - } - #[cfg(not(unix))] - { - let res = bun_sys::c::SetConsoleCtrlHandler( - Some(Self::windows_ctrl_handler), - bun_sys::windows::TRUE, - ); - if res == 0 { - if bun_core::env::IS_DEBUG { - bun_core::warn!("Failed to set abort handler\n"); - } - } - } - } - - pub(crate) fn uninstall() { - // only necessary on Windows, as on posix we pass the SA_RESETHAND flag - #[cfg(windows)] - { - // restores default Ctrl+C behavior - let _ = bun_sys::c::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); - } + aggregate_exit_code( + self.handles + .iter() + .map(|h| h.process.as_ref().map(|p| &p.status)), + ) } } @@ -943,18 +861,8 @@ pub(crate) fn run_scripts_with_filter( process: None, options: SpawnOptions { stdin: spawn::Stdio::Ignore, - #[cfg(unix)] - stdout: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stdout: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), - #[cfg(unix)] - stderr: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stderr: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), + stdout: buffered_stdio(), + stderr: buffered_stdio(), cwd: bun_paths::resolve_path::dirname::( &script.package_json_path, ) diff --git a/src/runtime/cli/install_command.rs b/src/runtime/cli/install_command.rs index 523eed71e266..0967cb445d38 100644 --- a/src/runtime/cli/install_command.rs +++ b/src/runtime/cli/install_command.rs @@ -1,5 +1,4 @@ use crate::Error; -use bun_bundler::bundle_v2::{DependenciesScanner, DependenciesScannerResult}; use bun_core::{Global, Output}; use bun_install::package_manager_real::{ CommandLineArguments, PackageManager, ROOT_PACKAGE_JSON_PATH, Subcommand, install_with_manager, @@ -7,7 +6,7 @@ use bun_install::package_manager_real::{ }; use crate::Cli; -use crate::build_command::BuildCommand; +use crate::cli::pm_update_package_json::analyze_dependencies_and_install; use crate::command::ContextData; pub(crate) struct InstallCommand; @@ -54,97 +53,8 @@ impl InstallCommand { fn install(ctx: &mut ContextData) -> Result<(), Error> { let mut cli = CommandLineArguments::parse(Subcommand::Install)?; - // The way this works: - // 1. Run the bundler on source files - // 2. Rewrite positional arguments to act identically to the developer - // typing in the dependency names - // 3. Run the install command if cli.analyze { - // `ctx` is stored as a raw `*mut ContextData`; the `on_fetch` callback - // re-enters the install path while `BuildCommand::exec` still holds the - // global `Context`, so a `&mut` here would be aliased UB. - struct Analyzer { - ctx: *mut ContextData, - cli: *mut CommandLineArguments, - } - impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer { - fn on_analyze( - &mut self, - result: &mut DependenciesScannerResult<'_, '_>, - ) -> Result<(), bun_bundler::Error> { - let this = self; - // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed - // - // Process-lifetime storage for the rewritten positionals — - // `Global::exit(0)` follows immediately. - // `OnceLock` (not leaking) per PORTING.md §Forbidden. - static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); - static POSITIONALS: std::sync::OnceLock> = - std::sync::OnceLock::new(); - - let owned = OWNED_KEYS.get_or_init(|| { - result - .dependencies - .keys() - .iter() - .map(|k| Box::<[u8]>::from(&**k)) - .collect() - }); - let positionals = POSITIONALS.get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); - v.push(b"install"); - for k in owned { - v.push(&**k); - } - v - }); - - // SAFETY: `this.cli` / `this.ctx` were set from live stack - // locals in `install()` whose scope encloses the entire - // `BuildCommand::exec` call (and hence this callback). The - // bundler does not touch the global `ContextData` between - // dependency-scan completion and `on_fetch` invocation, so - // forming a fresh `&mut` here is exclusive for the duration of - // `install_with_cli`. - let cli = unsafe { &mut *this.cli }; - cli.positionals = positionals.as_slice(); - // SAFETY: see above — same invariant covers `this.ctx`. - let ctx = unsafe { &mut *this.ctx }; - - install_with_cli(ctx, cli.clone()).map_err(bun_bundler::Error::from)?; - - Global::exit(0); - } - } - - // `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. Clone the - // argv slices into an owned buffer (small one-shot list — no perf - // concern). Captured *before* - // raw-ptr aliasing of `cli` below so the access goes through the live - // `&mut cli` borrow. - let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] - .iter() - .map(|s| Box::<[u8]>::from(*s)) - .collect(); - - // Derive raw pointers from the existing `&mut` borrows; all subsequent - // access to `ctx` / `cli` in this branch goes through these. - let ctx_ptr: *mut ContextData = ctx; - let mut analyzer = Analyzer { - ctx: ctx_ptr, - cli: &raw mut cli, - }; - - let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); - - // `Command.get()` resolves to the same `*ContextData` already held in - // `ctx`; reborrow through `ctx_ptr` rather than minting a fresh - // `&'static mut` from the global static (which would alias the - // still-live `ctx` parameter under stacked borrows). - // SAFETY: `ctx_ptr` was just derived from the live `ctx: &mut - // ContextData` parameter; `ctx` is not accessed again in this branch. - BuildCommand::exec(unsafe { &mut *ctx_ptr }, Some(&fetcher))?; - return Ok(()); + return analyze_dependencies_and_install(ctx, &mut cli, b"install", &mut install_with_cli); } install_with_cli(ctx, cli) diff --git a/src/runtime/cli/link_command.rs b/src/runtime/cli/link_command.rs index a52f8c3a3429..1df8ba37d3f2 100644 --- a/src/runtime/cli/link_command.rs +++ b/src/runtime/cli/link_command.rs @@ -10,7 +10,7 @@ use bun_install::Features; use bun_install::bin_real as bin; use bun_install::lockfile_real::{Lockfile, package::Package}; use bun_install::package_manager_real::{ - self as pm, CommandLineArguments, Subcommand, attempt_to_create_package_json, + self as pm, CommandLineArguments, PackageManager, Subcommand, attempt_to_create_package_json, options::LogLevel, package_manager_options, setup_global_dir, update_package_json_and_install_with_manager, }; @@ -25,6 +25,108 @@ impl LinkCommand { } } +/// Shared by `bun link` / `bun unlink`: parse the nearest package.json into an +/// empty lockfile and validate that it declares a valid npm package name. +/// Crashes with a user-facing error otherwise. The package name is re-derived +/// by callers via `lockfile.str(&package.name)`. +pub(crate) fn load_package_for_link( + manager: &mut PackageManager, + verb: &str, +) -> crate::Result<(Lockfile, Package)> { + let mut lockfile = Lockfile::default(); + let mut package = Package::default(); + + let package_json_source = match bun_ast::to_source( + manager.original_package_json_path.as_zstr(), + Default::default(), + ) { + Ok(s) => s, + Err(e) => { + Output::err_generic( + "failed to read \"{}\" for {}: {}", + ( + BStr::new(manager.original_package_json_path.as_bytes()), + verb, + BStr::new(e.name()), + ), + ); + Global::crash(); + } + }; + lockfile.init_empty(); + + let mut resolver: () = (); + // `log_mut()` returns a borrow decoupled from `&self`; disjoint + // storage from `&mut PackageManager` (owned by the CLI `Context`). + let log = manager.log_mut(); + package.parse::<()>( + &mut lockfile, + manager, + log, + &package_json_source, + &mut resolver, + Features::FOLDER, + )?; + let name = lockfile.str(&package.name); + if name.is_empty() { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: package.json missing \"name\" in \"{}\"", + BStr::new(package_json_source.path.text), + ); + } + Global::crash(); + } else if !strings::is_npm_package_name(name) { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: invalid package.json name \"{}\" in \"{}\"", + BStr::new(name), + BStr::new(package_json_source.path.text), + ); + } + Global::crash(); + } + + Ok((lockfile, package)) +} + +/// Shared by `bun link` / `bun unlink`: open the global directory (storing it +/// in `manager.global_dir`) and create+open its `node_modules` folder. +/// Crashes with a user-facing error if `node_modules` cannot be created. +pub(crate) fn open_global_node_modules( + manager: &mut PackageManager, + ctx: &mut command::ContextData, +) -> crate::Result

{ + bin::Linker::ensure_umask(); + let explicit_global_dir: &[u8] = match &ctx.install { + Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), + None => b"", + }; + manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( + explicit_global_dir, + )?)); + + setup_global_dir(manager, &ctx)?; + + match manager + .global_dir + .as_ref() + .unwrap() + .make_open_path(b"node_modules", Default::default()) + { + Ok(d) => Ok(d), + Err(e) => { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: failed to create node_modules in global dir due to error {}", + BStr::new(e.name()), + ); + } + Global::crash(); + } + } +} + fn link(ctx: command::Context) -> crate::Result<()> { let cli = CommandLineArguments::parse(Subcommand::Link)?; let (manager, original_cwd) = match pm::init(&mut *ctx, cli, Subcommand::Link) { @@ -51,98 +153,15 @@ fn link(ctx: command::Context) -> crate::Result<()> { if manager.options.positionals.len() == 1 { // bun link - let mut lockfile = Lockfile::default(); - let mut package = Package::default(); - // Step 1. parse the nearest package.json file - { - let package_json_source = match bun_ast::to_source( - manager.original_package_json_path.as_zstr(), - Default::default(), - ) { - Ok(s) => s, - Err(e) => { - Output::err_generic( - "failed to read \"{s}\" for linking: {s}", - ( - BStr::new(manager.original_package_json_path.as_bytes()), - BStr::new(e.name()), - ), - ); - Global::crash(); - } - }; - lockfile.init_empty(); + let (lockfile, package) = load_package_for_link(manager, "linking")?; - let mut resolver: () = (); - // `log_mut()` returns a borrow decoupled from `&self`; disjoint - // storage from `&mut PackageManager` (owned by the CLI `Context`). - let log = manager.log_mut(); - package.parse::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - &mut resolver, - Features::FOLDER, - )?; - let name = lockfile.str(&package.name); - if name.is_empty() { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: package.json missing \"name\" in \"{}\"", - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } else if !strings::is_npm_package_name(name) { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: invalid package.json name \"{}\" in \"{}\"", - BStr::new(name), - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } - } - - // Reshaped for borrowck — re-derive `name` here so its - // lifetime is tied only to `lockfile.buffers.string_bytes`, decoupled - // from `package_json_source` (dropped above). + // `name` is a slice into `lockfile.buffers.string_bytes`, decoupled + // from the helper-local package.json source. let name = lockfile.str(&package.name); // Step 2. Setup the global directory - let node_modules: Dir = 'brk: { - bin::Linker::ensure_umask(); - let explicit_global_dir: &[u8] = match &ctx.install { - Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), - None => b"", - }; - manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( - explicit_global_dir, - )?)); - - setup_global_dir(manager, &&mut *ctx)?; - - match manager - .global_dir - .as_ref() - .unwrap() - .make_open_path(b"node_modules", Default::default()) - { - Ok(d) => break 'brk d, - Err(e) => { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: failed to create node_modules in global dir due to error {}", - BStr::new(e.name()), - ); - } - Global::crash(); - } - } - }; + let node_modules: Dir = open_global_node_modules(manager, &mut *ctx)?; // Step 3a. symlink to the node_modules folder { diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index d1ab71efe869..d382459ecc08 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -386,6 +386,10 @@ pub use filter_run as FilterRun; #[path = "multi_run.rs"] pub mod multi_run; pub use multi_run as MultiRun; +#[path = "run_processes_shared.rs"] +pub(crate) mod run_processes_shared; +#[path = "workspace_helpers.rs"] +pub(crate) mod workspace_helpers; // ─── crate-local helper for param-table concatenation ──────────────────────── // `bun_clap::parse_param!` is a real proc-macro (const `Param` literal), diff --git a/src/runtime/cli/multi_run.rs b/src/runtime/cli/multi_run.rs index 0647410b1d47..8a1f05210fce 100644 --- a/src/runtime/cli/multi_run.rs +++ b/src/runtime/cli/multi_run.rs @@ -1,6 +1,6 @@ use core::ffi::{c_char, c_void}; use core::ptr; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; use std::time::Instant; use crate::Error; @@ -14,6 +14,9 @@ use bun_paths::{self as path, PathBuffer}; use bun_resolver::package_json::{IncludeDependencies, IncludeScripts}; use crate::Command; +use crate::cli::run_processes_shared::{ + AbortHandler, SHOULD_ABORT, aggregate_exit_code, buffered_stdio, watch_or_reap, +}; use crate::filter_arg as FilterArg; use crate::run_command::RunCommand; @@ -22,8 +25,7 @@ use crate::run_command::RunCommand; #[cfg(unix)] use crate::api::bun::process::SpawnResultExt as _; use crate::api::bun::process::{ - self as spawn, Process, Rusage, SpawnOptions, SpawnProcessResult, Status, - event_loop_handle_to_ctx, + self as spawn, Process, SpawnOptions, SpawnProcessResult, Status, event_loop_handle_to_ctx, }; use bun_dotenv::Loader as DotEnvLoader; type OutputWriter = bun_core::io::Writer; @@ -248,16 +250,7 @@ impl<'a> ProcessHandle<'a> { ) }); - match process.watch_or_reap() { - Ok(_) => {} - Err(err) => { - if !process.has_exited() { - // SAFETY: all-zero is a valid Rusage (POD C struct) - let rusage = bun_core::ffi::zeroed::(); - process.on_exit(Status::Err(err), &rusage); - } - } - } + watch_or_reap(process); Ok(()) } @@ -510,84 +503,11 @@ impl<'a> State<'a> { } pub(crate) fn finalize(&self) -> u8 { - for handle in self.handles.iter() { - if let Some(proc) = &handle.process { - match &proc.status { - Status::Exited(exited) => { - if exited.code != 0 { - return exited.code; - } - } - Status::Signaled(signal) => { - return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); - } - _ => return 1, - } - } - } - 0 - } -} - -struct AbortHandler; - -static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); - -impl AbortHandler { - #[cfg(unix)] - extern "C" fn posix_signal_handler( - _sig: i32, - _info: *const bun_sys::posix::siginfo_t, - _: *const c_void, - ) { - SHOULD_ABORT.store(true, Ordering::SeqCst); - } - - #[cfg(windows)] - extern "system" fn windows_ctrl_handler( - dw_ctrl_type: bun_sys::windows::DWORD, - ) -> bun_sys::windows::BOOL { - if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { - SHOULD_ABORT.store(true, Ordering::SeqCst); - return bun_sys::windows::TRUE; - } - bun_sys::windows::FALSE - } - - pub(crate) fn install() { - #[cfg(unix)] - { - // bun_sys::posix::Sigaction is a re-export of libc::sigaction; construct - // via zeroed() (POD C struct) and populate sa_sigaction/sa_mask/sa_flags. - // SAFETY: all-zero is a valid `libc::sigaction`; sigemptyset/sigaction are - // FFI calls with no extra preconditions beyond valid pointers. - unsafe { - let mut action: bun_sys::posix::Sigaction = bun_core::ffi::zeroed(); - action.sa_sigaction = Self::posix_signal_handler as *const () as usize; - libc::sigemptyset(&raw mut action.sa_mask); - action.sa_flags = (libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND) as _; - bun_sys::posix::sigaction(libc::SIGINT, &raw const action, core::ptr::null_mut()); - } - } - #[cfg(not(unix))] - { - let res = bun_sys::windows::SetConsoleCtrlHandler( - Some(Self::windows_ctrl_handler), - bun_sys::windows::TRUE, - ); - if res == 0 { - if bun_core::env::IS_DEBUG { - bun_core::warn!("Failed to set abort handler\n"); - } - } - } - } - - pub(crate) fn uninstall() { - #[cfg(windows)] - { - let _ = bun_sys::windows::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); - } + aggregate_exit_code( + self.handles + .iter() + .map(|h| h.process.as_ref().map(|p| &p.status)), + ) } } @@ -1140,18 +1060,8 @@ pub(crate) fn run(ctx: &mut Command::ContextData) -> Result(), - ))), - #[cfg(unix)] - stderr: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stderr: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), + stdout: buffered_stdio(), + stderr: buffered_stdio(), cwd: config.cwd.clone(), #[cfg(windows)] windows: spawn::WindowsOptions { diff --git a/src/runtime/cli/outdated_command.rs b/src/runtime/cli/outdated_command.rs index 48b36fb19553..bc493503b881 100644 --- a/src/runtime/cli/outdated_command.rs +++ b/src/runtime/cli/outdated_command.rs @@ -8,16 +8,12 @@ use bun_core::{Global, Output}; use bun_glob as glob; use bun_install::dependency::{self, Behavior}; use bun_install::lockfile::package::PackageColumns as _; -use bun_install::lockfile::{LoadResult, LoadStep}; -use bun_install::package_manager::{ - self, LogLevel, ManifestLoad, Subcommand, WorkspaceFilter, populate_manifest_cache, -}; +use bun_install::package_manager::{self, ManifestLoad, Subcommand, populate_manifest_cache}; use bun_install::{CommandLineArguments, DependencyID, PackageID, PackageManager, resolution}; -use bun_paths::{self as path, PathBuffer}; -use bun_resolver::fs::FileSystem; use bun_wyhash::hash; use crate::Command; +use crate::cli::workspace_helpers; pub(crate) struct OutdatedCommand; @@ -89,68 +85,7 @@ impl OutdatedCommand { original_cwd: &[u8], manager: &mut PackageManager, ) -> crate::Result<()> { - // Reshaped for borrowck — `load_from_cwd` would otherwise alias - // `PackageManager` with its `lockfile` field. Project disjoint - // raw pointers from the singleton first; `load_from_cwd` only reads - // `manager.options` / migration helpers and never re-borrows - // `manager.lockfile` through the `pm` argument. - let pm_ptr: *mut PackageManager = manager; - let not_silent = manager.options.log_level != LogLevel::Silent; - let log_ptr: *mut bun_ast::Log = manager.log; - - // SAFETY: `lockfile` is the owned `Box` field on the singleton; - // no other live `&mut Lockfile` exists at this point. - let lockfile: &mut bun_install::lockfile::Lockfile = unsafe { &mut *(*pm_ptr).lockfile }; - // SAFETY: `manager.log` is set non-null by `PackageManager::init`. - let log = unsafe { &mut *log_ptr }; - match lockfile.load_from_cwd::( - // SAFETY: see comment above — `load_from_cwd` accesses `manager` - // fields disjoint from `lockfile`. - Some(unsafe { &mut *pm_ptr }), - log, - ) { - LoadResult::NotFound => { - if not_silent { - Output::err_generic("missing lockfile, nothing outdated", ()); - } - Global::crash(); - } - LoadResult::Err(cause) => { - if not_silent { - match cause.step { - LoadStep::OpenFile => Output::err_generic( - "failed to open lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ParseFile => Output::err_generic( - "failed to parse lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ReadFile => Output::err_generic( - "failed to read lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::Migrating => Output::err_generic( - "failed to migrate lockfile: {s}", - (cause.value.name(),), - ), - } - if ctx.log_ref().has_errors() { - // SAFETY: `log_ptr` aliases `manager.log` which is the - // `*logger.Log` borrowed from `Command::Context`; no - // other `&mut Log` is live here. - let _ = - unsafe { (*log_ptr).print(std::ptr::from_mut(Output::error_writer())) }; - } - } - Global::crash(); - } - LoadResult::Ok(_) => { - // `load_from_cwd(&mut self, ..)` populates the - // lockfile in place, so the `ok.lockfile: &mut Lockfile` reborrow - // is the same storage and no reassignment is needed. - } - } + workspace_helpers::load_lockfile_or_crash(ctx, manager); if Output::enable_ansi_colors_stdout() { Self::outdated_dispatch::(original_cwd, manager) @@ -165,14 +100,15 @@ impl OutdatedCommand { ) -> crate::Result<()> { if !manager.options.filter_patterns.is_empty() { let filters = manager.options.filter_patterns; - let workspace_pkg_ids = Self::find_matching_workspaces(original_cwd, manager, filters); + let workspace_pkg_ids = + workspace_helpers::find_matching_workspaces(original_cwd, manager, filters); populate_manifest_cache::populate_manifest_cache( manager, populate_manifest_cache::Packages::Ids(&workspace_pkg_ids), )?; Self::print_outdated_info_table::(manager, &workspace_pkg_ids, true) } else if manager.options.do_.recursive() { - let all_workspaces = Self::get_all_workspaces(manager); + let all_workspaces = workspace_helpers::get_all_workspaces(manager); populate_manifest_cache::populate_manifest_cache( manager, populate_manifest_cache::Packages::Ids(&all_workspaces), @@ -194,118 +130,6 @@ impl OutdatedCommand { } } - fn get_all_workspaces(manager: &PackageManager) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_resolutions = packages.items_resolution(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - workspace_pkg_ids - } - - fn find_matching_workspaces( - original_cwd: &[u8], - manager: &PackageManager, - filters: &[&[u8]], - ) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_names = packages.items_name(); - let pkg_resolutions = packages.items_resolution(); - let string_buf = lockfile.buffers.string_bytes.as_slice(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - - let mut path_buf = PathBuffer::uninit(); - - let converted_filters: Vec = filters - .iter() - .map(|filter| { - bun_core::handle_oom(WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0)) - }) - .collect(); - // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. - - // SAFETY: `FileSystem::init` runs during `PackageManager::init` so the - // process-singleton is populated. - let top_level_dir = FileSystem::get().top_level_dir; - - // move all matched workspaces to front of array - let mut i: usize = 0; - while i < workspace_pkg_ids.len() { - let workspace_pkg_id = workspace_pkg_ids[i]; - - let matched = 'matched: { - for filter in &converted_filters { - match filter { - WorkspaceFilter::Path(pattern) => { - if pattern.is_empty() { - continue; - } - let res = &pkg_resolutions[workspace_pkg_id as usize]; - let res_path: &[u8] = match res.tag { - resolution::Tag::Workspace => { - // Borrow the field in-place so the returned slice (which may - // point into the inline small-string storage) stays valid. - res.workspace().slice(string_buf) - } - resolution::Tag::Root => top_level_dir, - _ => unreachable!(), - }; - - let abs_res_path = path::resolve_path::join_abs_string_buf::< - path::platform::Posix, - >( - top_level_dir, &mut path_buf.0, &[res_path] - ); - - if !glob::r#match( - pattern, - strings::without_trailing_slash(abs_res_path), - ) - .matches() - { - break 'matched false; - } - } - WorkspaceFilter::Name(pattern) => { - let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); - if !glob::r#match(pattern, name).matches() { - break 'matched false; - } - } - WorkspaceFilter::All => {} - } - } - true - }; - - if matched { - i += 1; - } else { - workspace_pkg_ids.swap_remove(i); - } - } - - workspace_pkg_ids - } - fn group_catalog_dependencies( manager: &PackageManager, outdated_items: &[OutdatedInfo], diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index ae5bfc7610f9..4cf2fa85aedc 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -33,9 +33,7 @@ use bun_glob::matcher::MatchResult as GlobMatchResult; use bun_paths::resolve_path; use bun_semver as Semver; use bun_sha_hmac::sha; -use bun_sys::{ - self, CloseOnDrop, Dir, Fd, FdDirExt as _, FdExt as _, File, dir_iterator as DirIterator, -}; +use bun_sys::{self, Dir, Fd, FdDirExt as _, FdExt as _, File, dir_iterator as DirIterator}; // ─────────────────────────────────────────────────────────────────────────── // local shims for upstream-stub gaps @@ -1906,7 +1904,7 @@ fn new_boxed_buffered_file_reader(file: bun_sys::File) -> Box Option<&[u8]> { // pack() // ─────────────────────────────────────────────────────────────────────────── -// Const generics cannot vary the -// return type directly, so both instantiations return an Option that is -// `Some` only when FOR_PUBLISH == true. -pub(crate) type PackReturn<'a, const FOR_PUBLISH: bool> = Option>; - -pub(crate) fn pack( - ctx: &mut Context<'_>, +/// Reads `abs_package_json_path` through the workspace package.json cache; +/// read/parse failures are fatal. Unlike +/// `WorkspacePackageJSONCache::get_with_path_or_exit`, this keeps pack's +/// error wording and ordering (`Output::err` first, then the log printed +/// unconditionally on parse errors), matching the `pack_command.zig` +/// reference. +fn load_package_json_or_exit<'a>( + manager_ptr: *mut PackageManager, abs_package_json_path: &ZStr, -) -> Result, PackError> { - // Note: reshaped for borrowck — - // `ctx`-whole calls (`run_lifecycle_script(ctx, …)`, - // `iterate_bundled_deps(ctx, …)`) overlap the manager borrow. - // Round-trip the field through a raw - // pointer so the long-lived `manager` reborrow is decoupled from `ctx`; - // every interleaved `ctx` access touches disjoint fields (`command_ctx`, - // `bundled_deps`, `stats`) or only reads `manager` via `pm_*` helpers. - let manager_ptr: *mut PackageManager = &raw mut *ctx.manager; - // SAFETY: `ctx.manager` is the sole `&mut PackageManager`; CLI is - // single-threaded and no callee retains a conflicting borrow. - let manager: &mut PackageManager = unsafe { &mut *manager_ptr }; - let log_level = manager.options.log_level; - let bump = pack_bump(); +) -> &'a mut WorkspacePackageJSONCache::MapEntry { // Note: `workspace_package_json_cache` and `log` are disjoint fields on // `PackageManager`; route through raw-pointer field projections so the // two `&mut` borrows don't conflict. - let mut json = match pm_workspace_cache(manager_ptr).get_with_path( + match pm_workspace_cache(manager_ptr).get_with_path( pm_log(manager_ptr), abs_package_json_path.as_bytes(), WorkspacePackageJSONCache::GetJSONOptions { @@ -2011,7 +1997,32 @@ pub(crate) fn pack( Global::crash(); } WorkspacePackageJSONCache::GetResult::Entry(entry) => entry, - }; + } +} + +// Const generics cannot vary the +// return type directly, so both instantiations return an Option that is +// `Some` only when FOR_PUBLISH == true. +pub(crate) type PackReturn<'a, const FOR_PUBLISH: bool> = Option>; + +pub(crate) fn pack( + ctx: &mut Context<'_>, + abs_package_json_path: &ZStr, +) -> Result, PackError> { + // Note: reshaped for borrowck — + // `ctx`-whole calls (`run_lifecycle_script(ctx, …)`, + // `iterate_bundled_deps(ctx, …)`) overlap the manager borrow. + // Round-trip the field through a raw + // pointer so the long-lived `manager` reborrow is decoupled from `ctx`; + // every interleaved `ctx` access touches disjoint fields (`command_ctx`, + // `bundled_deps`, `stats`) or only reads `manager` via `pm_*` helpers. + let manager_ptr: *mut PackageManager = &raw mut *ctx.manager; + // SAFETY: `ctx.manager` is the sole `&mut PackageManager`; CLI is + // single-threaded and no callee retains a conflicting borrow. + let manager: &mut PackageManager = unsafe { &mut *manager_ptr }; + let log_level = manager.options.log_level; + let bump = pack_bump(); + let mut json = load_package_json_or_exit(manager_ptr, abs_package_json_path); if FOR_PUBLISH { if let Some(config) = json.root.get(b"publishConfig") { @@ -2241,33 +2252,7 @@ pub(crate) fn pack( let _ = pm_workspace_cache(manager_ptr).map.remove(cache_key); // Re-read package.json from disk - json = match pm_workspace_cache(manager_ptr).get_with_path( - pm_log(manager_ptr), - abs_package_json_path.as_bytes(), - WorkspacePackageJSONCache::GetJSONOptions { - guess_indentation: true, - ..Default::default() - }, - ) { - WorkspacePackageJSONCache::GetResult::ReadErr(err) => { - Output::err( - err, - "failed to read package.json: {}", - format_args!("{}", bstr::BStr::new(abs_package_json_path.as_bytes())), - ); - Global::crash(); - } - WorkspacePackageJSONCache::GetResult::ParseErr(err) => { - Output::err( - err, - "failed to parse package.json: {}", - format_args!("{}", bstr::BStr::new(abs_package_json_path.as_bytes())), - ); - let _ = pm_log(manager_ptr).print(std::ptr::from_mut(Output::error_writer())); - Global::crash(); - } - WorkspacePackageJSONCache::GetResult::Entry(entry) => entry, - }; + json = load_package_json_or_exit(manager_ptr, abs_package_json_path); // Re-validate private flag after scripts may have modified it. if FOR_PUBLISH { @@ -2664,11 +2649,9 @@ pub(crate) fn pack( node = Some(progress.start(b"", pack_queue.count() + bundled_pack_queue.count() + 1)); node.as_mut().expect("infallible: progress active").unit = Progress::Unit::Files; } - // Note: the loop bodies' only early exits are `continue` - // and `Global::crash()` (never returns, no - // unwinding). `scopeguard` captures of `&mut node` overlap the inline - // uses below, so call `complete_one()` explicitly at every loop-body - // exit and `end()` once after the loops. + // Note: `scopeguard` captures of `&mut node` would overlap the inline + // uses below, so call `complete_one()` explicitly after each archived + // entry and `end()` once after the queues drain. entry = archive_package_json( ctx, @@ -2685,142 +2668,41 @@ pub(crate) fn pack( .complete_one(); } - while let Some(item) = pack_queue.remove_or_null() { - let file = match bun_sys::openat( - Fd::from_std_dir(&root_dir), - &item.path, - bun_sys::O::RDONLY, - 0, - ) { - Ok(f) => f, - Err(err) => { - if item.optional { - ctx.stats.total_files -= 1; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - continue; - } - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - let fd: Fd = match file - .make_lib_uv_owned_for_syscall(bun_sys::Tag::open, bun_sys::ErrorCase::CloseOnFail) - { - Ok(fd) => fd, - Err(err) => { - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - let _close_fd = CloseOnDrop::new(fd); - - let stat = match bun_sys::sys_uv::fstat(fd) { - Ok(s) => s, - Err(err) => { - Output::err( - err, - "failed to stat file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - pack_list.push(PackListEntry { - subpath: ZBox::from_bytes(item.path.as_bytes()), - size: usize::try_from(stat.st_size).expect("int cast"), - }); - - entry = add_archive_entry( - ctx, - fd, - &stat, - &item.path, - &mut read_buf, - &mut file_reader, - // SAFETY: `archive` is the non-null `*mut Archive` returned by - // `Archive::write_new()` above; only this thread accesses it. - unsafe { &mut *archive }, - entry, - &mut print_buf, - &bins, - )?; - - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - } - - while let Some(item) = bundled_pack_queue.remove_or_null() { - let file = match root_dir.open_file(&item.path, bun_sys::O::RDONLY, 0) { - Ok(f) => f, - Err(err) => { - if item.optional { - ctx.stats.total_files -= 1; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - continue; - } - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - let stat = match file.stat() { - Ok(s) => s, - Err(err) => { - Output::err( - err, - "failed to stat file: \"{}\"", - format_args!("{}", file.handle), - ); - Global::crash(); - } - }; - - entry = add_archive_entry( - ctx, - file.handle, - &stat, - &item.path, - &mut read_buf, - &mut file_reader, - // SAFETY: `archive` is the non-null `*mut Archive` returned by - // `Archive::write_new()` above; only this thread accesses it. - unsafe { &mut *archive }, - entry, - &mut print_buf, - &bins, - )?; + entry = archive_pack_queue( + ctx, + &mut pack_queue, + PackQueueOpenMode::UvOwnedFd, + &root_dir, + Some(&mut pack_list), + &mut read_buf, + &mut file_reader, + // SAFETY: `archive` is the non-null `*mut Archive` returned by + // `Archive::write_new()` above; only this thread accesses it. + unsafe { &mut *archive }, + entry, + &mut print_buf, + &bins, + log_level, + &mut node, + )?; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - } + entry = archive_pack_queue( + ctx, + &mut bundled_pack_queue, + PackQueueOpenMode::PlainFile, + &root_dir, + None, + &mut read_buf, + &mut file_reader, + // SAFETY: `archive` is the non-null `*mut Archive` returned by + // `Archive::write_new()` above; only this thread accesses it. + unsafe { &mut *archive }, + entry, + &mut print_buf, + &bins, + log_level, + &mut node, + )?; if log_level.show_progress() { if let Some(n) = node.as_mut() { @@ -3275,6 +3157,144 @@ fn archive_package_json( Ok(entry.clear()) } +/// How [`archive_pack_queue`] opens each queued file. +#[derive(Clone, Copy, PartialEq, Eq)] +enum PackQueueOpenMode { + /// Open with `bun_sys::openat` and convert to a libuv-owned descriptor + /// stat'd through `sys_uv`. + UvOwnedFd, + /// Open relative to `root_dir` as a plain `File`. + PlainFile, +} + +/// Drains `queue`, archiving each file via [`add_archive_entry`]; see +/// [`PackQueueOpenMode`] for how each file is opened. Each entry is also +/// appended to `pack_list` when provided. +/// +/// The loop body's only early exits are `continue` and `Global::crash()` +/// (never returns, no unwinding), so `node.complete_one()` is called +/// explicitly at every loop-body exit instead of via a scope guard. +fn archive_pack_queue( + ctx: &mut Context<'_>, + queue: &mut PackQueue, + open_mode: PackQueueOpenMode, + root_dir: &Dir, + mut pack_list: Option<&mut PackList>, + read_buf: &mut [u8], + file_reader: &mut BufferedFileReader, + archive: &mut Archive, + mut entry: *mut ArchiveEntry, + print_buf: &mut Vec, + bins: &[BinInfo], + log_level: LogLevel, + node: &mut Option<&mut Progress::Node>, +) -> Result<*mut ArchiveEntry, AllocError> { + let uv_owned_fd = open_mode == PackQueueOpenMode::UvOwnedFd; + while let Some(item) = queue.remove_or_null() { + let opened = if uv_owned_fd { + bun_sys::openat( + Fd::from_std_dir(root_dir), + &item.path, + bun_sys::O::RDONLY, + 0, + ) + .map(File::from_fd) + } else { + root_dir.open_file(&item.path, bun_sys::O::RDONLY, 0) + }; + let file = match opened { + Ok(f) => f, + Err(err) => { + if item.optional { + ctx.stats.total_files -= 1; + if log_level.show_progress() { + node.as_mut() + .expect("infallible: progress active") + .complete_one(); + } + continue; + } + Output::err( + err, + "failed to open file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + Global::crash(); + } + }; + + let file = if uv_owned_fd { + match file + .into_raw() + .make_lib_uv_owned_for_syscall(bun_sys::Tag::open, bun_sys::ErrorCase::CloseOnFail) + { + Ok(fd) => File::from_fd(fd), + Err(err) => { + Output::err( + err, + "failed to open file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + Global::crash(); + } + } + } else { + file + }; + + let stat = match if uv_owned_fd { + bun_sys::sys_uv::fstat(file.handle) + } else { + file.stat() + } { + Ok(s) => s, + Err(err) => { + if uv_owned_fd { + Output::err( + err, + "failed to stat file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + } else { + Output::err( + err, + "failed to stat file: \"{}\"", + format_args!("{}", file.handle), + ); + } + Global::crash(); + } + }; + + if let Some(pack_list) = pack_list.as_deref_mut() { + pack_list.push(PackListEntry { + subpath: ZBox::from_bytes(item.path.as_bytes()), + size: usize::try_from(stat.st_size).expect("int cast"), + }); + } + + entry = add_archive_entry( + ctx, + file.handle, + &stat, + &item.path, + read_buf, + file_reader, + archive, + entry, + print_buf, + bins, + )?; + + if log_level.show_progress() { + node.as_mut() + .expect("infallible: progress active") + .complete_one(); + } + } + Ok(entry) +} + fn add_archive_entry( ctx: &mut Context<'_>, file: Fd, diff --git a/src/runtime/cli/pm_update_package_json.rs b/src/runtime/cli/pm_update_package_json.rs index a19296c5ab90..3c0d47c8fc61 100644 --- a/src/runtime/cli/pm_update_package_json.rs +++ b/src/runtime/cli/pm_update_package_json.rs @@ -17,7 +17,7 @@ use bun_install::package_manager_real::{Subcommand, update_package_json_and_inst use crate::build_command::BuildCommand; use crate::cli::Cli; -use crate::command::{self, Context, ContextData}; +use crate::command::{Context, ContextData}; pub fn update_package_json_and_install_catch_error( ctx: Context, @@ -49,96 +49,111 @@ pub fn update_package_json_and_install(ctx: Context, subcommand: Subcommand) -> // `parse` requires ``, expand to a `match`. let mut cli = CommandLineArguments::parse(subcommand)?; - // The way this works: - // 1. Run the bundler on source files - // 2. Rewrite positional arguments to act identically to the developer - // typing in the dependency names - // 3. Run the install command if cli.analyze { - // `ctx`/`cli` are stored as raw `*mut` because - // `BuildCommand::exec` holds `command::get()` (the same `ContextData`) across - // the `on_fetch` callback, and `DependenciesScanner.entry_points` owns a copy - // of `cli.positionals[1..]` for the duration of the scan; storing `&mut` here - // would assert exclusivity we don't have. - struct Analyzer { - ctx: *mut ContextData, - cli: *mut CommandLineArguments, - subcommand: Subcommand, - } - impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer { - fn on_analyze( - &mut self, - result: &mut DependenciesScannerResult<'_, '_>, - ) -> Result<(), bun_bundler::Error> { - let this = self; - // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed - // - // Process-lifetime storage for the rewritten positionals — - // `Global::exit(0)` follows immediately. `OnceLock` (not - // leaked). - static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); - static POSITIONALS: std::sync::OnceLock> = - std::sync::OnceLock::new(); + return analyze_dependencies_and_install(ctx, &mut cli, b"add", &mut |ctx, cli| { + update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) + }); + } + + update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) +} + +/// Shared body of the `cli.analyze` branch of `bun install` / `bun add`: +/// 1. Run the bundler's dependency scanner over the positional entry points +/// 2. Rewrite the positionals to `[verb, ...discovered dependency names]`, +/// acting identically to the developer typing in the dependency names +/// 3. Re-enter the install path via `install` and exit the process +pub(crate) fn analyze_dependencies_and_install( + ctx: &mut ContextData, + cli: &mut CommandLineArguments, + verb: &'static [u8], + install: &mut dyn FnMut(&mut ContextData, CommandLineArguments) -> Result<(), Error>, +) -> Result<(), Error> { + // `ctx`/`cli` are stored as raw `*mut` because `BuildCommand::exec` holds + // the global `Context` (the same `ContextData`) across the `on_analyze` + // callback, and `DependenciesScanner.entry_points` owns a copy of + // `cli.positionals[1..]` for the duration of the scan; storing `&mut` + // here would assert exclusivity we don't have. + struct Analyzer<'a> { + ctx: *mut ContextData, + cli: *mut CommandLineArguments, + verb: &'static [u8], + install: &'a mut dyn FnMut(&mut ContextData, CommandLineArguments) -> Result<(), Error>, + } + impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer<'_> { + fn on_analyze( + &mut self, + result: &mut DependenciesScannerResult<'_, '_>, + ) -> Result<(), bun_bundler::Error> { + let this = self; + // Process-lifetime storage for the rewritten positionals — + // `Global::exit(0)` follows immediately. + // `OnceLock` (not leaking) per PORTING.md §Forbidden. + static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); + static POSITIONALS: std::sync::OnceLock> = + std::sync::OnceLock::new(); - let owned = OWNED_KEYS.get_or_init(|| { - result - .dependencies - .keys() - .iter() - .map(|k| Box::<[u8]>::from(&**k)) - .collect() - }); - let positionals = POSITIONALS.get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); - v.push(b"add"); - for k in owned { - v.push(&**k); - } - v - }); + let owned = OWNED_KEYS.get_or_init(|| { + result + .dependencies + .keys() + .iter() + .map(|k| Box::<[u8]>::from(&**k)) + .collect() + }); + let positionals = POSITIONALS.get_or_init(|| { + let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); + v.push(this.verb); + for k in owned { + v.push(&**k); + } + v + }); - // SAFETY: `this.cli` / `this.ctx` were set from live stack locals in - // `update_package_json_and_install` whose scope encloses the entire - // `BuildCommand::exec` call (and hence this callback). The bundler has - // finished reading `entry_points` before invoking `on_fetch`, and this - // callback never returns (`Global::exit` below), so forming fresh `&mut` - // here is exclusive for the remainder of the process. - let cli = unsafe { &mut *this.cli }; - cli.positionals = positionals.as_slice(); - // SAFETY: `this.ctx` points to the `ctx` stack local in - // `update_package_json_and_install`, whose frame outlives this - // callback; `Global::exit` below makes this `&mut` exclusive for - // the remainder of the process. - let ctx = unsafe { &mut *this.ctx }; + // SAFETY: `this.cli` / `this.ctx` were set from live locals in + // `analyze_dependencies_and_install`'s caller, whose scope + // encloses the entire `BuildCommand::exec` call (and hence this + // callback). The bundler does not touch the global `ContextData` + // between dependency-scan completion and `on_analyze` invocation, + // so forming a fresh `&mut` here is exclusive for the duration of + // the `install` continuation. + let cli = unsafe { &mut *this.cli }; + cli.positionals = positionals.as_slice(); + // SAFETY: see above — same invariant covers `this.ctx`. + let ctx = unsafe { &mut *this.ctx }; - update_package_json_and_install_and_cli(ctx, this.subcommand, cli.clone()) - .map_err(crate::Error::from)?; + (this.install)(ctx, cli.clone()).map_err(bun_bundler::Error::from)?; - Global::exit(0); - } + Global::exit(0); } + } - // Note: `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. - // Clone the argv slices into an owned - // buffer (small one-shot list — no perf concern) so `cli` is not borrowed across - // the `&mut analyzer` setup. - let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] - .iter() - .map(|s| Box::<[u8]>::from(*s)) - .collect(); - - let mut analyzer = Analyzer { - ctx: std::ptr::from_mut::(ctx), - cli: &raw mut cli, - subcommand, - }; + // `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. Clone the + // argv slices into an owned buffer (small one-shot list — no perf + // concern). Captured *before* raw-ptr aliasing of `cli` below so the + // access goes through the live `&mut cli` borrow. + let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] + .iter() + .map(|s| Box::<[u8]>::from(*s)) + .collect(); - let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); + // Derive raw pointers from the existing `&mut` borrows; all subsequent + // access to `ctx` / `cli` in this function goes through these. + let ctx_ptr: *mut ContextData = ctx; + let mut analyzer = Analyzer { + ctx: ctx_ptr, + cli, + verb, + install, + }; - // This runs the bundler. - BuildCommand::exec(command::get(), Some(&fetcher))?; - return Ok(()); - } + let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); - update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) + // `Command.get()` resolves to the same `*ContextData` already held in + // `ctx`; reborrow through `ctx_ptr` rather than minting a fresh + // `&'static mut` from the global static (which would alias the + // still-live `ctx` parameter under stacked borrows). + // SAFETY: `ctx_ptr` was just derived from the live `ctx: &mut + // ContextData` parameter; `ctx` is not accessed again in this function. + BuildCommand::exec(unsafe { &mut *ctx_ptr }, Some(&fetcher)) } diff --git a/src/runtime/cli/repl.rs b/src/runtime/cli/repl.rs index dcce6191a6f9..408b77fccee5 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -580,6 +580,16 @@ enum ReplResult { SkipEval, } +/// How `evaluate_to_value` reports promise rejections and interrupts. +/// Mirrors the difference between repl.zig's evaluateAndPrint (sets `_error` +/// on globalThis, prints a newline on interrupt) and evaluateAndCopy (does +/// neither). +#[derive(Clone, Copy, PartialEq, Eq)] +enum ReportMode { + Print, + Copy, +} + fn cmd_help(repl: &mut Repl, _: &[u8]) -> ReplResult { repl.print(format_args!( "\n{}REPL Commands:{}\n", @@ -1255,19 +1265,20 @@ impl<'a> Repl<'a> { // JavaScript Evaluation // ======================================================================== - fn evaluate_and_print(&mut self, code: &[u8]) { - let Some(global) = self.global else { - return; - }; - let Some(vm) = self.vm else { - return; - }; + /// Run `code` through the interactive REPL pipeline: transform_for_repl, + /// evaluate, await any async IIFE promise (with Ctrl+C signal handling), + /// unwrap the `{ value: expr }` wrapper, then store the result and set `_` + /// on globalThis. Returns `None` when the outcome was already reported + /// (errors, interrupts, or raw-evaluation fallback). + fn evaluate_to_value(&mut self, code: &[u8], mode: ReportMode) -> Option { + let global = self.global?; + let vm = self.vm?; // Transform the code using REPL mode (hoists declarations, wraps result in { value: expr }) let Some(transformed_code) = self.transform_for_repl(code) else { // Transform failed, try evaluating raw code (for syntax errors, etc.) self.evaluate_raw(code); - return; + return None; }; // Evaluate the transformed code @@ -1289,7 +1300,7 @@ impl<'a> Repl<'a> { if !exception.is_undefined() && !exception.is_null() { self.set_last_error(exception); self.print_js_error(exception); - return; + return None; } // Handle async IIFE results - wait for promise to resolve @@ -1313,7 +1324,7 @@ impl<'a> Repl<'a> { global.clear_termination_exception(); self.print(format_args!("\n")); self.disable_signals_during_wait(); - return; + return None; } // SAFETY: `vm.jsc_vm` is the live JSC VM handle for this thread. @@ -1326,18 +1337,22 @@ impl<'a> Repl<'a> { PromiseStatus::Rejected => { let rejection = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref); self.set_last_error(rejection); - // Set _error on the global object - let global_this = global.to_js_value(); - global_this.put(global, b"_error", rejection); + if mode == ReportMode::Print { + // Set _error on the global object + let global_this = global.to_js_value(); + global_this.put(global, b"_error", rejection); + } self.print_js_error(rejection); self.disable_signals_during_wait(); - return; + return None; } PromiseStatus::Pending => { // Interrupted by signal or timed out - self.print(format_args!("\n")); + if mode == ReportMode::Print { + self.print(format_args!("\n")); + } self.disable_signals_during_wait(); - return; + return None; } } self.disable_signals_during_wait(); @@ -1357,7 +1372,7 @@ impl<'a> Repl<'a> { self.set_last_error(exc); self.print_js_error(exc); vm.as_mut().tick(); - return; + return None; } }; if let Some(value) = maybe_value { @@ -1365,7 +1380,7 @@ impl<'a> Repl<'a> { } } - // Store and print result + // Store the result self.set_last_result(actual_result); // Set _ to the last result (only if not undefined) @@ -1375,6 +1390,14 @@ impl<'a> Repl<'a> { global_this.put(global, b"_", actual_result); } + Some(actual_result) + } + + fn evaluate_and_print(&mut self, code: &[u8]) { + let Some(actual_result) = self.evaluate_to_value(code, ReportMode::Print) else { + return; + }; + if actual_result.is_undefined() { if self.use_colors { self.print(format_args!("{}undefined{}\n", Color::DIM, Color::RESET)); @@ -1386,7 +1409,9 @@ impl<'a> Repl<'a> { } // Tick the event loop to handle any pending work - vm.as_mut().tick(); + if let Some(vm) = self.vm { + vm.as_mut().tick(); + } } /// Evaluate a script from `bun repl -e/--eval` or `-p/--print` non-interactively. @@ -1566,104 +1591,20 @@ impl<'a> Repl<'a> { /// Evaluate code and copy the result to clipboard instead of printing it fn evaluate_and_copy(&mut self, code: &[u8]) { - let Some(global) = self.global else { + let Some(actual_result) = self.evaluate_to_value(code, ReportMode::Copy) else { return; }; - let Some(vm) = self.vm else { - return; - }; - - let Some(transformed_code) = self.transform_for_repl(code) else { - self.evaluate_raw(code); - return; - }; - - let mut exception: JSValue = JSValue::UNDEFINED; - // SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs - // are valid for the duration of the call; `exception` is a stack local. - let result = unsafe { - Bun__REPL__evaluate( - global, - transformed_code.as_ptr(), - transformed_code.len(), - b"[repl]".as_ptr(), - b"[repl]".len(), - &raw mut exception, - ) - }; - if !exception.is_undefined() && !exception.is_null() { - self.set_last_error(exception); - self.print_js_error(exception); - return; - } - - let mut resolved_result = result; - if let Some(promise) = result.as_promise() { - // SAFETY: `promise` is a live JSC heap cell; `vm.jsc_vm` is the - // owning JSC VM handle for this thread. - jsc::JSPromise::opaque_mut(promise).set_handled(); - self.enable_signals_during_wait(); - // Note: reshaped for borrowck — disable_signals_during_wait called on each path - vm.as_mut() - .wait_for_promise(jsc::AnyPromise::Normal(promise)); - if vm.jsc_vm().execution_forbidden() { - vm.jsc_vm().set_execution_forbidden(false); - global.clear_termination_exception(); - self.print(format_args!("\n")); - self.disable_signals_during_wait(); - return; - } - let jsc_vm_ref = vm.jsc_vm(); - match jsc::JSPromise::opaque_mut(promise).status() { - PromiseStatus::Fulfilled => { - resolved_result = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref) - } - PromiseStatus::Rejected => { - let rejection = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref); - self.set_last_error(rejection); - self.print_js_error(rejection); - self.disable_signals_during_wait(); - return; - } - PromiseStatus::Pending => { - self.disable_signals_during_wait(); - return; - } - } - self.disable_signals_during_wait(); - } - - let mut actual_result = resolved_result; - if resolved_result.is_object() { - let maybe_value = - match resolved_result.get_own(global, &bun_core::String::static_("value")) { - Ok(v) => v, - Err(err) => { - let exc = global.take_exception(err); - self.set_last_error(exc); - self.print_js_error(exc); - vm.as_mut().tick(); - return; - } - }; - if let Some(value) = maybe_value { - actual_result = value; + if let Err(err) = self.copy_value_to_clipboard(actual_result) { + if let Some(global) = self.global { + let exc = global.take_exception(err); + self.set_last_error(exc); + self.print_js_error(exc); } } - - self.set_last_result(actual_result); - if !actual_result.is_undefined() { - let global_this = global.to_js_value(); - global_this.put(global, b"_", actual_result); - } - - if let Err(err) = self.copy_value_to_clipboard(actual_result) { - let exc = global.take_exception(err); - self.set_last_error(exc); - self.print_js_error(exc); + if let Some(vm) = self.vm { + vm.as_mut().tick(); } - vm.as_mut().tick(); } /// Format a JS value as a string suitable for clipboard. diff --git a/src/runtime/cli/repl_command.rs b/src/runtime/cli/repl_command.rs index faf25548c8fd..7c392244b2b5 100644 --- a/src/runtime/cli/repl_command.rs +++ b/src/runtime/cli/repl_command.rs @@ -98,29 +98,7 @@ impl ReplCommand { // ReplRunner construction to avoid a move-after-borrow. // Configure bundler options - // `BundleOptions.install` is `Option>` so no - // lifetime-extension cast is needed. - let install_ptr = ctx.install.as_deref().map(core::ptr::NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - b.resolver.opts.prefer_offline_install = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Offline; - let prefer_latest = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Latest; - // The resolver's `BundleOptions` stub has no `prefer_latest_install` field and the - // resolver never reads it; only the bundler-side mirror carries it (matches - // run_command.rs / production.rs). - b.options.global_cache = b.resolver.opts.global_cache; - b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; - b.options.prefer_latest_install = prefer_latest; - b.resolver.env_loader = NonNull::new(b.env); + crate::cli::run_command::wire_install_options(b, ctx); b.options.env.behavior = EnvBehavior::LoadAllWithoutInlining; b.options.dead_code_elimination = false; // REPL needs all code @@ -305,4 +283,3 @@ unsafe extern "C" { } use bun_bundler::options::EnvBehavior; -use bun_options_types::offline_mode::OfflineMode; diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 28ed08bfbaeb..13fdd905b629 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -90,6 +90,36 @@ impl Default for ExecCfg { } } +/// Shared ctx→install/global-cache/offline-mode option projection for the +/// transpiler and its resolver, used by the run, repl, and bake-production +/// boot paths. +pub(crate) fn wire_install_options(b: &mut Transpiler<'_>, ctx: &ContextData) { + use bun_options_types::offline_mode::OfflineMode; + + // `BundleOptions::install` is a raw `NonNull` backref into + // the CLI's `Box` (process-lifetime). + // `as_deref` yields `&BunInstall`, which + // `NonNull::from` converts without the lifetime tie. + let install_ptr = ctx.install.as_deref().map(::core::ptr::NonNull::from); + b.options.install = install_ptr; + b.resolver.opts.install = install_ptr; + b.resolver.opts.global_cache = ctx.debug.global_cache; + let offline = ctx + .debug + .offline_mode_setting + .unwrap_or(OfflineMode::Online); + b.resolver.opts.prefer_offline_install = offline == OfflineMode::Offline; + // resolver's forward-decl `BundleOptions` lacks + // `prefer_latest_install`; only the bundler-side mirror carries it. + b.options.global_cache = ctx.debug.global_cache; + b.options.prefer_offline_install = offline == OfflineMode::Offline; + b.options.prefer_latest_install = offline == OfflineMode::Latest; + // Stored as `NonNull` (not `&Loader`): `configure_defines()` later + // reborrows the same allocation as `&mut Loader`, which would alias a + // live `&Loader`. The Loader outlives the resolver. + b.resolver.env_loader = ::core::ptr::NonNull::new(b.env); +} + pub struct RunCommand; impl RunCommand { @@ -782,27 +812,8 @@ Full documentation is available at https://bun.com/docs/cli/run /// [`boot_standalone`]. fn wire_transpiler_from_ctx(b: &mut Transpiler<'_>, ctx: &mut ContextData) { use bun_options_types::context::MacroOptions; - use bun_options_types::offline_mode::OfflineMode; - - // `BundleOptions::install` is a raw `NonNull` backref into - // the CLI's `Box` (process-lifetime). - // `as_deref` yields `&BunInstall`, which - // `NonNull::from` converts without the lifetime tie. - let install_ptr = ctx.install.as_deref().map(::core::ptr::NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - let offline = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online); - b.resolver.opts.prefer_offline_install = offline == OfflineMode::Offline; - // resolver's forward-decl `BundleOptions` lacks - // `prefer_latest_install`; only the bundler-side mirror carries it. - b.options.global_cache = ctx.debug.global_cache; - b.options.prefer_offline_install = offline == OfflineMode::Offline; - b.options.prefer_latest_install = offline == OfflineMode::Latest; - b.resolver.env_loader = ::core::ptr::NonNull::new(b.env); + + wire_install_options(b, ctx); b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; diff --git a/src/runtime/cli/run_processes_shared.rs b/src/runtime/cli/run_processes_shared.rs new file mode 100644 index 000000000000..4bfca5dd252c --- /dev/null +++ b/src/runtime/cli/run_processes_shared.rs @@ -0,0 +1,117 @@ +//! Helpers shared by the multi-process script runners: `filter_run` +//! (`bun run --filter`) and `multi_run` (`bun run --parallel`/`--sequential`). + +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::api::bun::process::{self as spawn, Process, Rusage, Status}; + +/// Set from a signal handler; polled by the run loops. +pub(crate) static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); + +pub(crate) struct AbortHandler; + +impl AbortHandler { + #[cfg(unix)] + extern "C" fn posix_signal_handler( + _sig: i32, + _info: *const bun_sys::posix::siginfo_t, + _: *const core::ffi::c_void, + ) { + SHOULD_ABORT.store(true, Ordering::SeqCst); + } + + #[cfg(windows)] + extern "system" fn windows_ctrl_handler( + dw_ctrl_type: bun_sys::windows::DWORD, + ) -> bun_sys::windows::BOOL { + if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { + SHOULD_ABORT.store(true, Ordering::SeqCst); + return bun_sys::windows::TRUE; + } + bun_sys::windows::FALSE + } + + pub(crate) fn install() { + #[cfg(unix)] + { + // SAFETY: all-zero is a valid `libc::sigaction`; sigemptyset/sigaction are + // FFI calls with no extra preconditions beyond valid pointers. + unsafe { + let mut action: bun_sys::posix::Sigaction = bun_core::ffi::zeroed(); + action.sa_sigaction = Self::posix_signal_handler as *const () as usize; + libc::sigemptyset(&raw mut action.sa_mask); + action.sa_flags = (libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND) as _; + bun_sys::posix::sigaction(libc::SIGINT, &raw const action, core::ptr::null_mut()); + } + } + #[cfg(not(unix))] + { + let res = bun_sys::windows::SetConsoleCtrlHandler( + Some(Self::windows_ctrl_handler), + bun_sys::windows::TRUE, + ); + if res == 0 { + if bun_core::env::IS_DEBUG { + bun_core::warn!("Failed to set abort handler\n"); + } + } + } + } + + pub(crate) fn uninstall() { + // only necessary on Windows, as on posix we pass the SA_RESETHAND flag + #[cfg(windows)] + { + // restores default Ctrl+C behavior + let _ = bun_sys::windows::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); + } + } +} + +/// `Process::watch_or_reap` with the shared error fallback: if registration +/// fails and the process has not already exited, synthesize an error exit so +/// the run loop still observes a terminal status. +pub(crate) fn watch_or_reap(process: &mut Process) { + if let Err(err) = process.watch_or_reap() { + if !process.has_exited() { + // SAFETY: all-zero is a valid Rusage (POD C struct) + let rusage = bun_core::ffi::zeroed::(); + process.on_exit(Status::Err(err), &rusage); + } + } +} + +/// First non-zero exit code across all spawned handles; signaled/errored +/// processes map to their signal exit code (or 1). 0 when every spawned +/// process exited cleanly. +pub(crate) fn aggregate_exit_code<'h>(statuses: impl Iterator>) -> u8 { + for status in statuses.flatten() { + match status { + Status::Exited(exited) => { + if exited.code != 0 { + return exited.code; + } + } + Status::Signaled(signal) => { + return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); + } + _ => return 1, + } + } + 0 +} + +/// A `Stdio::Buffer` slot for `SpawnOptions`; on Windows this carries a freshly +/// allocated libuv pipe whose ownership moves into the spawn result. +pub(crate) fn buffered_stdio() -> spawn::Stdio { + #[cfg(unix)] + { + spawn::Stdio::Buffer + } + #[cfg(not(unix))] + { + spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new(bun_core::ffi::zeroed::< + bun_sys::windows::libuv::Pipe, + >()))) + } +} diff --git a/src/runtime/cli/unlink_command.rs b/src/runtime/cli/unlink_command.rs index cfababcd13c5..55001cc46995 100644 --- a/src/runtime/cli/unlink_command.rs +++ b/src/runtime/cli/unlink_command.rs @@ -5,13 +5,11 @@ use bun_core::{Global, Output}; use bun_paths::{AbsPath, PathBuffer, platform, resolve_path}; use bun_sys::{self as sys, Dir, Fd, FdDirExt}; -use bun_install::Features; use bun_install::bin as stub_bin; use bun_install::bin_real as bin; -use bun_install::lockfile_real::{Lockfile, package::Package}; use bun_install::package_manager_real::{ self as pm, CommandLineArguments, Subcommand, attempt_to_create_package_json, - global_link_dir_path, options::LogLevel, package_manager_options, setup_global_dir, + global_link_dir_path, options::LogLevel, }; use crate::command::ContextData; @@ -50,66 +48,11 @@ fn unlink(ctx: &mut ContextData) -> crate::Result<()> { if manager.options.positionals.len() == 1 { // bun unlink - let mut lockfile = Lockfile::default(); - let mut package = Package::default(); - // Step 1. parse the nearest package.json file - { - let package_json_source = match bun_ast::to_source( - manager.original_package_json_path.as_zstr(), - Default::default(), - ) { - Ok(s) => s, - Err(e) => { - Output::err_generic( - "failed to read \"{}\" for unlinking: {}", - ( - BStr::new(manager.original_package_json_path.as_bytes()), - BStr::new(e.name()), - ), - ); - Global::crash(); - } - }; - lockfile.init_empty(); - - let mut resolver: () = (); - // `log_mut()` returns a borrow decoupled from `&self`; disjoint - // storage from `&mut PackageManager` (owned by the CLI `Context`). - let log = manager.log_mut(); - package.parse::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - &mut resolver, - Features::FOLDER, - )?; - let name = lockfile.str(&package.name); - if name.is_empty() { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: package.json missing \"name\" in \"{}\"", - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } else if !strings::is_npm_package_name(name) { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: invalid package.json name \"{}\" in \"{}\"", - BStr::new(name), - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } - } + let (lockfile, package) = super::link_command::load_package_for_link(manager, "unlinking")?; - // Reshaped for borrowck — `name` borrows `lockfile`; re-derive - // it after the parse block so its lifetime is decoupled from - // `package_json_source` (dropped above) while remaining a slice into - // `lockfile.buffers.string_bytes`. + // `name` is a slice into `lockfile.buffers.string_bytes`, decoupled + // from the helper-local package.json source. let name = lockfile.str(&package.name); match sys::lstat(resolve_path::join_abs_string_z::( @@ -135,36 +78,7 @@ fn unlink(ctx: &mut ContextData) -> crate::Result<()> { } // Step 2. Setup the global directory - let node_modules: Dir = 'brk: { - bin::Linker::ensure_umask(); - let explicit_global_dir: &[u8] = match &ctx.install { - Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), - None => b"", - }; - manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( - explicit_global_dir, - )?)); - - setup_global_dir(manager, &&mut *ctx)?; - - match manager - .global_dir - .as_ref() - .unwrap() - .make_open_path(b"node_modules", Default::default()) - { - Ok(d) => break 'brk d, - Err(e) => { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: failed to create node_modules in global dir due to error {}", - BStr::new(e.name()), - ); - } - Global::crash(); - } - } - }; + let node_modules: Dir = super::link_command::open_global_node_modules(manager, &mut *ctx)?; // Step 3b. Link any global bins if package.bin.tag != stub_bin::Tag::None { diff --git a/src/runtime/cli/update_interactive_command.rs b/src/runtime/cli/update_interactive_command.rs index 0b39657b04d9..e898f85f1209 100644 --- a/src/runtime/cli/update_interactive_command.rs +++ b/src/runtime/cli/update_interactive_command.rs @@ -8,13 +8,10 @@ use bstr::BStr; use bun_alloc::Arena as Bump; use bun_collections::StringHashMap; use bun_core::{Global, Output}; -use bun_glob as glob; use bun_install::dependency::{self, Behavior}; use bun_install::lockfile::package::PackageColumns as _; -use bun_install::lockfile::{LoadResult, LoadStep}; use bun_install::package_manager::{ - LogLevel, ManifestLoad, ROOT_PACKAGE_JSON_PATH, Subcommand, WorkspaceFilter, - install_with_manager, populate_manifest_cache, + ManifestLoad, ROOT_PACKAGE_JSON_PATH, Subcommand, install_with_manager, populate_manifest_cache, }; use bun_install::{ CommandLineArguments, GetJsonOptions, GetJsonResult, INVALID_PACKAGE_ID, PackageID, @@ -36,6 +33,7 @@ use bun_paths::{self as path, PathBuffer}; use bun_semver::{self as semver, SlicedString}; use crate::Command; +use crate::cli::workspace_helpers; pub(crate) struct TerminalHyperlink<'a> { link: &'a [u8], @@ -474,60 +472,13 @@ impl UpdateInteractiveCommand { original_cwd: &[u8], manager: &mut PackageManager, ) -> crate::Result<()> { - // Reshaped for borrowck — capture `log_level` / `ctx.log` - // before borrowing `&mut manager.lockfile`. - let not_silent = manager.options.log_level != LogLevel::Silent; - let ctx_log_ptr: *mut bun_ast::Log = ctx.log; - - match manager.load_lockfile_from_cwd::() { - LoadResult::NotFound => { - if not_silent { - Output::err_generic("missing lockfile, nothing outdated", ()); - } - Global::crash(); - } - LoadResult::Err(cause) => { - if not_silent { - match cause.step { - LoadStep::OpenFile => Output::err_generic( - "failed to open lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ParseFile => Output::err_generic( - "failed to parse lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ReadFile => Output::err_generic( - "failed to read lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::Migrating => Output::err_generic( - "failed to migrate lockfile: {s}", - (cause.value.name(),), - ), - } - // SAFETY: `ctx.log` is set by `Command::create_context_data` - // for every subcommand and is non-null for the command's - // lifetime. - if unsafe { (*ctx_log_ptr).has_errors() } { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - } - Global::crash(); - } - LoadResult::Ok(_) => { - // `load_lockfile_from_cwd` populates `manager.lockfile` (Box) - // in place, so no reassignment is needed. - } - } + workspace_helpers::load_lockfile_or_crash(ctx, manager); let workspace_pkg_ids: Vec = if !manager.options.filter_patterns.is_empty() { let filters = manager.options.filter_patterns; - Self::find_matching_workspaces(original_cwd, manager, filters) + workspace_helpers::find_matching_workspaces(original_cwd, manager, filters) } else if manager.options.do_.recursive() { - Self::get_all_workspaces(manager) + workspace_helpers::get_all_workspaces(manager) } else { let root_pkg_id = manager .root_package_id @@ -710,113 +661,6 @@ impl UpdateInteractiveCommand { Ok(()) } - fn get_all_workspaces(manager: &PackageManager) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_resolutions = packages.items_resolution(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - workspace_pkg_ids - } - - fn find_matching_workspaces( - original_cwd: &[u8], - manager: &PackageManager, - filters: &[&[u8]], - ) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_names = packages.items_name(); - let pkg_resolutions = packages.items_resolution(); - let string_buf = lockfile.buffers.string_bytes.as_slice(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - - let mut path_buf = PathBuffer::uninit(); - - let converted_filters: Vec = filters - .iter() - .map(|filter| { - WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0).expect("OOM") - }) - .collect(); - // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. - - // SAFETY: `FileSystem::init` ran during `PackageManager::init`. - let top_level_dir = FileSystem::get().top_level_dir; - - // move all matched workspaces to front of array - let mut i: usize = 0; - while i < workspace_pkg_ids.len() { - let workspace_pkg_id = workspace_pkg_ids[i]; - - let matched = 'matched: { - for filter in &converted_filters { - match filter { - WorkspaceFilter::Path(pattern) => { - if pattern.is_empty() { - continue; - } - let res = &pkg_resolutions[workspace_pkg_id as usize]; - let res_path: &[u8] = match res.tag { - resolution::Tag::Workspace => res.workspace().slice(string_buf), - resolution::Tag::Root => top_level_dir, - _ => unreachable!(), - }; - - let abs_res_path = path::resolve_path::join_abs_string_buf::< - path::platform::Posix, - >( - top_level_dir, &mut path_buf.0, &[res_path] - ); - - if !glob::r#match( - pattern, - strings::without_trailing_slash(abs_res_path), - ) - .matches() - { - break 'matched false; - } - } - WorkspaceFilter::Name(pattern) => { - let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); - if !glob::r#match(pattern, name).matches() { - break 'matched false; - } - } - WorkspaceFilter::All => {} - } - } - true - }; - - if matched { - i += 1; - } else { - workspace_pkg_ids.swap_remove(i); - } - } - - workspace_pkg_ids - } - fn group_catalog_dependencies( packages: Vec, ) -> crate::Result> { diff --git a/src/runtime/cli/workspace_helpers.rs b/src/runtime/cli/workspace_helpers.rs new file mode 100644 index 000000000000..f91ef5041606 --- /dev/null +++ b/src/runtime/cli/workspace_helpers.rs @@ -0,0 +1,160 @@ +//! Workspace/lockfile helpers shared by `bun outdated` and +//! `bun update --interactive`. + +use bun_core::strings; +use bun_core::{Global, Output}; +use bun_glob as glob; +use bun_install::lockfile::package::PackageColumns as _; +use bun_install::lockfile::{LoadResult, LoadStep}; +use bun_install::package_manager::{LogLevel, WorkspaceFilter}; +use bun_install::{PackageID, PackageManager, resolution}; +use bun_paths::{self as path, PathBuffer}; +use bun_resolver::fs::FileSystem; + +use crate::Command; + +/// Load the lockfile from the current directory, reporting errors and exiting +/// the process on failure. +pub(crate) fn load_lockfile_or_crash(ctx: &Command::ContextData, manager: &mut PackageManager) { + let not_silent = manager.options.log_level != LogLevel::Silent; + match manager.load_lockfile_from_cwd::() { + LoadResult::NotFound => { + if not_silent { + Output::err_generic("missing lockfile, nothing outdated", ()); + } + Global::crash(); + } + LoadResult::Err(cause) => { + if not_silent { + match cause.step { + LoadStep::OpenFile => { + Output::err_generic("failed to open lockfile: {s}", (cause.value.name(),)); + } + LoadStep::ParseFile => { + Output::err_generic("failed to parse lockfile: {s}", (cause.value.name(),)); + } + LoadStep::ReadFile => { + Output::err_generic("failed to read lockfile: {s}", (cause.value.name(),)); + } + LoadStep::Migrating => { + Output::err_generic( + "failed to migrate lockfile: {s}", + (cause.value.name(),), + ); + } + } + if ctx.log_ref().has_errors() { + let _ = manager + .log_mut() + .print(std::ptr::from_mut(Output::error_writer())); + } + } + Global::crash(); + } + LoadResult::Ok(_) => { + // `load_from_cwd(&mut self, ..)` populates the lockfile in place, + // so no reassignment is needed. + } + } +} + +/// Collect the package IDs of the root package and every workspace package. +pub(crate) fn get_all_workspaces(manager: &PackageManager) -> Vec { + let lockfile = &manager.lockfile; + let packages = lockfile.packages.slice(); + let pkg_resolutions = packages.items_resolution(); + + let mut workspace_pkg_ids: Vec = Vec::new(); + for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { + if resolution.tag != resolution::Tag::Workspace && resolution.tag != resolution::Tag::Root { + continue; + } + workspace_pkg_ids.push(pkg_id as PackageID); + } + workspace_pkg_ids +} + +/// Collect the workspace package IDs matching the `--filter` patterns. +pub(crate) fn find_matching_workspaces( + original_cwd: &[u8], + manager: &PackageManager, + filters: &[&[u8]], +) -> Vec { + let lockfile = &manager.lockfile; + let packages = lockfile.packages.slice(); + let pkg_names = packages.items_name(); + let pkg_resolutions = packages.items_resolution(); + let string_buf = lockfile.buffers.string_bytes.as_slice(); + + let mut workspace_pkg_ids = get_all_workspaces(manager); + + let mut path_buf = PathBuffer::uninit(); + + let converted_filters: Vec = filters + .iter() + .map(|filter| { + bun_core::handle_oom(WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0)) + }) + .collect(); + // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. + + // SAFETY: `FileSystem::init` runs during `PackageManager::init` so the + // process-singleton is populated. + let top_level_dir = FileSystem::get().top_level_dir; + + // move all matched workspaces to front of array + let mut i: usize = 0; + while i < workspace_pkg_ids.len() { + let workspace_pkg_id = workspace_pkg_ids[i]; + + let matched = 'matched: { + for filter in &converted_filters { + match filter { + WorkspaceFilter::Path(pattern) => { + if pattern.is_empty() { + continue; + } + let res = &pkg_resolutions[workspace_pkg_id as usize]; + let res_path: &[u8] = match res.tag { + resolution::Tag::Workspace => { + // Borrow the field in-place so the returned slice (which may + // point into the inline small-string storage) stays valid. + res.workspace().slice(string_buf) + } + resolution::Tag::Root => top_level_dir, + _ => unreachable!(), + }; + + let abs_res_path = path::resolve_path::join_abs_string_buf::< + path::platform::Posix, + >( + top_level_dir, &mut path_buf.0, &[res_path] + ); + + if !glob::r#match(pattern, strings::without_trailing_slash(abs_res_path)) + .matches() + { + break 'matched false; + } + } + WorkspaceFilter::Name(pattern) => { + let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); + if !glob::r#match(pattern, name).matches() { + break 'matched false; + } + } + WorkspaceFilter::All => {} + } + } + true + }; + + if matched { + i += 1; + } else { + workspace_pkg_ids.swap_remove(i); + } + } + + workspace_pkg_ids +} diff --git a/src/runtime/crypto/CryptoHasher.rs b/src/runtime/crypto/CryptoHasher.rs index eb19f059a3f7..1d0a97ba65d2 100644 --- a/src/runtime/crypto/CryptoHasher.rs +++ b/src/runtime/crypto/CryptoHasher.rs @@ -49,6 +49,97 @@ fn is_bun_file_blob(input: &BlobOrStringOrBuffer) -> bool { } } +/// Parsed form of the optional `digest()`/`hash()` output argument: either a +/// caller-provided byte sink (`None` → allocate a fresh buffer), or an +/// encoding name to stringify the digest with. +enum DigestOutput { + Bytes(Option), + Encoding(Encoding), +} + +fn parse_digest_output( + global: &JSGlobalObject, + output: Option, +) -> JsResult { + let Some(string_or_buffer) = output else { + return Ok(DigestOutput::Bytes(None)); + }; + if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { + return Ok(DigestOutput::Bytes(Some(buffer.buffer))); + } + // `inline else => |*str|` — every non-buffer arm yields a string-like + // `defer str.deinit()` — handled by Drop. + let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { + return Err(global + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!( + "Unknown encoding: {}", + bstr::BStr::new(string_or_buffer.slice()) + ), + ) + .throw()); + }; + Ok(DigestOutput::Encoding(encoding)) +} + +/// Hand-expanded `wrapInstanceMethod` decode for the trailing +/// `?Node.StringOrBuffer` parameter (instance-method arm: +/// empty/undefined/null → None). +fn digest_output_argument( + global: &JSGlobalObject, + callframe: &CallFrame, +) -> JsResult> { + let arguments = callframe.arguments_old::<1>(); + if arguments.len > 0 { + let arg = arguments.ptr[0]; + if !arg.is_empty_or_undefined_or_null() { + return match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + }; + } + } + Ok(None) +} + +/// Hand-expanded static-method decode for the `Node.BlobOrStringOrBuffer` +/// input parameter. +fn hash_input_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult { + if let Some(arg) = arg { + if let Some(b) = BlobOrStringOrBuffer::from_js(global, arg)? { + return Ok(b); + } + } + Err(global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))) +} + +/// Hand-expanded static-method decode for the trailing `?Node.StringOrBuffer` +/// output parameter (static-method arm: only `undefined` → None). +fn hash_output_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult> { + match arg { + Some(arg) => match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + if arg.is_undefined() { + Ok(None) + } else { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + } + }, + None => Ok(None), + } +} + /// `union(enum)` → Rust enum with payload variants. /// `.classes.ts`-backed type: the C++ JSCell wrapper stays generated; this is the `m_ctx` payload. /// @@ -218,24 +309,7 @@ impl CryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -243,58 +317,20 @@ impl CryptoHasher { /// `(algorithm string, input, optional output buffer/encoding)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<3>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; let algorithm = { - let Some(string_value) = next_eat() else { + if arguments.len == 0 { return Err(global.throw_invalid_arguments(format_args!("Missing argument"))); - }; + } + let string_value = arguments.ptr[0]; if string_value.is_undefined_or_null() { return Err(global.throw_invalid_arguments(format_args!("Expected string"))); } string_value.get_zig_string(global)? }; - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; + let input = hash_input_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; + let output = hash_output_argument(global, (arguments.len > 2).then(|| arguments.ptr[2]))?; Self::hash_(global, algorithm, &input, output) } @@ -437,28 +473,11 @@ impl CryptoHasher { }; // `defer evp.deinit()` — handled by Drop on `evp`. - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_to_bytes(global, &mut evp, input, Some(ab)); + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_to_bytes(global, &mut evp, input, ab), + DigestOutput::Encoding(encoding) => { + Self::hash_to_encoding(global, &mut evp, input, encoding) } - // `inline else => |*str|` — every non-buffer arm yields a string-like - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - Self::hash_to_encoding(global, &mut evp, input, encoding) - } else { - Self::hash_to_bytes(global, &mut evp, input, None) } } @@ -670,27 +689,9 @@ impl CryptoHasher { global: &JSGlobalObject, output: Option, ) -> JsResult { - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return this.digest_to_bytes(global, Some(ab)); - } - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - this.digest_to_encoding(global, encoding) - } else { - this.digest_to_bytes(global, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => this.digest_to_bytes(global, ab), + DigestOutput::Encoding(encoding) => this.digest_to_encoding(global, encoding), } } @@ -910,30 +911,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( @@ -1197,24 +1183,7 @@ impl StaticCryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -1224,49 +1193,8 @@ impl StaticCryptoHasher { /// `(*JSGlobalObject, Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<2>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; - - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; - + let input = hash_input_argument(global, (arguments.len > 0).then(|| arguments.ptr[0]))?; + let output = hash_output_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; Self::hash_(global, &input, output) } @@ -1304,29 +1232,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. @@ -1358,26 +1294,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), } } @@ -1448,26 +1367,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), } } @@ -1477,23 +1379,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); @@ -1501,7 +1388,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 9409a40ba3e0..fb0c0bfa2e53 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -811,6 +811,40 @@ pub(crate) fn js_password_object_hash_sync( // ─── verify host functions ──────────────────────────────────────────────── +/// Parse the optional third `verify(password, hash, algorithm)` argument. +fn parse_verify_algorithm( + global_object: &JSGlobalObject, + arguments: &[JSValue], +) -> JsResult> { + let Some(&arg) = arguments.get(2) else { + return Ok(None); + }; + + if arg.is_empty_or_undefined_or_null() { + return Ok(None); + } + + if !arg.is_string() { + return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); + } + + let algorithm_string = arg.get_zig_string(global_object)?; + + match algorithm_from_zig_string(&algorithm_string) { + Some(a) => Ok(Some(a)), + None => { + if !global_object.has_exception() { + return Err(global_object.throw_invalid_argument_type( + "verify", + "algorithm", + UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, + )); + } + Err(JsError::Thrown) + } + } +} + // Once we have bindings generator, this should be replaced with a generated function #[bun_jsc::host_fn] pub(crate) fn js_password_object_verify( @@ -824,29 +858,7 @@ pub(crate) fn js_password_object_verify( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Err(JsError::Thrown); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; // TODO: this most likely should error like `verifySync` instead of stringifying. // @@ -905,29 +917,7 @@ pub(crate) fn js_password_object_verify_sync( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Ok(JSValue::ZERO); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; let Some(password) = StringOrBuffer::from_js(global_object, arguments[0])? else { return Err(global_object.throw_invalid_argument_type( diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 58100a8f6c88..20d34a9ea832 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -602,6 +602,78 @@ pub(super) extern "C" fn napi_create_int64( env.ok() } +/// Code-unit type accepted by the `napi_create_string_*` entry points; selects +/// how a `NAPI_AUTO_LENGTH` (NUL-terminated) input is measured. +trait NapiStringUnit: Copy { + /// # Safety + /// `ptr` must be non-null and point to a NUL-terminated sequence. + unsafe fn cstr_units<'a>(ptr: *const Self) -> &'a [Self]; +} + +impl NapiStringUnit for u8 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u8) -> &'a [u8] { + // SAFETY: forwarded caller contract. + unsafe { bun_core::ffi::cstr(ptr.cast::()) }.to_bytes() + } +} + +impl NapiStringUnit for u16 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u16) -> &'a [u16] { + // SAFETY: forwarded caller contract. Scans to the NUL u16 terminator. + unsafe { bun_core::ffi::wstr_units(ptr) } + } +} + +/// Shared argument-validation prologue for the `napi_create_string_*` entry +/// points: extracts the source code units, or `Err(())` when the arguments +/// are invalid (caller returns `env.invalid_arg()`). +/// +/// # Safety +/// When `str_` is non-null, the NAPI caller contract must hold: if `length == +/// NAPI_AUTO_LENGTH`, `str_` points to a NUL-terminated sequence; otherwise +/// `[str_, str_ + length)` must be readable. The returned borrow has an +/// unconstrained lifetime and must not outlive the caller's buffer. +#[inline(always)] +unsafe fn napi_string_slice<'a, T: NapiStringUnit>( + str_: *const T, + length: usize, +) -> Result<&'a [T], ()> { + if !str_.is_null() { + if NAPI_AUTO_LENGTH == length { + // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. + Ok(unsafe { T::cstr_units(str_) }) + } else if length > i32::MAX as usize { + Err(()) + } else { + // SAFETY: caller guarantees [ptr, ptr+length) is valid. + Ok(unsafe { bun_core::ffi::slice(str_, length) }) + } + } else if length == 0 { + Ok(&[]) + } else { + Err(()) + } +} + +/// Writes a converted string's `to_js` result through the out-param, mapping +/// conversion failure to `generic_failure`. +#[inline(always)] +fn set_string_result( + env: &NapiEnv, + result: &mut napi_value, + js: jsc::JsResult, +) -> napi_status { + match js { + Ok(v) => { + result.set(env, v); + env.ok() + } + Err(_) => NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), + } +} + #[unsafe(no_mangle)] pub(super) extern "C" fn napi_create_string_latin1( env_: napi_env, @@ -612,24 +684,11 @@ pub(super) extern "C" fn napi_create_string_latin1( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!( @@ -639,23 +698,13 @@ pub(super) extern "C" fn napi_create_string_latin1( ); if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (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)] @@ -668,24 +717,11 @@ pub(super) extern "C" fn napi_create_string_utf8( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!(napi, "napi_create_string_utf8: {}", bstr::BStr::new(slice)); @@ -709,25 +745,11 @@ pub(super) extern "C" fn napi_create_string_utf16( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u16] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - // Scan to the NUL u16 terminator. - break 'brk unsafe { bun_core::ffi::wstr_units(str_) }; - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; if cfg!(debug_assertions) { @@ -740,23 +762,13 @@ pub(super) extern "C" fn napi_create_string_utf16( } if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (mut string, chars) = bun_core::String::create_uninitialized_utf16(slice.len()); chars.copy_from_slice(slice); - let js = match string.transfer_to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - env.ok() + set_string_result(env, result, string.transfer_to_js(env.to_js())) } // Implemented in C++ (napi.cpp); declared extern here for Rust-side callers. diff --git a/src/runtime/server/ServerWebSocket.rs b/src/runtime/server/ServerWebSocket.rs index d1858f15e7ba..cd6613b9f476 100644 --- a/src/runtime/server/ServerWebSocket.rs +++ b/src/runtime/server/ServerWebSocket.rs @@ -221,11 +221,9 @@ impl ServerWebSocket { // guard on `compress` even when compress is args[2] (long-standing // user-visible behavior; do not "fix"). // - // A unified `publish_prologue` covering the full callframe header was - // considered and rejected: publishText omits the empty-topic check and - // reuses "publish" in its min-args message (both user-visible), so a single - // prologue would either change user-visible errors or carry per-caller - // bool flags — net more code than three small orthogonal helpers. + // `publish_prologue` parameterizes the two per-method divergences: + // publishText omits the empty-topic check and reuses "publish" in its + // min-args message and debug logs (both user-visible; do not "fix"). // ────────────────────────────────────────────────────────────────────── /// `(app, ssl, publish_to_self)` from the handler, or `None` when the @@ -264,6 +262,60 @@ impl ServerWebSocket { Ok(args_len > 1 && compress_value.to_boolean()) } + /// Shared prologue for `publish`/`publishText`/`publishBinary`: min-arity + /// check, closed-server check, topic validation, and compress parsing. + /// `Ok(None)` means the server is closed (caller returns `0`). + /// + /// `fn_name` is the method name used in error messages; `log_name` is the + /// name used in debug logs and the min-args message (`publishText` reports + /// "publish" there) and `require_non_empty_topic` is `false` only for + /// `publishText` — both long-standing user-visible behavior; do not "fix". + #[inline] + fn publish_prologue( + &self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + fn_name: &'static str, + log_name: &'static str, + require_non_empty_topic: bool, + ) -> JsResult> { + let args = callframe.arguments_old::<4>(); + if args.len < 1 { + bun_output::scoped_log!(WebSocketServer, "{}()", log_name); + return Err(global_this.throw(format_args!("{log_name} requires at least 1 argument"))); + } + + let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { + bun_output::scoped_log!(WebSocketServer, "publish() closed"); + return Ok(None); + }; + + let topic_value = args.ptr[0]; + let message_value = args.ptr[1]; + let compress_value = args.ptr[2]; + + if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { + bun_output::scoped_log!(WebSocketServer, "{}() topic invalid", log_name); + return Err(global_this.throw(format_args!("{fn_name} requires a topic string"))); + } + + let topic_slice = topic_value.to_slice(global_this)?; + if require_non_empty_topic && topic_slice.slice().is_empty() { + return Err(global_this.throw(format_args!("{fn_name} requires a non-empty topic"))); + } + + let compress = Self::parse_compress_arg(global_this, fn_name, compress_value, args.len)?; + + Ok(Some(( + app, + ssl, + publish_to_self, + topic_slice, + message_value, + compress, + ))) + } + /// Route a publish through either the per-socket uWS handle (when /// `!publish_to_self && !closed`) or the app-wide broadcast, then map the /// aggregated `SendStatus` to the JS number contract shared with `send()`. @@ -759,33 +811,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publish", "publish", true)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publish requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publish requires a non-empty topic"))); - } - - let compress = Self::parse_compress_arg(global_this, "publish", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() { return Err(global_this.throw(format_args!("publish requires a non-empty message"))); } @@ -828,32 +859,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publishText", "publish", false)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publishText requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - - let compress = - Self::parse_compress_arg(global_this, "publishText", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() || !message_value.is_string() { return Err(global_this.throw(format_args!("publishText requires a non-empty message"))); } @@ -881,35 +892,17 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publishBinary()"); - return Err( - global_this.throw(format_args!("publishBinary requires at least 1 argument")) - ); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = self + .publish_prologue( + global_this, + callframe, + "publishBinary", + "publishBinary", + true, + )? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publishBinary() topic invalid"); - return Err(global_this.throw(format_args!("publishBinary requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publishBinary requires a non-empty topic"))); - } - - let compress = - Self::parse_compress_arg(global_this, "publishBinary", compress_value, args.len)?; if message_value.is_empty_or_undefined_or_null() { return Err( diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index a4452f57bef5..71779c600ce3 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -870,6 +870,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 @@ -923,16 +946,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 @@ -1594,16 +1599,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`. @@ -2068,7 +2064,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) => { @@ -2077,15 +2072,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 19496c6ac5fa..341e342adb2b 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -7,8 +7,8 @@ use bun_jsc::JsCell; use bun_jsc::array_buffer::BinaryType; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, MarkedArgumentBuffer, Ref as JscRef, - StringJsc, SysErrorJsc, SystemError, + CallFrame, JSGlobalObject, JSValue, JsError, JsRef, JsResult, MarkedArgumentBuffer, + Ref as JscRef, StringJsc, SysErrorJsc, SystemError, }; use bun_ptr::BackRef; @@ -686,63 +686,38 @@ impl UDPSocket { event_loop.exit(); } - #[bun_jsc::host_fn(method)] - pub fn set_broadcast( - this: &Self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) + fn throw_setsockopt_errno(global_this: &JSGlobalObject, errno: SystemErrno) -> JsError { + global_this.throw_value( + bun_sys::Error::from_code_int(errno as c_int, bun_sys::Tag::setsockopt) .to_js(global_this), - )); - } - - let arguments = callframe.arguments(); - if arguments.len() < 1 { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected 1 argument, got {}", - arguments.len() - ))); - } - - let enabled = arguments[0].to_boolean(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), - )); - }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_broadcast(enabled); + ) + } + fn check_setsockopt(global_this: &JSGlobalObject, res: c_int) -> JsResult<()> { if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); } + Ok(()) + } - Ok(arguments[0]) + fn require_socket(&self, global_this: &JSGlobalObject) -> JsResult<&mut uws::udp::Socket> { + let Some(socket) = self.socket.get() else { + return Err(global_this.throw(format_args!("Socket is closed"))); + }; + // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. + Ok(uws::udp::Socket::opaque_mut(socket)) } - #[bun_jsc::host_fn(method)] - pub fn set_multicast_loopback( + fn set_bool_opt( this: &Self, global_this: &JSGlobalObject, callframe: &CallFrame, + function: fn(&mut uws::udp::Socket, bool) -> c_int, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -760,24 +735,46 @@ impl UDPSocket { // test-dgram-multicast-loopback.js). Throw EBADF to match the // `closed` branch above instead of panicking. let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); }; // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_loopback(enabled); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let res = function(uws::udp::Socket::opaque_mut(socket), enabled); + Self::check_setsockopt(global_this, res)?; Ok(arguments[0]) } + #[bun_jsc::host_fn(method)] + pub fn set_broadcast( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_broadcast, + ) + } + + #[bun_jsc::host_fn(method)] + pub fn set_multicast_loopback( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_multicast_loopback, + ) + } + fn set_membership( this: &Self, global_this: &JSGlobalObject, @@ -785,12 +782,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, )); } @@ -809,20 +803,15 @@ impl UDPSocket { arguments[0], &mut addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } let mut interface: sockaddr_storage = bun_core::ffi::zeroed(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; + let socket = this.require_socket(global_this)?; let res = if arguments.len() > 1 && this.parse_addr( @@ -836,15 +825,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) } @@ -874,12 +860,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, )); } @@ -901,12 +884,9 @@ impl UDPSocket { arguments[0], &mut source_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -917,12 +897,9 @@ impl UDPSocket { arguments[1], &mut group_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -934,9 +911,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( @@ -950,25 +925,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) } @@ -998,12 +960,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, )); } @@ -1033,16 +992,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) } @@ -1082,12 +1034,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, )); } @@ -1100,15 +1049,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/valkey_jsc/js_valkey_functions.rs b/src/runtime/valkey_jsc/js_valkey_functions.rs index d77eabb00ecb..fad44d045908 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 `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,130 +212,17 @@ macro_rules! cmd_key_varargs { }; } -macro_rules! cmd_key_value { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } +macro_rules! cmd_varargs { + ($fn_name:ident, $name:literal, $command:literal, required $arg0_name:literal, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state, $arg0_name); }; -} - -macro_rules! cmd_key_value_value2 { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $arg2_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - let Some(value2) = from_js(global, frame.argument(2))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg2_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value, value2]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, skip_null, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state); }; -} - -macro_rules! cmd_strings_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let mut args: Vec = Vec::with_capacity(frame.arguments().len()); - - for arg in frame.arguments() { - let Some(another) = from_js(global, *arg)? else { - return Err(global.throw_invalid_argument_type( - bname($name), - "additional arguments", - "string or buffer", - )); - }; - args.push(another); - } - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&args), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, strict, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, false, $state); }; -} - -macro_rules! cmd_key_value_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { + (@impl $fn_name:ident, $name:literal, $command:literal, $skip_null:literal, $state:ident $(, $arg0_name:literal)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -404,11 +233,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 { @@ -1117,27 +955,45 @@ impl JSValkeyClient { Self::hset_impl(this, global, frame, b"HMSET") } - cmd_key_varargs!(hdel, b"hdel", "HDEL", "key", NotSubscriber); - cmd_key_varargs!( + cmd_varargs!(hdel, b"hdel", "HDEL", required "key", NotSubscriber); + cmd_varargs!( hrandfield, b"hrandfield", "HRANDFIELD", - "key", + required "key", + NotSubscriber + ); + cmd_varargs!(hscan, b"hscan", "HSCAN", required "key", NotSubscriber); + cmd_varargs!(hgetdel, b"hgetdel", "HGETDEL", strict, NotSubscriber); + cmd_varargs!(hgetex, b"hgetex", "HGETEX", strict, NotSubscriber); + cmd_varargs!(hsetex, b"hsetex", "HSETEX", strict, NotSubscriber); + cmd_varargs!(hexpire, b"hexpire", "HEXPIRE", strict, NotSubscriber); + cmd_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", strict, NotSubscriber); + cmd_varargs!( + hexpiretime, + b"hexpiretime", + "HEXPIRETIME", + strict, NotSubscriber ); - cmd_key_varargs!(hscan, b"hscan", "HSCAN", "key", NotSubscriber); - cmd_strings_varargs!(hgetdel, b"hgetdel", "HGETDEL", NotSubscriber); - cmd_strings_varargs!(hgetex, b"hgetex", "HGETEX", NotSubscriber); - cmd_strings_varargs!(hsetex, b"hsetex", "HSETEX", NotSubscriber); - cmd_strings_varargs!(hexpire, b"hexpire", "HEXPIRE", NotSubscriber); - cmd_strings_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hexpiretime, b"hexpiretime", "HEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpersist, b"hpersist", "HPERSIST", NotSubscriber); - cmd_strings_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", NotSubscriber); - cmd_strings_varargs!(hpexpireat, b"hpexpireat", "HPEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hpexpiretime, b"hpexpiretime", "HPEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpttl, b"hpttl", "HPTTL", NotSubscriber); - cmd_strings_varargs!(httl, b"httl", "HTTL", NotSubscriber); + cmd_varargs!(hpersist, b"hpersist", "HPERSIST", strict, NotSubscriber); + cmd_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", strict, NotSubscriber); + cmd_varargs!( + hpexpireat, + b"hpexpireat", + "HPEXPIREAT", + strict, + NotSubscriber + ); + cmd_varargs!( + hpexpiretime, + b"hpexpiretime", + "HPEXPIRETIME", + strict, + NotSubscriber + ); + cmd_varargs!(hpttl, b"hpttl", "HPTTL", strict, NotSubscriber); + cmd_varargs!(httl, b"httl", "HTTL", strict, NotSubscriber); #[bun_jsc::host_fn(method)] pub fn hsetnx(this: &Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { @@ -1216,12 +1072,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", @@ -1230,8 +1086,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", @@ -1240,7 +1096,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( getrange, b"getrange", "GETRANGE", @@ -1249,7 +1105,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value_value2!( + cmd!( setrange, b"setrange", "SETRANGE", @@ -1258,8 +1114,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", @@ -1267,28 +1123,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", @@ -1297,7 +1153,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lrem, b"lrem", "LREM", @@ -1306,7 +1162,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lset, b"lset", "LSET", @@ -1315,7 +1171,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( ltrim, b"ltrim", "LTRIM", @@ -1324,8 +1180,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", @@ -1333,7 +1189,7 @@ impl JSValkeyClient { "milliseconds", NotSubscriber ); - cmd_key_value!( + cmd!( pexpireat, b"pexpireat", "PEXPIREAT", @@ -1341,17 +1197,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", @@ -1359,21 +1215,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", @@ -1382,7 +1268,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zlexcount, b"zlexcount", "ZLEXCOUNT", @@ -1391,47 +1277,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", @@ -1439,7 +1327,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( incrbyfloat, b"incrbyfloat", "INCRBYFLOAT", @@ -1447,7 +1335,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( decrby, b"decrby", "DECRBY", @@ -1455,13 +1343,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", @@ -1470,7 +1358,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( psetex, b"psetex", "PSETEX", @@ -1479,8 +1367,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", @@ -1489,27 +1377,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", @@ -1546,7 +1458,7 @@ impl JSValkeyClient { ) } - cmd_key_value_value2!( + cmd!( substr, b"substr", "SUBSTR", @@ -1555,7 +1467,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value!( + cmd!( hstrlen, b"hstrlen", "HSTRLEN", @@ -1563,10 +1475,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", @@ -1575,7 +1493,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyrank, b"zremrangebyrank", "ZREMRANGEBYRANK", @@ -1584,7 +1502,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyscore, b"zremrangebyscore", "ZREMRANGEBYSCORE", @@ -1593,15 +1511,27 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zrevrank, b"zrevrank", "ZREVRANK", "key", NotSubscriber); - cmd_strings_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", DontCare); - cmd_strings_varargs!(punsubscribe, b"punsubscribe", "PUNSUBSCRIBE", DontCare); - cmd_strings_varargs!(pubsub, b"pubsub", "PUBSUB", DontCare); - cmd_strings_varargs!(copy, b"copy", "COPY", NotSubscriber); - cmd_key_varargs!(unlink, b"unlink", "UNLINK", "key", NotSubscriber); - cmd_key_varargs!(touch, b"touch", "TOUCH", "key", NotSubscriber); - cmd_key_value!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); - cmd_key_value!( + cmd_varargs!( + zrevrank, + b"zrevrank", + "ZREVRANK", + required "key", + NotSubscriber + ); + cmd_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", strict, DontCare); + cmd_varargs!( + punsubscribe, + b"punsubscribe", + "PUNSUBSCRIBE", + strict, + DontCare + ); + cmd_varargs!(pubsub, b"pubsub", "PUBSUB", strict, DontCare); + cmd_varargs!(copy, b"copy", "COPY", strict, NotSubscriber); + cmd_varargs!(unlink, b"unlink", "UNLINK", required "key", NotSubscriber); + cmd_varargs!(touch, b"touch", "TOUCH", required "key", NotSubscriber); + cmd!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); + cmd!( renamenx, b"renamenx", "RENAMENX", diff --git a/src/semver/Version.rs b/src/semver/Version.rs index 286705e86f84..7e8e2e99615d 100644 --- a/src/semver/Version.rs +++ b/src/semver/Version.rs @@ -103,10 +103,6 @@ impl VersionType { impl VersionType { /// Assumes that there is only one buffer for all the strings - pub fn sort_gt(ctx: &[u8], lhs: Self, rhs: Self) -> bool { - Self::order_fn(ctx, lhs, rhs) == Ordering::Greater - } - pub fn order_fn(ctx: &[u8], lhs: Self, rhs: Self) -> Ordering { lhs.order(rhs, ctx, ctx) } diff --git a/src/semver/lib.rs b/src/semver/lib.rs index 3e0fd6290102..a2701ae3c63f 100644 --- a/src/semver/lib.rs +++ b/src/semver/lib.rs @@ -321,11 +321,35 @@ pub mod semver_string { const MAX_INLINE_LEN_M1: usize = String::MAX_INLINE_LEN - 1; match buf.len() { 0..=MAX_INLINE_LEN_M1 => true, + // A string of exactly MAX_INLINE_LEN bytes is only inlinable when its + // final byte's top bit is clear — that bit distinguishes inline data + // from a packed pointer. Non-ASCII strings of exactly 8 bytes hit this. Self::MAX_INLINE_LEN => buf[Self::MAX_INLINE_LEN - 1] & 0x80 == 0, _ => false, } } + /// Caller must ensure `in_.len() <= MAX_INLINE_LEN`. + /// Unrolled per length so this compiles to direct loads instead of a + /// memcpy libcall — short strings dominate lockfile/install parsing. + #[inline] + fn inline_bytes(in_: &[u8]) -> [u8; Self::MAX_INLINE_LEN] { + match in_.len() { + 0 => [0, 0, 0, 0, 0, 0, 0, 0], + 1 => [in_[0], 0, 0, 0, 0, 0, 0, 0], + 2 => [in_[0], in_[1], 0, 0, 0, 0, 0, 0], + 3 => [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], + 4 => [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], + 5 => [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], + 6 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], + 7 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], + Self::MAX_INLINE_LEN => [ + in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], + ], + _ => unreachable!(), + } + } + #[inline] pub fn is_inline(self) -> bool { self.bytes[Self::MAX_INLINE_LEN - 1] & 0x80 == 0 @@ -347,133 +371,34 @@ pub mod semver_string { // bun_install (or bun_install_types) as inherent helpers there. pub fn init(buf: &[u8], in_: &[u8]) -> String { - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } + if Self::can_inline(in_) { + String { + bytes: Self::inline_bytes(in_), } - _ => { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } + } else { + let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); + let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); + String { + bytes: packed.to_ne_bytes(), } } } pub fn init_inline(in_: &[u8]) -> String { debug_assert!(Self::can_inline(in_)); - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - 8 => String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - }, - _ => unreachable!(), + String { + bytes: Self::inline_bytes(in_), } } pub fn init_append_if_needed(buf: &mut Vec, in_: &[u8]) -> Result { - Ok(match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - Self::init_append(buf, in_)? - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } - } - - _ => Self::init_append(buf, in_)?, - }) + if Self::can_inline(in_) { + Ok(String { + bytes: Self::inline_bytes(in_), + }) + } else { + Self::init_append(buf, in_) + } } pub fn init_append(buf: &mut Vec, in_: &[u8]) -> Result { diff --git a/src/sourcemap/Chunk.rs b/src/sourcemap/Chunk.rs index 7af834e8fa3c..e04e51f815f3 100644 --- a/src/sourcemap/Chunk.rs +++ b/src/sourcemap/Chunk.rs @@ -55,20 +55,6 @@ impl Chunk { unsafe { core::ptr::read(self) } } - pub fn print_source_map_contents( - &self, - source: &Source, - mutable: &mut MutableString, - include_sources_contents: bool, - ) -> Result<(), crate::Error> { - print_source_map_contents_json::( - source, - mutable, - include_sources_contents, - self.buffer.list.as_slice(), - ) - } - /// `chunk.buffer` holds an InternalSourceMap blob (the runtime path). Re-encode /// to a standard VLQ "mappings" string before emitting JSON. pub fn print_source_map_contents_from_internal( diff --git a/src/sourcemap/lib.rs b/src/sourcemap/lib.rs index bd4f20bfd85f..827e4106dfab 100644 --- a/src/sourcemap/lib.rs +++ b/src/sourcemap/lib.rs @@ -1215,22 +1215,3 @@ fn find_source_mapping_url_u16(source: &[u16]) -> Option( - origin: &bun_url::URL<'_>, - source: &bun_ast::Source, - asset_prefix_path: &[u8], - writer: &mut W, -) -> bun_io::Result<()> { - writer.write_all(b"\n//# sourceMappingURL=")?; - writer.write_all(bun_core::strings::without_trailing_slash(origin.href))?; - if !asset_prefix_path.is_empty() { - writer.write_all(asset_prefix_path)?; - } - if !source.path.pretty.is_empty() && source.path.pretty[0] != b'/' { - writer.write_all(b"/")?; - } - writer.write_all(source.path.pretty)?; - writer.write_all(b".map")?; - Ok(()) -} diff --git a/src/sourcemap_jsc/CodeCoverage.rs b/src/sourcemap_jsc/CodeCoverage.rs index 4211602d374e..4b79e381f836 100644 --- a/src/sourcemap_jsc/CodeCoverage.rs +++ b/src/sourcemap_jsc/CodeCoverage.rs @@ -516,6 +516,25 @@ impl ByteRangeMapping { let line_count: u32; + // Resolves a byte offset to a zero-based (line, column) pair, or `None` + // when the offset does not land strictly after a known line start. + let resolve_line = |byte_offset: usize| -> Option<(u32, usize)> { + let new_line_index = LineOffsetTable::find_index( + line_starts, + Loc { + start: i32::try_from(byte_offset).expect("int cast"), + }, + )?; + let line_start_byte_offset = line_starts[new_line_index]; + if (line_start_byte_offset as usize) >= byte_offset { + return None; + } + Some(( + u32::try_from(new_line_index).expect("int cast"), + byte_offset.saturating_sub(line_start_byte_offset as usize), + )) + }; + if ignore_sourcemap || parsed_mappings_.is_none() { line_count = line_starts.len() as u32; executable_lines = Bitset::init_empty(line_count as usize)?; @@ -538,20 +557,9 @@ impl ByteRangeMapping { let has_executed = block.has_executed || block.execution_count > 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((line, _)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { - continue; - } - - let line: u32 = u32::try_from(new_line_index).expect("int cast"); min_line = min_line.min(line); max_line = max_line.max(line); @@ -587,20 +595,9 @@ impl ByteRangeMapping { let mut max_line: u32 = 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((line, _)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { - continue; - } - - let line: u32 = u32::try_from(new_line_index).expect("int cast"); min_line = min_line.min(line); max_line = max_line.max(line); } @@ -636,6 +633,26 @@ impl ByteRangeMapping { let mut cur_: Option = parsed_mapping.internal_cursor(); + // Maps a generated (line, column) to the original zero-based line, + // or `None` when no in-range original mapping exists. + let mut map_to_original = |line: u32, column: usize| -> Option { + let generated_line = + Ordinal::from_zero_based(i32::try_from(line).expect("int cast")); + let generated_column = + Ordinal::from_zero_based(i32::try_from(column).expect("int cast")); + let point: bun_sourcemap::Mapping = if let Some(c) = cur_.as_mut() { + c.move_to(generated_line, generated_column) + } else { + parsed_mapping.find_mapping(generated_line, generated_column) + }?; + if point.original.lines.zero_based() < 0 { + return None; + } + let original_line: u32 = + u32::try_from(point.original.lines.zero_based()).expect("int cast"); + (original_line < line_count).then_some(original_line) + }; + for (i, block) in blocks.iter().enumerate() { if block.end_offset < 0 || block.start_offset < 0 { continue; // does not map to anything @@ -650,60 +667,21 @@ impl ByteRangeMapping { let has_executed = block.has_executed || block.execution_count > 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((generated_line, column_position)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { + let Some(line) = map_to_original(generated_line, column_position) else { continue; - } - let column_position = - byte_offset.saturating_sub(line_start_byte_offset as usize); - - let found: Option = if let Some(c) = cur_.as_mut() { - c.move_to( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) - } else { - parsed_mapping.find_mapping( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) }; - if let Some(point) = found.as_ref() { - if point.original.lines.zero_based() < 0 { - continue; - } - - let line: u32 = - u32::try_from(point.original.lines.zero_based()).expect("int cast"); - if line >= line_count { - continue; - } - executable_lines.set(line as usize); - if has_executed { - lines_which_have_executed.set(line as usize); - line_hits_slice[line as usize] += 1; - } - - min_line = min_line.min(line); - max_line = max_line.max(line); + executable_lines.set(line as usize); + if has_executed { + lines_which_have_executed.set(line as usize); + line_hits_slice[line as usize] += 1; } + + min_line = min_line.min(line); + max_line = max_line.max(line); } if min_line != u32::MAX { @@ -731,54 +709,14 @@ impl ByteRangeMapping { let mut max_line: u32 = 0; for byte_offset in min..max { - let Some(new_line_index) = LineOffsetTable::find_index( - line_starts, - Loc { - start: i32::try_from(byte_offset).expect("int cast"), - }, - ) else { + let Some((generated_line, column_position)) = resolve_line(byte_offset) else { continue; }; - let line_start_byte_offset = line_starts[new_line_index]; - if (line_start_byte_offset as usize) >= byte_offset { + let Some(line) = map_to_original(generated_line, column_position) else { continue; - } - - let column_position = - byte_offset.saturating_sub(line_start_byte_offset as usize); - - let found: Option = if let Some(c) = cur_.as_mut() { - c.move_to( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) - } else { - parsed_mapping.find_mapping( - Ordinal::from_zero_based( - i32::try_from(new_line_index).expect("int cast"), - ), - Ordinal::from_zero_based( - i32::try_from(column_position).expect("int cast"), - ), - ) }; - if let Some(point) = found { - if point.original.lines.zero_based() < 0 { - continue; - } - - let line: u32 = - u32::try_from(point.original.lines.zero_based()).expect("int cast"); - if line >= line_count { - continue; - } - min_line = min_line.min(line); - max_line = max_line.max(line); - } + min_line = min_line.min(line); + max_line = max_line.max(line); } // no sourcemaps? ignore it diff --git a/src/threading/WaitGroup.rs b/src/threading/WaitGroup.rs index 584fea64aaa7..256107994e49 100644 --- a/src/threading/WaitGroup.rs +++ b/src/threading/WaitGroup.rs @@ -31,10 +31,6 @@ impl WaitGroup { } } - pub fn add_unsynchronized(&mut self, n: usize) { - *self.raw_count.get_mut() += n; - } - pub fn add(&self, n: usize) { // Not Acquire because we don't need to synchronize with other tasks (each runs independently). // Not Release because there are no side effects that other threads depend on when they see diff --git a/src/threading/channel.rs b/src/threading/channel.rs index 1c6738a8772b..f268378274a8 100644 --- a/src/threading/channel.rs +++ b/src/threading/channel.rs @@ -58,14 +58,6 @@ impl<'a, T: Copy> Channel> { } } -impl Channel> { - #[inline] - pub fn init_dynamic() -> Self { - // No allocator param; this non-AST crate uses the global mimalloc. - Self::with_buffer(LinearFifo::>::init()) - } -} - // `T: Copy` because `LinearFifo::write`/`read` are slice-copy based. All // in-tree channel payloads are POD; revisit if a non-`Copy` T appears. impl> Channel { @@ -89,11 +81,6 @@ impl> Channel { self.getters.broadcast(); } - pub fn try_write_item(&self, item: T) -> Result { - let wrote = self.write(core::slice::from_ref(&item))?; - Ok(wrote == 1) - } - pub fn write_item(&self, item: T) -> Result<(), ChannelError> { self.write_all(core::slice::from_ref(&item)) } diff --git a/src/threading/guarded.rs b/src/threading/guarded.rs index 180b4936ada1..62173867a3bf 100644 --- a/src/threading/guarded.rs +++ b/src/threading/guarded.rs @@ -78,15 +78,6 @@ impl GuardedBy { None } } - - /// Borrow the underlying raw [`Mutex`]. Needed by callers that split - /// `lock()`/`unlock()` across function boundaries (e.g. `Progress.rs` - /// porting `lock_api::RawMutex`) or pair this `Guarded` with a bare - /// [`Condition::wait`](crate::Condition::wait). - #[inline] - pub fn raw_mutex(&self) -> &Mutex { - &self.mutex - } } impl GuardedBy { diff --git a/src/threading/work_pool.rs b/src/threading/work_pool.rs index 8980a8154971..60f4a0c98fe6 100644 --- a/src/threading/work_pool.rs +++ b/src/threading/work_pool.rs @@ -146,10 +146,6 @@ impl WorkPool { POOL.get_or_init(create) } - pub fn schedule_batch(batch: Batch) { - Self::get().schedule(batch); - } - pub fn schedule(task: *mut Task) { Self::get().schedule(Batch::from(task)); } diff --git a/src/url/Cargo.toml b/src/url/Cargo.toml index 3ecf09213d22..5b912414efaf 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -21,6 +21,7 @@ libc.workspace = true bitflags.workspace = true bun_alloc.workspace = true bun_core.workspace = true +bun_opaque.workspace = true bun_collections.workspace = true # TODO(b1): bun_io gated — crate does not compile yet; local Write stub in lib.rs # bun_io.workspace = true diff --git a/src/url/lib.rs b/src/url/lib.rs index 2e588eb83834..fc6feb5ed2f9 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -45,11 +45,12 @@ 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 `*mut URL`. + /// Construct via `from_string`/`from_utf8`; free via `deinit`. + /// `!Send`/`!Sync` per the macro: WTF::URL holds non-atomically-refcounted + /// WTF::Strings, so the handle must stay on the thread that created it. + pub struct URL; } // Getters take `*const URL` — the C++ side (BunString.cpp) never mutates the @@ -125,12 +126,22 @@ pub mod whatwg { } impl URL { - pub fn from_string(str: &String) -> Option> { - let mut input = *str; + // `from_string`/`from_utf8` return an owned C++ heap pointer that the + // caller must free exactly once via `deinit`/`destroy`. + pub fn from_string(str: String) -> Option> { + let mut input = str; URL__fromString(&mut input) } 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`] helper. + pub fn file_url_from_string(str: String) -> String { + file_url_from_string(&str) + } + /// By-value form of the free [`path_from_file_url`] helper. + pub fn path_from_file_url(str: String) -> String { + path_from_file_url(&str) } /// Includes the leading '#'. pub fn hash(&self) -> String { @@ -188,6 +199,15 @@ pub mod whatwg { pub fn deinit(&mut self) { URL__deinit(self) } + /// Raw-pointer form of [`URL::deinit`]. + /// + /// # Safety + /// `this` must be a live heap pointer from `from_string`/`from_utf8` + /// (or the C++ side), freed exactly once. + pub unsafe fn destroy(this: *mut Self) { + // SAFETY: caller guarantees `this` is valid and uniquely owned. + unsafe { URL__deinit(&mut *this) } + } } } // Re-export the free helpers at crate root so lower-tier callers can write diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5bf0cca96d9e..917da88a3a9b 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -8257,6 +8257,21 @@ describe("outdated", () => { expect(rest).toMatchSnapshot(); }); } + test("errors without a lockfile", async () => { + await write(packageJson, JSON.stringify({ name: "no-lockfile", version: "1.0.0" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain("error: missing lockfile, nothing outdated"); + expect(exitCode).toBe(1); + }); test("in workspace", async () => { await Promise.all([ write( diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 76438f8dadf1..209aa1b86563 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -472,3 +472,37 @@ it("should link dependency without crashing", async () => { // This should fail with a non-zero exit code. expect(await exited4).toBe(1); }); + +for (const command of ["link", "unlink"]) { + it(`should error when ${command}ing a package without a name`, async () => { + await writeFile(join(link_dir, "package.json"), JSON.stringify({ version: "0.0.1" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), command], + cwd: link_dir, + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env, + }); + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain('error: package.json missing "name"'); + expect(exitCode).toBe(1); + }); +} + +it("should error when linking a package with an invalid name", async () => { + await writeFile(join(link_dir, "package.json"), JSON.stringify({ name: "NOT a valid name!", version: "0.0.1" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link"], + cwd: link_dir, + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env, + }); + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain('error: invalid package.json name "NOT a valid name!"'); + expect(exitCode).toBe(1); +}); diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 370d9c0e1b23..09192597ccd7 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -51,6 +51,13 @@ test("basic", async () => { expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]); }); +test("fails when package.json cannot be parsed", async () => { + await write(join(packageDir, "package.json"), '{"name": "pack-bad-json",'); + + const { err } = await packExpectError(packageDir, bunEnv); + expect(err).toContain(`failed to parse package.json: ${join(packageDir, "package.json")}`); +}); + test("in subdirectory", async () => { await Promise.all([ write( diff --git a/test/cli/update_interactive_install.test.ts b/test/cli/update_interactive_install.test.ts index 8392d7cae5ed..27f2ac4c9aa1 100644 --- a/test/cli/update_interactive_install.test.ts +++ b/test/cli/update_interactive_install.test.ts @@ -201,3 +201,24 @@ describe.concurrent("bun update --interactive actually installs packages", () => } }); }); + +describe.concurrent("bun update --interactive error handling", () => { + test("errors without a lockfile", async () => { + using dir = tempDir("update-interactive-no-lockfile", { + "package.json": JSON.stringify({ name: "no-lockfile", version: "1.0.0" }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "update", "--interactive"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + + const [stderr, _stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); + expect(stderr).toContain("error: missing lockfile, nothing outdated"); + expect(exitCode).toBe(1); + }); +});